DRY与“喜欢遏制继承”

时间:2009-05-01 22:18:58

标签: design-patterns oop

OO设计的一般规则是,您应该建模 - 使用继承的关系,并使用包含/聚合和转发/委派来建立关系。通过GoF的警告,你应该通常倾向于遏制继承,这可能会进一步缩小,或许可以说,如果你能在某一特定情况下对任何一个人提出强有力的理由,那么由于维护,这种遏制通常应得到点头。问题继承有时会导致。

我理解这种思维背后的原因,我并不一定不同意。但是,当我看到一个包含大量方法的类时,每个方法只转发到一些实例变量,我看到了一种代码重复的形式。在我看来,代码重复是最终的代码味道。重新实现一个庞大的方法协议只是因为两个类之间的关系并不严格 - 似乎有点矫枉过正。这是添加到系统中的额外的,不必要的代码,现在需要像系统的任何其他部分一样进行测试和记录的代码 - 如果您刚刚继承,则可能不需要编写的代码。

坚持这种遏制继承原则的成本是否超过了它的好处?

9 个答案:

答案 0 :(得分:19)

几乎所有东西的成本都可以超过它的好处。坚持不懈,没有例外规则总会让你陷入困境。通常,使用(如果您的语言/运行时支持它)反射是一个坏主意,以便获得对您的代码不可见的变量的访问。这是一种不好的做法吗?和所有事情一样,

it depends.

组合有时比直接继承更灵活或更容易维护吗?当然。这就是它存在的原因。同样,与纯组合体系结构相比,继承可以更灵活或更易于维护。然后你有接口或(取决于语言)多重继承。

这些概念都不是坏事,人们应该意识到这样一个事实,即有时候我们自己缺乏理解或对变革的抵制会导致我们制造任意规则,将某些东西定义为“坏”而没有任何真正的理由这样做

答案 1 :(得分:3)

你必须为手头的问题选择正确的解决方案。有时遗产比遏制更好,有时候不是。你必须使用你的判断,当你无法弄清楚要走哪条路时,写一些代码,看看它有多糟糕。有时候编写一些代码可以帮助你做出一个不明显的决定。

一如既往:正确的答案取决于许多不能制定严格规则的因素。

答案 2 :(得分:1)

是的,你所看到的是来自宇宙不同角落的设计范式的可怕碰撞:GoF的聚合/构图利用与“得墨忒耳定律”的碰撞。

我记录在案,相信在聚合和作曲使用的背景下,the Law of Demeter is an anti-pattern

与此相反,我认为像person->brain->performThought()这样的结构是完全正确和恰当的。

答案 3 :(得分:1)

我同意你的分析,并赞成在那些情况下继承。在我看来,这与盲目实现愚蠢的访问器以提供封装的天真努力有点相同。我认为这里的教训是,根本没有任何通用规则总是适用。

答案 4 :(得分:1)

虽然我同意所有说“它依赖”的人 - 并且它在某种程度上也依赖于语言 - 但我很惊讶没有人提到过(在Allen Holub的名言中)“{{ 3}}”。当我第一次阅读那篇文章时,我不得不承认我有点推迟,但他是对的:无论语言如何,是一种关系是关于最紧密的耦合形式。高大的继承链是一种独特的反模式。因此,虽然说你应该总是避免继承是不对的,但是应该谨慎使用它(对于类 - 建议使用接口继承)。我的面向对象 - noob趋势是将所有内容建模为一个继承链,是的,它确实减少了代码重复,但实际上是紧耦合的代价,这意味着不可避免的维护难题会在未来发生。

他的文章更好地解释了为什么继承是紧密耦合,但基本思想是-a要求每个子类(和孙子等)依赖于实现祖先班。 “编程到界面”是一种众所周知的降低复杂性和协助敏捷开发的策略。您无法真正编程到父类的接口,因为实例该类。

另一方面,使用聚合/合成会强制进行良好的封装,从而使系统的刚性降低。将可重用代码分组到实用程序类中,使用has-a关系链接到它,并且您的客户端类现在正在使用根据合同提供的服务。您现在可以根据自己的内容重构实用程序类;只要你符合界面,你的客户端类就可以保持幸福而不知道变化,并且(重要的是)它不应该被重新编译。

我不是在暗示这是一种宗教,只是一种最佳实践。当然,它们意味着在需要时被打破,但通常有一个很好的理由,那就是“最佳”。

答案 5 :(得分:0)

这可能无法回答你的问题,但有些东西一直困扰着我和Java及其Stack。 就我的知识范围而言,堆栈应该是一个非常简单(或可能是最简单的)容器数据结构,具有三个基本的公共操作:pop,push和peek。你为什么要在Stack insertAt,removeAt等等。有哪些功能? (在Java中,Stack继承自Vector)。

有人可能会说,好吧,至少你不必记录这些方法,但为什么首先不应该使用那些方法呢?

答案 6 :(得分:0)

在某种程度上,这是语言支持的问题。例如,在Ruby中,我们可以实现一个内部使用数组的简单堆栈,如下所示:

class Stack
  extend Forwardable
  def_delegators :@internal_array, :<<, :push, :pop
  def initialize() @internal_array = [] end
end

我们在这里所做的就是声明我们想要使用的其他类功能的子集。真的没有太多的重复。哎呀,如果我们真的想重用所有其他类的功能,我们甚至可以指定它而不重复任何事情:

class ArrayClone
  extend Forwardable
  def_delegators(:@internal_array, 
                  *(Array.instance_methods - Object.instance_methods))
  def initialize() @internal_array = [] end
end

显然(我希望),这不是我一般会写的代码,但我认为它表明它可以完成。在没有简单元编程的语言中,一般来说保持DRY可能会有些困难。

答案 7 :(得分:0)

您可以考虑在抽象基类中实现“装饰器”代码(默认情况下)将所有方法调用转发到包含的对象。然后,根据需要继承抽象装饰器并覆盖/添加方法。

abstract class AbstractFooDecorator implements Foo {
    protected Foo foo;

    public void bar() {
        foo.bar();
    }
}

class MyFoo extends AbstractFooDecorator {
    public void bar() {
        super.bar();
        baz();
    }
}

如果您有许多包装特定类型的类,这至少可以避免重复“转发”代码。

至于指南是否有用,我认为应该把重点放在“偏好”这个词上。显然,有些情况下使用继承是完全合理的。这是一个example of when inheritance should not have been used

  

Hashtable类在JDK 1.2中得到了增强,包括一个新方法entrySet,它支持从Hashtable中删除条目。 Provider类未更新以覆盖此新方法。这种疏忽允许攻击者绕过在Provider.remove中强制执行的SecurityManager检查,并通过简单地调用Hashtable.entrySet方法来删除Provider映射。

该示例强调,继承关系中的类仍然需要进行测试,这与仅仅需要维护/测试“封装”式代码的含义相反 - 维护从另一个继承的类的成本可能不会尽可能便宜。

答案 8 :(得分:-1)

GoF现在是旧文本。取决于您所看到的OO环境(即使这样,很明显您可以在OO不友好的部分中遇到它,例如C-with-classes)。

对于运行时环境,您几乎没有选择,单继承。任何试图绕过其局限性的解决办法都是这样,无论它看起来多么复杂或“酷”。

再次,你会看到这在任何地方都有表现,包括C ++(最有能力的一个),它与C回调接口(这种回调足以引起所有人的注意)。但是,C ++为您提供了具有模板功能的混合和基于策略的设计,因此它偶尔可以帮助进行硬工程。

虽然包含可以为您提供间接的好处,但继承可以为您提供易于访问的组合。选择毒药..聚合端口更好,但总是看起来像违反DRY,而继承可以更容易地重用不同的潜在维护头痛。

真正的问题是,计算机语言或建模工具应该为您提供一个选项,它应该独立于选择而做,因此使其不易出错;但是在让计算机为他们编写程序之前没有多少人建模+没有好的工具(Osla当然不是一个)或者他们的环境正在推动像反思一样愚蠢的东西,IoC和什么不是......这是非常受欢迎的本身就说了很多。

曾经有一个古老的COM技术被一个最好的Doom玩家称为Universal Delegator,但这不是任何人都会采用的那种发展方式。它需要接口肯定(和一般情况下不是真正的硬性要求)。这个想法很简单,它早于处理中断处理。只有类似的aproaches才能让你在两个方面都做得最好,而且它们在脚本作为JavaScript和函数编程方面有些明显(尽管可读性或执行性较差)。