扩展接口模式

25

在 .Net 3.5 中的新扩展允许从接口中分离功能。

比如,在 .Net 2.0 中:

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }

    List<IChild> GetChildren()
}

可以(在3.5中)变成:

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }
}

public static class HaveChildrenExtension {
    public static List<IChild> GetChildren( this IHaveChildren ) {
        //logic to get children by parent type and id
        //shared for all classes implementing IHaveChildren 
    }
}

这个机制对于许多接口来说似乎是更好的。它们不再需要抽象基类来共享这段代码,从功能上来看代码仍然有效。这可以使代码更易于维护和测试。
唯一的缺点是抽象基类的实现可能是虚拟的,但这能否得到解决(一个实例方法是否会隐藏具有相同名称的扩展方法?这样做是否会使代码混乱?)
还有其他原因不经常使用这种模式吗?
澄清:
是的,我看到使用扩展方法的趋势是到处都有它们。如果没有进行大量同行评审,我会特别小心在.Net值类型上使用任何一个(我认为我们仅在字符串上有一个.SplitToDictionary() - 类似于.Split(),但也带有键值分隔符)
我认为这里有整个最佳实践的辩论;-)
(顺便说一句:DannySmurf,你的私人消息听起来很可怕。)
我在这里特别询问了先前我们使用接口方法的扩展方法的使用。
我试图避免大量级别的抽象基类 - 实现这些模型的类大多已经有了基类。我认为这个模型可能比添加其他对象层次结构更易于维护和不过度耦合。
这是 MS 对 Linq 的 IEnumerable 和 IQueryable 所做的吗?
11个回答

10

扩展方法应该只用作扩展,任何关键的结构/设计相关的代码或非平凡操作都应该放在一个组成/继承自类或接口的对象中。

一旦另一个对象尝试使用扩展的对象,他们将看不到这些扩展,并且可能需要重新实现/重新引用它们。

传统的智慧认为:扩展方法只应用于:

  • 实用程序类,如Vaibhav提到的
  • 扩展密封的第三方API

9
我认为明智地使用扩展方法可以使接口与(抽象)基类处于更平等的位置。
版本控制。基类比接口具有一个优点,即您可以在以后的版本中轻松添加新的虚拟成员,而向接口添加成员将会破坏针对旧版本库构建的实现者。相反,需要创建一个带有新成员的接口的新版本,并且库将不得不解决或限制仅实现原始接口的遗留对象的访问。
以一个具体的例子来说,第一个版本的库可能定义如下的接口:
public interface INode {
  INode Root { get; }
  List<INode> GetChildren( );
}

一旦库已发布,我们不能修改接口,否则会破坏当前用户的使用。相反,在下一次发布中,我们需要定义一个新的接口来添加额外的功能:

public interface IChildNode : INode {
  INode Parent { get; }
}

然而,只有使用新库的用户才能实现新接口。为了与旧代码进行配合,我们需要改进旧实现方式,这正是扩展方法能够很好处理的:

public static class NodeExtensions {
  public INode GetParent( this INode node ) {
    // If the node implements the new interface, call it directly.
    var childNode = node as IChildNode;
    if( !object.ReferenceEquals( childNode, null ) )
      return childNode.Parent;

    // Otherwise, fall back on a default implementation.
    return FindParent( node, node.Root );
  }
}

现在新库的所有用户都能将传统和现代实现同等对待。 < br/> 重载。 扩展方法还可以在为接口方法提供重载方面发挥作用。您可能有一个带有几个参数来控制其操作的方法,其中只有前两个参数在90%情况下是重要的。由于C#不允许为参数设置默认值,因此用户必须每次调用完全参数化的方法,或者每个实现都必须实现核心方法的平凡重载。
相反,扩展方法可用于提供平凡的重载实现:
public interface ILongMethod {
  public bool LongMethod( string s, double d, int i, object o, ... );
}

...
public static LongMethodExtensions {
  public bool LongMethod( this ILongMethod lm, string s, double d ) {
    lm.LongMethod( s, d, 0, null );
  }
  ...
}

请注意,这两种情况都是根据接口提供的操作编写的,并涉及微不足道或众所周知的默认实现。也就是说,您只能继承一次类,并且有针对性地使用扩展方法可以提供一种有价值的方式来处理基类提供但接口缺少的某些细节 :)
编辑: Joe Duffy 的相关文章:扩展方法作为默认接口方法实现

6

我认为扩展方法最好取代的是每个项目中都会找到的那些实用程序类。

至少目前来看,我觉得扩展方法的任何其他用途都会在工作场所引起混乱。

以上仅代表个人意见。


2
我认为使用扩展方法将域/模型和UI/视图功能分离是一件好事,尤其是它们可以存在于不同的命名空间中。
例如:
namespace Model
{
    class Person
    {
        public string Title { get; set; }
        public string FirstName { get; set; }
        public string Surname { get; set; }
    }
}

namespace View
{
    static class PersonExtensions
    {
        public static string FullName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName + " " + p.Surname;
        }

        public static string FormalName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName[0] + ". " + p.Surname;
        }
    }
}

这样,扩展方法就可以像XAML数据模板一样使用。虽然无法访问类的私有/受保护成员,但它允许在整个应用程序中维护数据抽象而无需过多的代码复制。


我也喜欢zooba的这种方法,但是我采用了部分类的方式,其中部分类包含了所有的“扩展”。 - user1704366

2

再多说一点。

如果多个接口具有相同的扩展方法签名,则需要显式将调用方转换为一个接口类型,然后调用该方法。例如:

((IFirst)this).AmbigousMethod()

同意,尽管如果你必须这样做,我建议要么接口设计不好,要么你的类不应该同时实现它们两个。 - Keith

2

扩展接口并没有什么问题,事实上这正是LINQ的工作方式,通过向集合类添加扩展方法来扩展接口。

尽管如此,你应该只在需要在实现该接口的所有类之间提供相同功能且该功能不属于任何派生类的“官方”实现时才使用扩展接口。如果为每个需要新功能的派生类型编写扩展方法是不切实际的,那么扩展接口也是很好的选择。


1

哎呀,不要扩展接口。
接口是一个干净的契约,类应该实现它,你对这些类的使用必须限制在核心接口中,才能正确地工作。

这就是为什么你总是将接口声明为类型而不是实际的类。

IInterface variable = new ImplementingClass();

是吗?

如果你真的需要一个带有额外功能的合同,抽象类就是你的好伙伴。


1
我看到很多人主张使用基类来共享公共功能。但要小心,你应该优先选择组合而不是继承。继承应该只用于多态,从建模角度来看才有意义。它并不是一个好的代码复用工具。
至于问题:要小心在这样做时的局限性 - 例如在所示的代码中,使用扩展方法来实现GetChildren effectively会“封闭”这个实现,并且不允许任何IHaveChildren实现根据需要提供自己的实现。如果这没问题,那么我对扩展方法的方式就不太介意。这并不是铁板一块,通常在需要更大灵活性时可以很容易地重构。
为了更大的灵活性,使用策略模式可能更可取。像这样:
public interface IHaveChildren 
{
    string ParentType { get; }
    int ParentId { get; }
}

public interface IChildIterator
{
    IEnumerable<IChild> GetChildren();
}

public void DefaultChildIterator : IChildIterator
{
    private readonly IHaveChildren _parent;

    public DefaultChildIterator(IHaveChildren parent)
    {
        _parent = parent; 
    }

    public IEnumerable<IChild> GetChildren() 
    { 
        // default child iterator impl
    }
}

public class Node : IHaveChildren, IChildIterator
{ 
    // *snip*

    public IEnumerable<IChild> GetChildren()
    {
        return new DefaultChildIterator(this).GetChildren();
    }
}

1

Rob Connery(Subsonic和MVC Storefront)在他的Storefront应用程序中实现了类似IRepository的模式。它不完全是上面提到的模式,但确实有一些相似之处。

数据层返回IQueryable,这使得消费层可以在其上应用过滤和排序表达式。奖励是能够指定一个单独的GetProducts方法,然后在消费层中适当地决定您想要的排序、过滤甚至只是特定范围的结果。

这不是传统的方法,但非常酷,绝对是DRY的一个案例。


0
我需要解决类似的问题: 我想将一个List<IIDable>传递给扩展函数,其中IIDable是具有long getId()函数的接口。 我尝试使用GetIds(this List<IIDable> bla),但编译器不允许我这样做。 我改用模板,然后在函数内部进行类型转换为接口类型。我需要这个函数来处理一些linq到sql生成的类。
希望这可以帮到你 :)
    public static List<long> GetIds<T>(this List<T> original){
        List<long> ret = new List<long>();
        if (original == null)
            return ret;

        try
        {
            foreach (T t in original)
            {
                IIDable idable = (IIDable)t;
                ret.Add(idable.getId());
            }
            return ret;
        }
        catch (Exception)
        {
            throw new Exception("Class calling this extension must implement IIDable interface");
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接