“构成超越继承”是否违反了“枯燥原则”?


36

例如,考虑我有一个可供其他类扩展的类:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

和一些子类:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

实际上,子类没有任何方法可以覆盖,也不会以通用方式访问HomePage,即:我不会做类似的事情:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

我只想重用登录页面。但是根据https://stackoverflow.com/a/53354,我在这里更喜欢合成,因为我不需要LoginLog接口,所以在这里我不使用继承:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

但问题出在新版本的代码中:

public LoginPage loginPage;

添加新类时重复。而且,如果LoginPage需要使用setter和getter,则需要复制更多代码:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

所以我的问题是,“构成继承”违反了“枯燥原则”吗?


13
有时您既不需要继承也不需要合成,也就是说,有时一个带有几个对象的类就可以完成工作。
Erik Eidt

23
为什么要将所有页面都设为登录页面?也许只有一个登录页面。
Cochese先生18年

29
但是,有了继承,您extends LoginPage到处都会重复。检查伴侣!
el.pescado

2
如果最后一个片段中有很多问题,则可能会过度使用getter和setter。它们倾向于违反封装。
Brian McCutchon '18

4
因此,使您的页面能够被装饰并成为LoginPage装饰器。不再需要重复,只需简单page = new LoginPage(new EditInfoPage())操作即可完成。或者,您可以使用open-closed-principle来制作可以动态添加到任何页面的身份验证模块。有很多方法可以处理代码重复,主要包括查找新的抽象。LoginPage可能是一个不好的名字,您想要确保的是,用户在浏览该页面时已通过身份验证,同时被重定向到LoginPage或显示正确的错误消息(如果不是)。
Polygnome

Answers:


46

嗯,等等,您担心重复

public LoginPage loginPage;

在两个地方违反了DRY?按照这种逻辑

int x;

现在只能在整个代码库的一个对象中存在。eh

记住DRY是一件好事,但是继续吧。除了

... extends LoginPage

在您的替代品中被复制,因此即使对DRY肛交也无济于事。

有效的DRY关注点倾向于集中于在多个位置定义的多个位置所需的相同行为,以使得更改此行为的需求最终需要在多个位置进行更改。在一个地方进行决策,您只需要在一个地方进行更改即可。这并不意味着只有一个对象可以保存对LoginPage的引用。

不要盲目地遵循DRY。如果您要进行复制是因为复制和粘贴比想出一个好的方法或类名容易,那么您可能就错了。

但是,如果您想将相同的代码放在不同的位置,因为该不同的位置要承担不同的责任,并且可能需要独立更改,那么放松DRY的执行并让相同的行为具有不同的标识可能是明智的。禁止魔术数字也是如此。

DRY不仅与代码的外观有关。这是关于不要通过盲目重复来传播想法的细节,从而迫使维护人员使用盲目重复来修复问题。当您试图告诉自己,无意识的重复只是您习惯的事,那就是事情的发展方向是糟糕的。

我认为您真正要抱怨的是样板代码。是的,使用合成而不是继承需要样板代码。没有什么是免费的,您必须编写代码将其公开。样板具有状态灵活性,缩小暴露的接口的能力,为事物赋予适合其抽象级别的不同名称,良好的ol间接性的能力,并且您使用的是外部构成的内容,而不是外部内部,因此您要面对的是普通界面。

但是,是的,这是很多额外的键盘输入。只要我能防止阅读代码时上下跳动继承堆栈的溜溜球问题,那是值得的。

现在不是我拒绝使用继承。我最喜欢的用途之一是为异常赋予新名称:

public class MyLoginPageWasNull extends NullPointerException{}

int x;在一个代码库中最多可以存在零次
K. Alan Bates

@ K.AlanBates 对不起
candied_orange

...我知道你要反驳Point类大声笑。没有人关心Point类。如果我进入您的代码库并看到int a; int b; int x;我将推动删除“您”。
K. Alan Bates

@ K.AlanBates哇。令人遗憾的是,您认为这种行为会使您成为一名优秀的编码人员。最好的编码器并不是可以找到其他人最喜欢的东西的人。它是使其余部分变得更好的一种。
candied_orange

使用无意义的名称是不可能的,并且可能会使该代码的开发人员承担责任而不是资产。
K. Alan Bates

127

对DRY原理的一个普遍误解是,它在某种程度上与不重复代码行有关。DRY原则是“每条知识在系统中必须具有单一,明确,权威的表示形式”。这是关于知识,而不是代码。

LoginPage知道如何绘制页面以进行登录。如果EditInfoPage知道如何执行此操作,那将是违法的。包括LoginPage通孔组成丝毫不违反DRY原理。

DRY原则可能是软件工程中最被滥用的原则,应始终将其视为不复制代码的原则,而不是不复制抽象域知识的原则。实际上,在很多情况下,如果正确应用DRY,那么您将复制代码,这不一定是一件坏事。


27
“单一责任原则”被滥用了很多。“不要重复自己”可能很容易成为
第二名

28
“ DRY”是“ Do n't Repeat Yourself”的方便首字母缩写,它是一个流行短语,但不是原则的名称,该原则的实际名称是“ Once And Only Once”,而这又是一个易用的名称对于需要几个段落来描述的原理。重要的是要理解两段,而不是记住三个字母D,R和Y。
JörgW Mittag

4
@ gnasher729您知道,我实际上同意您的说法,SRP可能被滥用了很多。尽管公平起见,但我认为在许多情况下它们经常被一起滥用。我的理论是,通常不应信任程序员使用带有“易名”的缩写。
wasatz

17
恕我直言,这是一个很好的答案。但是,公平地说:根据我的经验,最常见的DRY违规形式是由复制粘贴编程和仅复制代码引起的。有人曾经告诉我“无论何时要复制粘贴某些代码,如果不能将重复的部分提取到通用函数中,请三思而后行”-这是恕我直言,这是非常好的建议。
布朗

9
滥用复制粘贴当然是违反DRY的驱动力,但仅禁止复制粘贴对于后续DRY的指导性很差。当两个方法具有相同的代码表示不同的知识时,我根本不会后悔。他们承担着两个不同的职责。他们今天恰好具有相同的代码。他们应该自由地独立改变。是的,知识,而不是代码。说得好。我写了其他答案之一,但我会鞠躬。+1。
candied_orange

12

简短的回答:是的,确实可以-在某种程度上可以接受。

乍一看,继承有时可以为您节省一些代码行,因为它的作用是说“我的重用类将以1:1的方式包含所有公共方法和属性”。因此,如果组件中有10个方法的列表,则不必在继承的类的代码中重复这些方法。当在组合场景中应该通过重用组件公开公开这10种方法中的9种时,则必须记下9个委托调用,而将其余的放出来,则无法解决。

为什么可以容忍?看一下在合成场景中复制的方法-这些方法专门委派了对组件接口的调用,因此不包含任何实际逻辑。

DRY原理的核心是避免在代码中在两个位置编码相同的逻辑规则 -因为当这些逻辑规则发生变化时,在非DRY代码中,很容易适应其中一个位置而忽略另一个位置,这引入了一个错误。

但是由于委派调用不包含逻辑,因此通常不会进行此类更改,因此在“优先考虑组合而不是继承”时不会引起真正的问题。即使组件的接口发生了变化(可能会在使用该组件的所有类上引起形式上的变化),编译器也会以一种编译语言告诉我们何时我们忘记更改其中一个调用者。

您的示例的注释:我不知道您HomePage和您的EditInfoPage外观,但是如果它们具有登录功能,并且HomePage(或EditInfoPage LoginPage,则继承可能是此处的正确工具。一个没有争议的例子,即组合将以更明显的方式成为更好的工具,这可能会使事情变得更清楚。

正如您所写,假设既不是HomePage也不EditInfoPageLoginPage,并且一个人想重用后者,那么很可能一个人只需要一部分LoginPage,而不是全部。在这种情况下,比所示方式使用合成更好的方法可能是

  • 将的可重用部分提取LoginPage到其自身的组件中

  • HomePageEditInfoPage现在在内部使用相同的方式重用该组件LoginPage

这样,通常会更加清楚为什么以及何时通过继承进行组合是正确的方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.