2012-07-11 50 views
6

背景:本着"program to an interface, not an implementation"Haskell type classes的精神,作为一个编码实验,我正在考虑创建一个主要建立在接口和扩展方法组合基础上的API意味着什么。我有两条准则:“使用扩展方法编程接口”:它何时变得太过分?

  1. 只要有可能就避免类继承。接口应实现为sealed class es。
    (这是出于两个原因:首先,因为子类化引发了一些关于如何在其派生类中指定和强制执行基类的契约的令人讨厌的问题;其次,这是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!" 

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

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

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

+0

我想一个更好的地方开始将IEnumerable的''拉泽而不是一个人为的例子。 – ChaosPandion 2012-07-11 21:42:19

+0

好吧,你通过使'IRottenApple'扩展'IApple'违反了规则#1(虽然它只是声明了类的继承) – 2012-07-11 21:43:46

+0

@John:我不这么认为。我相信* interface *继承避免了许多子类化的问题。 (例如:是否必须从派生类中调用重写的方法?当您不允许子类化时,该问题就会消失。) – stakx 2012-07-11 21:50:04

回答

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

如果扔掉苹果的过程中无论是何种苹果的一样的,操作是首要以扩展方法实施的候选人。

+0

+1。隐藏与突出显示区别。附注我想你应该说“使用接口方法”。而不是“使用虚拟方法”。 – 2012-07-11 22:12:51

+0

+1和: - D.但是,这不是通过多态性抽象的目的,使不同的实现向外看起来是相同的吗?也就是说,不适当的抽象*总是*鼓励不同的隐藏方法,比如'ThrowAway'? – stakx 2012-07-11 22:22:43

+0

关于我以前的评论:你的答案的最后一段提供了一个很好的指导方针来区分你的两个案例。我在上面的评论中提到了_“不同的实现”_,同时给出了一个实现总是相同的例子。谢谢你的回答。 – stakx 2012-07-11 23:14:26

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()。这是飞行物体的基本方法。创建扩展方法确实没有意义。

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

+0

我的例子让我担心的是,代码建议'apple.Eat'会打印一个投诉,因为在通常的虚拟调度情况下会发生这种情况;然而,正如你正确地指出的那样,扩展方法不会这样工作。 - 假设吃苹果不是'IApple'类型的基本操作,并且'Eat'因此被正确地实现为扩展方法。但是,我的准则(2)有什么问题?为什么会让我陷入这种意想不到的境地?我将如何适应它,以便在有人遇到它之前检测并防止这些*? – stakx 2012-07-11 22:49:35

+0

如果Apple和RottenApple在一起购买篮子会怎么样?他们都会很好吃! – 2012-07-13 18:09:10

+0

@stakx我认为这只是一个基本的理解,扩展方法实际上只是静态方法。因此,当编译器试图搜索一个方法签名时,它会尽可能地找到匹配的类型。 'IRottenApple'比'IApple'更接近匹配,因此它使用'IRottenApple'的方法将被用于'IApple'。 – 2012-07-13 20:01:46

0

我注意到,C#扩展方法可以非常类似于C++非成员非友元函数以下列方式:两个Scott MeyersHerb Sutter要求在C++中,封装有时增加制作功能一个班级成员:

“如果可能,更喜欢将函数作为非成员非友人写入。”Summary of Herb Sutter's GotW #84

(萨特证明他article about the Interface Principle这种方法。)早在1991年

,斯科特迈尔斯甚至制定了一个算法来决定一个函数是否应该是一个成员函数,友元函数,或者非成员非友元函数:

如果f需要是虚拟
    使fC一个成员函数;
如果别的foperator>>operator<<
    使f非成员函数;
    如果f需要访问的C非公共成员)
        使fC朋友;
否则,如果f需要键入其转换最左边的参数
    使f非成员函数;
    如果f需要访问的C非公共成员)
        使fC朋友;
否则如果
    使ff可以通过C的公共接口来实现)非成员函数;
其他
    使fC一个成员函数;

Scott Meyers expanded algorithm from 1998(格式化)

有些显然是专门针对C++,但它应该是很容易找到的C#语言类似的算法。 (作为开始,friend可以在C#中用internal访问修饰符近似;“非成员函数”可以是扩展方法或其他静态方法)。

算法没有说的是什么时候或为什么, f“需要虚拟”。@dasblinkenlight's answer对解释这一点有很大的帮助。

堆栈溢出

相关的问题: