为什么选择合成优于继承?每种方法都有哪些权衡取舍?什么时候应该选择继承而不是作文?
答案 0 :(得分:1091)
首选组合而不是继承,因为它更具延展性/易于修改,但不使用compose-always方法。使用组合,可以通过依赖注入/动态更改行为setter方法。继承更加严格,因为大多数语言不允许您从多个类型派生。因此,一旦你从TypeA派生,鹅就会或多或少地被煮熟。
我对上述酸的测试是:
e.g。如果不是更多的话,塞斯纳双翼飞机将暴露飞机的完整界面。因此,它适合从飞机派生。
e.g。鸟可能只需要飞机的飞行行为。在这种情况下,将它作为接口/类/两者提取出来并将其作为两个类的成员是有意义的。
更新:刚回到我的回答,现在看来,如果没有特别提及Barbara Liskov的Liskov Substitution Principle作为'我是否应继承此类型?“
答案 1 :(得分:380)
将遏制视为具有关系。汽车“有”引擎,“有”名字等等。
将继承视为是关系。汽车“是”汽车,人是“哺乳动物”等。
我不相信这种方法。我直接从Second Edition of Code Complete到Steve McConnell,第6.3节。
答案 2 :(得分:197)
如果您了解其中的差异,则更容易解释。
这方面的一个例子是没有使用类的PHP(特别是在PHP5之前)。所有逻辑都在一组函数中编码。您可以包含其他包含帮助程序函数的文件,并通过在函数中传递数据来执行您的业务逻辑。随着应用程序的增长,这可能非常难以管理。 PHP5试图通过提供更多面向对象的设计来解决这个问题。
这鼓励使用课程。继承是OO设计的三个原则之一(继承,多态,封装)。
class Person {
String Title;
String Name;
Int Age
}
class Employee : Person {
Int Salary;
String Title;
}
这是继承工作。员工“是”人或继承人。所有继承关系都是“is-a”关系。 Employee还会从Person隐藏Title属性,这意味着Employee.Title将返回Employee的Title而不是Person。
组合比继承更受青睐。简单地说,你会有:
class Person {
String Title;
String Name;
Int Age;
public Person(String title, String name, String age) {
this.Title = title;
this.Name = name;
this.Age = age;
}
}
class Employee {
Int Salary;
private Person person;
public Employee(Person p, Int salary) {
this.person = p;
this.Salary = salary;
}
}
Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);
组合通常是“拥有”或“使用”关系。这里Employee类有一个Person。它不会从Person继承,而是将Person对象传递给它,这就是它“拥有”Person的原因。
现在说你要创建一个Manager类型,最后得到:
class Manager : Person, Employee {
...
}
这个例子可以正常工作,但是,如果Person和Employee都声明Title
怎么办? Manager.Title应该返回“运营经理”还是“先生”?在构图下,这种歧义可以更好地处理:
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
Manager对象由Employee和Person组成。标题行为取自员工。这种明确的构图消除了其他事物的歧义,你会遇到更少的错误。
答案 3 :(得分:112)
由于继承提供了所有无可否认的好处,这里有一些缺点。
继承的缺点:
另一方面,对象组合在运行时通过获取对其他对象的引用的对象来定义。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并且将被迫尊重彼此的接口。在这种情况下,实现依赖性将比继承的情况少得多。
答案 4 :(得分:79)
另一个非常务实的原因是,优先考虑组合而不是继承,这与域模型有关,并将其映射到关系数据库。将继承映射到SQL模型真的很难(最终会遇到各种各样的hacky变通方法,比如创建不常用的列,使用视图等)。一些ORML试图解决这个问题,但它总是很快变得复杂。可以通过两个表之间的外键关系轻松地对组合进行建模,但继承更加困难。
答案 5 :(得分:73)
虽然简而言之,我同意“更喜欢继承作品”,但对我而言,这听起来像“喜欢土豆而不是可口可乐”。有继承的地方和组成的地方。你需要了解差异,然后这个问题就会消失。对我来说真正意味着“如果你要继承 - 再考虑一下,你很可能需要作文”。
当你想吃的时候,你应该更喜欢土豆而不是可口可乐,而当你想要喝的时候,你可以选择土豆可乐加上土豆。
创建子类不仅仅意味着调用超类方法的方便方法。当子类“is-a”超类在结构和功能上都应该使用继承,当它可以用作超类时,你将使用它。如果不是这种情况 - 它不是继承,而是其他东西。组合是指您的对象由另一个组成,或与它们有某种关系。
所以对我来说,看起来如果有人不知道他是否需要继承或成分,真正的问题是他不知道他是想喝酒还是吃饭。更多地考虑您的问题域,更好地理解它。
答案 6 :(得分:55)
继承非常诱人,特别是来自程序性的土地,它看起来往往看起来很优雅。我的意思是我需要做的就是将这一点功能添加到其他类中,对吧?好吧,其中一个问题是
您的基类通过以受保护成员的形式将实现细节暴露给子类来打破封装。这使您的系统变得僵硬和脆弱。然而,更为悲惨的缺陷是新的子类带来了继承链的所有包袱和意见。
文章Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinder,在C#中介绍了这个例子。它显示了在应该使用组合时如何使用继承以及如何重构它。
答案 7 :(得分:39)
在Java或C#中,对象在实例化后无法更改其类型。
因此,如果您的对象需要显示为不同的对象或根据对象状态或条件而有所不同,请使用合成:请参阅State和Strategy设计模式。
如果对象需要属于同一类型,则使用继承或实现接口。
答案 8 :(得分:31)
就个人而言,我学会了总是喜欢构图而不是继承。你可以用继承来解决没有程序问题,你无法用组合来解决这个问题。虽然在某些情况下您可能必须使用接口(Java)或协议(Obj-C)。由于C ++不知道任何这样的事情,你必须使用抽象基类,这意味着你不能完全摆脱C ++中的继承。
组合通常更符合逻辑,它提供更好的抽象,更好的封装,更好的代码重用(特别是在非常大的项目中),并且不太可能在远处破坏任何东西,因为你在代码中的任何地方做了一个孤立的更改。它还可以更容易地维护“单一责任原则”,这通常被概括为“一个类不应该有多个原因要改变。”,并且它意味着每个类都存在于特定目的,并且它应该只有与其目的直接相关的方法。即使项目开始变得非常庞大,使用非常浅的继承树也可以更容易地保持概览。许多人认为继承代表了我们的现实世界,但事实并非如此。现实世界使用的组合比继承更多。你可以握在手中的每一个真实物体都是由其他较小的现实世界物体组成的。
但是,构图有些缺点。如果你完全跳过继承而只关注组合,你会注意到你经常需要编写一些额外的代码行,如果你使用了继承则不需要这些代码行。您有时也被迫重复自己,这违反了 DRY Principle (DRY =不要重复自己)。组合通常也需要委托,而方法只是调用另一个对象的另一个方法而没有围绕此调用的其他代码。这种“双方法调用”(可能很容易扩展到三次或四次方法调用,甚至比这更远)的性能比继承要差得多,在继承中只需继承父级的方法。调用一个继承的方法可能与调用一个非继承的方法同样快,或者它可能稍慢,但通常仍然比两个连续的方法调用快。
您可能已经注意到大多数OO语言不允许多重继承。虽然有几种情况下多重继承可以真正为你买点东西,但这些只是规则的例外。每当你遇到你认为“多重继承将是解决这个问题的一个非常酷的功能”的情况时,你通常都应该重新考虑继承,因为即使它可能需要一些额外的代码行基于构图的解决方案通常会变得更加优雅,灵活和面向未来。
继承真的是一个很酷的功能,但我担心它在过去几年里被滥用了。人们将遗传视为可以钉住所有东西的一把锤子,无论它是钉子,螺钉还是完全不同的东西。
答案 9 :(得分:20)
我的一般经验法则:在使用继承之前,请考虑合成是否更有意义。
原因:子类化通常意味着更多的复杂性和连通性,即更难以改变,维护和扩展而不会出错。
更完整,更具体的answer from Tim Boudreau太阳:
我认为使用继承的常见问题是:
- 无辜的行为会产生意想不到的结果 - 这个经典的例子是从超类中调用可覆盖的方法 构造函数,在子类实例字段之前 初始化。在一个完美的世界里,没有人会这样做。这是 不是一个完美的世界。
- 它为子类提供了不正确的诱惑,以便对方法调用的顺序做出假设等等 - 这种假设往往不会 如果超类可能会随着时间的推移而变得稳定。另请参阅my toaster and coffee pot analogy。
- 类变得越来越重 - 你不一定知道你的超类在它的构造函数中做了什么工作,或者它的内存是多少 使用。因此,构建一些无辜的轻量级对象可以 比你想象的要贵得多,如果这种情况可能会随着时间而改变 超类演变
- 它鼓励子类爆炸。类加载成本时间,更多类成本内存。这可能是一个非问题,直到你 处理NetBeans规模的应用程序,但在那里,我们有真实的 例如,由于第一个显示器,菜单很慢 一个菜单触发了大量的类加载。我们通过迁移来解决这个问题 更多的声明性语法和其他技术,但这需要花费时间 也可以修复。
- 这使得以后更改内容变得更加困难 - 如果你公开了一个类,那么交换超类会打破子类 - 这是一个选择,一旦你公开了代码,你就结婚了 至。所以,如果你没有改变你的真实功能 如果你超级,那么你以后可以更自由地改变事物 使用,而不是扩展你需要的东西。举个例子, 子类化JPanel - 这通常是错误的;如果子类是 在某个地方公开,你永远不会有机会重新审视这个决定。如果 它作为JComponent getThePanel()访问,你仍然可以这样做(提示: 公开API中的组件模型。
- 对象层次结构不能扩展(或者使它们扩展比以后规划要困难得多) - 这是经典的“太多层次” 问题。我将在下面讨论,以及AskTheOracle模式如何 解决它(虽然它可能冒犯OOP纯粹主义者)。
...
如果你允许继承,我可以做什么 拿一粒盐是:
- 除了常量
之外,不要公开任何字段- 方法应为抽象或最终
- 从超类构造函数中调用no方法
...
所有这些对小型项目的应用较少,而不是大型项目 私人课程而非公共课程
答案 10 :(得分:17)
继承非常强大,但你不能强制它(参见:circle-ellipse problem)。如果你真的不能完全确定一个真正的“is-a”子类型关系,那么最好选择合成。
答案 11 :(得分:16)
见其他答案。
每当句子“一个Bar是Foo,而一个Bar可以做Foo可以做的所有事情”是有意义的。
传统观点认为,如果句子“a Bar is a Foo”是有意义的,那么选择继承是恰当的是一个很好的暗示。例如,狗是一种动物,因此将Dog类继承自Animal可能是一个很好的设计。
不幸的是,这个简单的“is-a”测试并不可靠。 Circle-Ellipse problem是一个很好的反例:即使圆是一个椭圆,让Circle类从Ellipse继承也是一个坏主意,因为有些椭圆可以做而圆不能。例如,椭圆可以拉伸,但圆圈不能拉伸。因此,虽然您可以ellipse.stretch()
,但您不能拥有circle.stretch()
。
这就是为什么更好的测试是“Bar是Foo,,而Bar可以完成Foo可以做的所有事情”。这真的意味着Foo可以多态使用。 “is-a”测试只是多态使用的必要条件,通常意味着所有Foo的getter在Bar中都有意义。额外的“可以做的一切”测试意味着所有的Foo设置者在Bar中也有意义。当类Bar“is-a”Foo时,这个额外的测试通常会失败,但会为它添加一些约束,在这种情况下你不应该使用继承,因为Foo不能以多态方式使用。换句话说,继承不是关于共享属性,而是关于共享功能。派生类应该扩展基类的功能,而不是限制它。
这相当于Liskov Substitution Principle:
使用指针或对基类的引用的函数必须能够使用派生类的对象而不知道它
答案 12 :(得分:14)
继承在子类和超类之间创建了一个强大的关系;子类必须知道超类的实现细节。当你必须考虑如何扩展它时,创建超级类会更加困难。您必须仔细记录类不变量,并说明其他方法可覆盖的方法在内部使用的内容。
如果层次结构确实表示is-a-relationship,则继承有时很有用。它涉及开放 - 封闭原则,它规定类应该被关闭以进行修改但是可以扩展。这样你就可以拥有多态性;有一个处理超类型及其方法的泛型方法,但是通过动态调度,可以调用子类的方法。这很灵活,有助于创建间接,这在软件中是必不可少的(更少了解实现细节)。
但是,继承很容易被滥用,并且会产生额外的复杂性,类之间存在硬依赖关系。由于层和动态选择方法调用,理解执行程序期间发生的事情也很困难。
我建议使用作曲作为默认值。它更模块化,并提供后期绑定的好处(您可以动态更改组件)。此外,单独测试这些东西也更容易。如果你需要使用一个类中的方法,你就不会被迫成为某种形式(Liskov Substitution Principle)。
答案 13 :(得分:13)
你需要看看鲍勃叔叔The Liskov Substitution Principle班级设计原则中的 SOLID 。 :)
答案 14 :(得分:12)
假设飞机只有两个部分:发动机和机翼 然后有两种方法来设计飞机类。
Class Aircraft extends Engine{
var wings;
}
现在你的飞机可以从固定的机翼开始
并在飞行中将它们改为旋转翅膀。它基本上是
带翅膀的发动机。但是如果我想改变什么呢
飞机上的引擎也是?
基类Engine
暴露一个mutator来改变它的
属性,或者我将Aircraft
重新设计为:
Class Aircraft {
var wings;
var engine;
}
现在,我也可以动态更换我的引擎。
答案 15 :(得分:10)
"优先考虑继承和#34;是一个设计原则,它表示在不适合的情况下不要滥用遗产。
如果两个实体之间不存在真实世界的层次关系,则不要使用继承,而是使用组合。作文代表"有A"关系。
E.g。汽车有车轮,车身,发动机等。但是如果你继承这里,它就变成了CAR A A Wheel - 这是不正确的。
有关进一步说明,请参阅prefer composition over inheritance
答案 16 :(得分:7)
如果要“复制”/公开基类的API,则使用继承。如果您只想“复制”功能,请使用委托。
这方面的一个例子:您想要从列表中创建堆栈。堆栈只有pop,push和peek。鉴于您不希望在堆栈中使用push_back,push_front,removeAt等等功能,您不应该使用继承。
答案 17 :(得分:6)
这两种方式可以很好地共存,并且实际上相互支持。
组合只是模块化:你创建类似于父类的接口,创建新对象并委托对它的调用。如果这些对象不需要彼此了解,那么使用组合物是非常安全和容易的。这里有很多可能性。
但是,如果父类由于某种原因需要访问“子类”为缺乏经验的程序员提供的功能,那么它可能看起来像是一个使用继承的好地方。父类可以调用它自己的抽象“foo()”,它被子类覆盖,然后它可以将值赋给抽象基。
这看起来是一个不错的主意,但在很多情况下,最好只给类一个实现foo()的对象(甚至设置手动提供foo()的值,而不是从某些基类继承新类需要指定函数foo()的类。
为什么?
因为继承是一种移动信息的糟糕方式。
组合在这里有一个真正的优势:关系可以颠倒:“父类”或“抽象工作者”可以聚合任何特定的“子”对象实现某些界面+ 任何孩子都可以在任何其他内部设置父类型,接受类型。并且可以有任意数量的对象,例如MergeSort或QuickSort可以对实现抽象比较接口的任何对象列表进行排序。或者换句话说:实现“foo()”的任何一组对象和可以使用具有“foo()”的对象的其他对象组可以一起玩。
我可以想到使用继承的三个真正原因:
如果这些都是真的,则可能需要使用继承。
使用原因1没有什么不好,在对象上有一个可靠的界面是非常好的。这可以使用组合或继承来完成,没问题 - 如果这个界面很简单而且不会改变。通常,继承在这里非常有效。
如果原因是2号,那就有点棘手了。你真的只需要使用相同的基类吗?一般来说,仅使用相同的基类是不够的,但它可能是您的框架的要求,这是一个无法避免的设计考虑因素。
但是,如果你想使用私有变量,案例3,那么你可能会遇到麻烦。 如果您认为全局变量不安全,那么您应该考虑使用继承来访问私有变量也是不安全的。请注意,全局变量并非都不好 - 数据库本质上是一组全局变量。但如果你能处理它,那就很好了。
答案 18 :(得分:5)
除了考虑之外,还必须考虑对象必须经历的继承的“深度”。超过五或六级继承深度的任何东西都可能导致意外的转换和装箱/拆箱问题,在这些情况下,构建对象可能是明智的。
答案 19 :(得分:5)
如果两个类之间存在 is-a 关系(示例狗是犬科动物),则继续进行继承。
另一方面,当你在两个班级(学生开设课程)或(教师学习课程)之间有 has-a 或某些形容词关系时,你选择了作文。
答案 20 :(得分:5)
理解这一点的一个简单方法是,当您需要类的对象具有与其父类相同的接口时,应该使用继承,以便可以对其进行处理作为父类的对象(向上转换)。此外,对派生类对象的函数调用在代码中的任何地方都保持不变,但调用的具体方法将在运行时确定(即低级实现不同,高级界面保持不变。)
当您不需要新课程具有相同的界面时,应该使用作文,即您希望隐藏课程的某些方面。该类用户不需要了解的实现。所以组合更像是支持封装(即隐藏实现),而继承意味着支持抽象(即提供某种东西的简化表示,在这种情况下是相同接口,适用于具有不同内部的一系列类型。
答案 21 :(得分:4)
在invariants can be enumerated中,子类型是合适且更强大的,否则使用函数组合来实现可扩展性。
答案 22 :(得分:4)
我同意@Pavel,当他说,有组成的地方,有继承的地方。
如果你的答案对这些问题都有肯定的话,我认为应该使用继承。
但是,如果您的意图纯粹是代码重用,那么组合很可能是更好的设计选择。
答案 23 :(得分:3)
我听说的一个经验法则是,当它是一个“is-a”关系时,应该使用继承,当它是一个“has-a”时。即便如此,我觉得你应该总是倾向于构图,因为它消除了很多复杂性。
答案 24 :(得分:3)
从不同的角度为新程序员解决这个问题:
当我们学习面向对象的编程时,通常会尽早传授继承,因此它被视为解决常见问题的简单方法。
我有三个类都需要一些常用功能。所以,如果我 写一个基类,并让它们都继承自它,然后它们会 都有这个功能,我只需要保持一次 的地方。
这听起来很棒,但在实践中,由于以下几个原因之一,它几乎永远不会有效:
最后,我们将代码绑在一些困难的结上并从中得不到任何好处,除了我们说,“很酷,我学会了继承,现在我用它了。”这并不意味着居高临下,因为我们都做到了。但我们都这样做是因为没有人告诉我们不要。
每当有人向我解释“赞成组合而非继承”时,我每次尝试使用继承在类之间共享功能时都会想到,并且意识到大多数时候它并没有真正起作用。
解毒剂是Single Responsibility Principle。把它想象成一种约束。我的班级必须做一件事。我必须能够给我的班级一个名字,以某种方式描述它做的一件事。 (所有内容都有例外,但是当我们学习时,绝对规则有时会更好。)因此我不能编写一个名为ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
的基类。我需要的任何不同功能必须在它自己的类中,然后其他需要该功能的类可以依赖于该类,不继承它。
存在过度简化的风险,即构成 - 组成多个类一起工作。一旦我们形成这种习惯,我们发现它比使用继承更灵活,可维护和可测试。
答案 25 :(得分:3)
继承是代码重用的一个非常强大的机制。但需要正确使用。我会说如果子类也是父类的子类型,则正确使用继承。如上所述,Liskov替代原则是这里的关键点。
子类与子类型不同。您可以创建不是子类型的子类(这是您应该使用合成的时候)。要了解子类型是什么,让我们开始解释类型是什么。
当我们说数字5是整数类型时,我们声明5属于一组可能的值(作为示例,请参阅Java原始类型的可能值)。我们还指出,我可以对加值和减法等值执行一组有效的方法。最后我们说明有一组总是满足的属性,例如,如果我添加值3和5,我将得到8作为结果。
再举一个例子,考虑抽象数据类型,整数集和整数列表,它们可以容纳的值仅限于整数。它们都支持一组方法,如add(newValue)和size()。并且它们都具有不同的属性(类不变),集合不允许重复,而List允许重复(当然还有其他属性,它们都满足)。
子类型也是一种类型,它与另一种类型有关系,称为父类型(或超类型)。子类型必须满足父类型的特征(值,方法和属性)。该关系意味着在期望超类型的任何上下文中,它可以由子类型替代,而不会影响执行的行为。我们来看看一些代码来举例说明我在说什么。假设我写了一个整数列表(用某种伪语言):
class List {
data = new Array();
Integer size() {
return data.length;
}
add(Integer anInteger) {
data[data.length] = anInteger;
}
}
然后,我将整数集写为整数列表的子类:
class Set, inheriting from: List {
add(Integer anInteger) {
if (data.notContains(anInteger)) {
super.add(anInteger);
}
}
}
我们的整数类集是整数列表的子类,但不是子类型,因为它不满足List类的所有功能。满足方法的值和签名,但属性不满足。 add(Integer)方法的行为已经明确更改,而不是保留父类型的属性。从您的课程客户的角度思考。它们可能会收到一组整数,其中需要一个整数列表。客户端可能希望添加值并将该值添加到List中,即使该值已存在于List中。但如果价值存在,她就不会得到那种行为。对她来说太大了!
这是不正确使用继承的典型示例。在这种情况下使用组合。
(来自use inheritance properly的片段)。
答案 26 :(得分:2)
组合v / s继承是一个广泛的主题。没有真正的答案,因为我认为这一切都取决于系统的设计。
一般来说,对象之间的关系类型提供了更好的信息来选择其中一个。
如果关系类型是“IS-A”关系,那么继承是更好的方法。 否则关系类型是“HAS-A”关系,那么组合将更好地接近。
完全取决于实体关系。
答案 27 :(得分:1)
正如许多人所说,我将首先检查 - 是否存在“is-a”关系。如果存在,我通常会检查以下内容:
是否可以实例化基类。也就是说,基类是否可以是非抽象的。如果它可以是非抽象的,我通常更喜欢组合
例如1.会计是员工。但是我不使用继承,因为可以实例化Employee对象。
E.g 2.预订是 SellingItem。 SellingItem无法实例化 - 它是抽象概念。因此我将使用inheritacne。 SellingItem是抽象基类(或C#中的接口)
您如何看待这种方法?
另外,我支持Why use inheritance at all?
中的@anon回答中说使用继承的主要原因不是作为一种组合形式 - 因此你可以获得多态行为。如果您不需要多态,则可能不应该使用继承。
继承问题是它可以用于两个正交目的:
接口(用于多态)
实现(代码重用)
<强>参考强>
答案 28 :(得分:1)
尽管首选的是Composition,但我想强调继承的优点以及 Composition 的缺点。
继承优势:
它建立了逻辑“ IS A”关系。如果 Car 和 Truck 是两种类型的 Vehicle (基类),则子类 IS A 基类。
即。
汽车是车辆
卡车是车辆
通过继承,您可以定义/修改/扩展功能
作文的缺点:
e.g。如果 Car 包含 Vehicle ,并且您必须获得已在 Vehicle 中定义的 Car 的价格,你的代码就像这样
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
答案 29 :(得分:0)
我看到没有人提到diamond problem,这可能与继承有关。
一目了然,如果B类和C类继承A并且都覆盖方法X和第四类D,则继承B和C,并且不覆盖X,XD的哪个实现应该使用?
Wikipedia提供了对此问题中讨论主题的精彩概述。
答案 30 :(得分:0)
你想强迫自己(或其他程序员)坚持什么,以及何时你想让自己(或其他程序员)更自由。有人认为,当你想强迫某人处理/解决某个特定问题时,继承是有帮助的,这样他们就不会走错方向。
Is-a
和Has-a
是一条有用的经验法则。
答案 31 :(得分:-2)
这条规则完全是胡说八道。为什么?
原因是在每种情况下都可以判断是使用合成还是继承。 这是由一个问题的答案决定的:“这是别的东西”或“有 别的东西“。
你不能“喜欢”将某些东西变成别的东西或者拥有别的东西。严格的逻辑规则适用。
此外,没有“人为的例子”,因为在任何情况下都可以给出这个问题的答案。
如果你不能回答这个问题,还有其他问题。 这包括 classess的重叠责任通常是wrong use of interfaces的结果,而不是通过在不同的classess中重写相同的代码。
为了避免这种情况,我还建议为课程使用好名字,这完全类似于他们的职责。