扩展接口模式

时间:2020-03-05 18:39:00  来源:igfitidea点击:

.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值类型上进行任何操作,而无需进行大量同行评审(我认为我们在字符串上唯一的一个是与.Split()类似的.SplitToDictionary(),但是要使用密钥值分隔符)

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

(顺便说一句:DannySmurf,PM听起来很吓人。)

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

我试图避免许多抽象基类级别,实现这些模型的类大多数已经具有基类。我认为,与添加更多的对象层次结构相比,该模型可能更易于维护,并且耦合程度更低。

这是MS对IEnumerable和IQueryable for Linq所做的事情吗?

解决方案

回答

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

即使是我自己的工作,我也可以看到这种情况的发生,因为我的PM希望将我们要处理的每一段代码都添加到UI或者数据访问层,我完全可以看到他坚持要添加20或者30种方法仅与字符串处理切线相关的System.String。

回答

我认为扩展方法替代的最好的东西是我们在每个项目中找到的所有实用程序类。

至少到目前为止,我觉得任何其他扩展方法的使用都会在工作场所造成混乱。

我的两位。

回答

扩展方法应仅用于:扩展。任何与关键的结构/设计相关的代码或者重要的操作都应放在由类或者接口组成/继承的对象中。

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

传统观点认为,扩展方法仅应用于:

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

回答

哎哟。请不要扩展接口。
接口是一个类应该实现的干净约定,并且我们对所述类的使用必须限于核心接口中的内容,才能正常工作。

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

IInterface variable = new ImplementingClass();

正确的?

如果我们确实需要带有某些添加功能的合同,那么抽象类是朋友。

回答

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

话虽这么说,我们仅应在需要为实现该接口的所有类中提供相同功能并且该功能不是(并且可能不应该)属于任何派生的"正式"实现的一部分的情况下,才这样做。类。如果为每种可能需要新功能的派生类型编写扩展方法是不切实际的,则扩展接口也很好。

回答

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

回答

我看到很多人主张使用基类来共享通用功能。注意这一点,我们应该更偏重于继承而不是继承。从建模的角度来看,继承应仅用于多态。它不是代码重用的好工具。

至于一个问题:例如,在所示的代码中,请当心这样做的局限性,使用扩展方法来实现GetChildren可以有效地"密封"该实现,并且如果需要,不允许任何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();
    }
}

回答

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

版本控制。基类相对于接口具有的一个优势是,我们可以轻松地在更高版本中添加新的虚拟成员,而将成员添加到接口将破坏针对库的旧版本构建的实现者。相反,需要创建具有新成员的接口的新版本,并且该库将不得不变通或者限制对仅实现原始接口的旧对象的访问。

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

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)的相关文章:扩展方法作为默认接口方法实现

回答

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

数据层返回IQueryable,这允许消费层在其之上应用过滤和排序表达式。额外的好处是,例如,可以指定一个GetProducts方法,然后在使用层中适当地决定我们希望如何进行排序,过滤或者什至只是特定范围的结果。

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

回答

我需要解决类似的问题:
我想将List <IIDable>传递给扩展功能,其中IIDable是具有长getId()函数的接口。
我尝试使用GetIds(此List <IIDable> bla),但编译器不允许我这样做。
我改用模板,然后将函数内部的类型强制转换为接口类型。对于一些linq to 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");
        }

回答

多一点点。

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

((IFirst)this).AmbigousMethod()