扩展接口模式

时间:2008-08-11 18:13:44

标签: c# .net-3.5 extension-methods

.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()但是拿了一把钥匙-value delimiter too)

我认为那里有一个完整的最佳实践辩论; - )

(顺便说一下:DannySmurf,你的PM听起来很可怕。)

我在这里特别询问使用扩展方法,以前我们有接口方法。


我试图避免许多级别的抽象基类 - 实现这些模型的类大多已经有了基类。我认为这个模型可以比添加更多的对象层次结构更易于维护和更少过度耦合。

这是MS对Linq的IEnumerable和IQueryable做了什么?

11 个答案:

答案 0 :(得分:11)

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

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

传统观点认为,扩展方法只能用于:

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

答案 1 :(得分:8)

我认为明智地使用扩展方法会将接口置于与(抽象)基类更加相等的位置。


版本控制。基类的一个优点是,您可以在以后的版本中轻松添加新的虚拟成员,而向界面添加成员将破坏针对旧版本库构建的实施者。相反,需要创建具有新成员的新版本的接口,并且库必须解决或限制对仅实现原始接口的旧对象的访问。

作为一个具体的例子,库的第一个版本可能定义如下的接口:

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 );
  }
}

现在,新库的所有用户都可以完全相同地处理旧版和现代版。


过载。扩展方法有用的另一个方面是为接口方法提供重载。您可能有一个方法具有多个参数来控制其操作,其中只有前一个或两个在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的相关帖子:Extension methods as default interface method implementations

答案 2 :(得分:6)

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

至少目前,我觉得任何其他扩展方法的使用都会导致工作场所混乱。

我的两位。

答案 3 :(得分:3)

我认为使用扩展方法将域/模型和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数据模板使用。您无法访问该类的私有/受保护成员,但它允许维护数据抽象,而不会在整个应用程序中进行过多的代码重复。

答案 4 :(得分:2)

我看到很多人主张使用基类来共享通用功能。小心这一点 - 你应该赞成组合而不是继承。从建模的角度来看,继承只应用于多态。它不是代码重用的好工具。

关于问题:在执行此操作时要了解限制 - 例如在显示的代码中,使用扩展方法有效地实现GetChildren'密封'此实现,并且不允许任何IHaveChildren impl提供自己的需要。如果这没关系,那么我不介意扩展方法的方法那么多。它不是一成不变的,通常可以在以后需要更大的灵活性时轻松重构。

为了获得更大的灵活性,使用策略模式可能更为可取。类似的东西:

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();
    }
}

答案 5 :(得分:2)

扩展接口没有任何问题,实际上这就是LINQ如何将扩展方法添加到集合类中。

话虽如此,你真的应该只在你需要在实现该接口的所有类中提供相同功能并且该功能不是(并且可能不应该是)“官方”实现的一部分的情况下才这样做任何派生类。如果为每个可能需要新功能的派生类型编写扩展方法是不切实际的,那么扩展接口也是很好的。

答案 6 :(得分:2)

多一点。

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

((IFirst)this).AmbigousMethod()

答案 7 :(得分:1)

哎哟。请不要扩展接口 接口是一个类应该实现的干净合同,并且您对的类的使用必须仅限于核心接口中的内容才能使其正常工作。

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

IInterface variable = new ImplementingClass();

右?

如果你真的需要一份具有一些附加功能的合同,抽象类就是你的朋友。

答案 8 :(得分:1)

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

数据层返回IQueryable,允许消费层在其上应用过滤和排序表达式。例如,奖金可以指定单个GetProducts方法,然后在消费层中适当地决定您希望如何排序,过滤甚至只是特定范围的结果。

不是传统方法,但非常酷,绝对是DRY的情况。

答案 9 :(得分:0)

我需要解决类似问题: 我想要一个List&lt; IIDable&gt;传递给扩展函数,其中IIDable是一个具有long getId()函数的接口。 我尝试使用GetIds(此List&lt; IIDable&gt; 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");
        }

答案 10 :(得分:0)

我可以看到的一个问题是,在一家大公司中,这种模式可能会让代码变得困难(如果不是不可能),任何人都可以理解和使用。如果多个开发人员不断地将他们自己的方法添加到现有的类中,与这些类分开(并且,上帝帮助我们所有人,甚至是BCL类),我可以看到代码库很快失去控制。

即使在我自己的工作中,我也能看到这种情况发生,我的PM希望将我们工作的所有代码添加到UI或数据访问层,我完全可以看到他坚持使用20或30种方法被添加到System.String,它只与字符串处理相关。