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

sta*_*ica 7 c# extension-methods api-design interface

背景:本着"编程接口,而不是实现"Haskell类型的精神,作为编码实验,我在考虑创建一个主要基于接口和扩展组合的API意味着什么方法.我有两条准则:

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

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

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

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!"
Run Code Online (Sandbox Code Playgroud)

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

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

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

das*_*ght 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);
}
Run Code Online (Sandbox Code Playgroud)

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