使用扩展方法“编程到接口”:什么时候走得太远?

时间:2012-07-11 21:38:46

标签: c# interface extension-methods api-design

背景:本着"program to an interface, not an implementation"Haskell type classes的精神,作为编码实验,我正在考虑创建一个主要成立的API意味着什么关于接口和扩展方法的组合。我有两条准则:

  1. 尽可能避免类继承。接口应实现为sealed class es。
    (这有两个原因:首先,因为子类化引发了一些关于如何在派生类中指定和强制执行基类'契约的讨厌问题。其次,这就是Haskell类型类的影响,多态不需要子类化。)

  2. 尽可能避免使用实例方法。如果可以使用扩展方法,则首选这些方法 (这是为了帮助保持接口紧凑:通过其他实例方法的组合可以完成的一切都成为扩展方法。接口中剩下的是核心功能,特别是状态改变方法。)

  3. 问题:我遇到了第二条准则的问题。考虑一下:

    interface IApple { }
    static void Eat(this IApple apple)
    {
        Console.WriteLine("Yummy, that was good!");
    }
    
    interface IRottenApple : IApple { }
    static void Eat(this IRottenApple apple)
    {
        Console.WriteLine("Eat it yourself, you disgusting human, you!");
    }
    
    sealed class RottenApple : IRottenApple { }
    IApple apple = new RottenApple();
    // API user might expect virtual dispatch to happen (as usual) when 'Eat' is called:
    apple.Eat(); // ==> "Yummy, that was good!"
    

    显然,对于预期结果("Eat it yourself…"),Eat应该是常规实例方法。

    问题:关于扩展方法与(虚拟)实例方法的使用,有哪些更精细/更准确的指南?什么时候使用扩展方法“编程到接口”太过分了?在什么情况下实际需要实例方法?

    我不知道是否任何明确的一般规则,所以我不期待一个完美的,普遍的答案。对上述准则(2)的任何有争议的改进都表示赞赏。

3 个答案:

答案 0 :(得分:6)

你的指导原则很好:它已经说“尽可能”。因此,任务实际上是在一些更详细的信息中详细说明“尽可能”的位。

我使用这个简单的二分法:如果添加方法的目的是隐藏子类之间的差异,请使用扩展方法;如果目的是突出差异,请使用虚拟方法。

你的Eat方法是一个在子类之间引入差异的方法的一个例子:吃(或不吃)苹果的过程取决于它是什么类型的苹果。因此,您应该将其实现为实例方法。

尝试隐藏差异的方法示例是ThrowAway

public static void ThrowAway(this IApple apple) {
    var theBin = RecycleBins.FindCompostBin();
    if (theBin != null) {
        theBin.Accept(apple);
        return;
    }
    apple.CutUp();
    RecycleBins.FindGarbage().Accept(apple);
}

如果丢弃苹果的过程与苹果的种类无关,则该操作是以扩展方法实施的主要候选者。

答案 1 :(得分:1)

对我来说,预期的输出是正确的。您将变量类型化(可能使用该错误)作为IApple。

例如:

IApple apple = new RottenApple();
apple.Eat();  // "Yummy, that was good!"
IRottenApple apple2 = new RottenApple();
apple2.Eat(); // "Eat it yourself, you disgusting human, you!"
var apple3 = new RottenApple();
apple.Eat();  // "Eat it yourself, you disgusting human, you!"
  

问题:关于扩展方法与(虚拟)实例方法的使用,哪些是精确/更准确的指南?什么时候使用扩展方法“编程到接口”远吗?在什么情况下实际需要实例方法?

开发应用程序时的个人意见:

当我写一些我可能或其他人可能会消费的内容时,我会使用实例方法。这是因为它是实际类型的要求。考虑具有方法FlyingObject的接口/类Fly()。这是飞行物体的基本基本方法。创建扩展方法确实没有意义。

我使用(很多)Extension方法,但这些方法从不是使用它们扩展的类的要求。例如,我在int上有一个扩展方法,它创建了一个SqlParameter(另外它是内部的)。将该方法作为int的基类的一部分仍然没有意义,它实际上与int是什么或做什么无关。扩展方法是创建一个消耗类/结构的可重用方法的视觉上很好的方法。

答案 2 :(得分:0)

我注意到C#扩展方法可以通过以下方式与C ++非成员非友元函数非常相似:Scott MeyersHerb Sutter都声称在C ++中,封装有时会增加通过使一个函数成为一个类成员:

  

“在可能的情况下,更喜欢将函数编写为非成员非朋友。” - Summary of Herb Sutter's GotW #84

(萨特在他的article about the Interface Principle证明了这种方法。)

早在1991年,Scott Meyers甚至制定了一个算法,用于决定一个函数应该是一个成员函数,一个朋友函数,还是一个非成员非朋友函数:

  

如果 f需要虚拟
  使f成为C 的成员函数;
  其他如果 foperator>>operator<<
  使f成为非成员函数;
  如果 f需要访问C的非公开成员)   f成为C 的朋友;
  其他如果 f需要在其最左侧参数上进行类型转换
  使f成为非成员函数;
  如果 f需要访问C的非公开成员)   f成为C 的朋友;
  其他如果 f可以通过C的公共接口实现
  使f成为非成员函数;
  的其他
  使f成为C 的成员函数;

     

- Scott Meyers expanded algorithm from 1998(重新格式化)

其中一些显然特定于C ++,但是为C#语言找到类似的算法应该相当容易。 (首先,friend可以使用internal访问修饰符在C#中近似;“非成员函数”可以是扩展方法或其他静态方法。)

该算法未说明的是f“何时或为何需要虚拟”。 @dasblinkenlight's answer在某种程度上解释了这一点。


有关Stack Overflow的相关问题: