许多小类与逻辑(但)复杂的继承


24

我想知道在良好的OOP设计,干净的代码,灵活性以及将来避免代码异味方面有什么更好的选择。图像情况,您需要将很多非常相似的对象表示为类。这些类没有任何特定的功能,只是数据类,而只是名称(和上下文)不同而已。示例:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

将它们保留在单独的类中以获得更好的可读性,但是要经过多次代码重复会更好,还是使用继承使其更干燥更好?

通过使用继承,您将得到如下所示的结果,但是类名将失去上下文的含义(因为is-a,而不是has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

我知道继承不是也不应该用于代码重用,但是在这种情况下,我看不到任何其他可能的方式来减少代码重复。

Answers:


58

一般规则是“优先选择继承而不是继承”,而不是“避免所有继承”。如果对象具有逻辑关系,并且可以在期望使用A的任何地方使用B,则最好使用继承。但是,如果对象恰好具有相同名称的字段,并且没有域关系,则不要使用继承。

通常,在设计过程中提出“变更原因”的问题很有意义:如果A类获得了额外的字段,您是否希望B类也获得它?如果是这样,则对象共享一个关系,并且继承是一个好主意。如果不是这样,则需要进行少量的重复操作以将不同的概念保留在不同的代码中。


当然,更改的原因是很重要的,但是在什么情况下这些类始终是恒定的呢?
user969153

20
@ user969153:当类真正是常量时,没关系。所有好的软件工程都是为了将​​来的维护者,他们需要进行更改。如果将来没有任何变化,一切都会进行,但是请相信我,除非您为垃圾堆编程,否则您将始终有变化
thiton

@ user969153:话虽如此,“变革的原因”是一种启发。它可以帮助您做出决定,仅此而已。
thiton

2
好答案。更改原因是bob叔叔的SOLID缩写中的S,这将有助于设计好的软件。
克莱(Klee)

4
@ user969153,根据我的经验,“这些类在哪里,并且将始终保持不变?” 不是有效的用例:)我尚未在没有发生意料之外的更改的大型应用程序上工作。
cdkMoose

18

通过使用继承,您将得到如下所示的结果,但是类名将失去上下文的含义(因为is-a,而不是has-a):

究竟。脱离上下文查看:

Class D : C
{
    String age;
}

D有哪些属性?你能记得吗?如果进一步使层次结构复杂化怎么办:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

然后,如果恐怖地想将一个imageUrl字段添加到F而不是E中,该怎么办?您不能从C和E派生它。

我已经看到了一种实际情况,其中许多不同的Component类型都具有一个名称,一个ID和一个Description。但这不是它们本身的本质,这只是一个巧合。每个都有一个ID,因为它们存储在数据库中。名称和说明仅供显示。

出于类似原因,产品还具有ID,名称和说明。但是它们不是组件,也不是组件产品。您将永远不会看到这两件事。

但是,一些开发人员阅读了有关DRY的内容,并决定将其从代码中删除所有重复提示。因此,他将一切都称为产品,并消除了定义这些字段的需要。它们是在“产品”中定义的,需要这些字段的所有内容都可以来自“产品”。

我不能说得足够强烈:这不是一个好主意。

DRY并不是要消除对通用属性的需求。如果对象不是产品,请不要将其称为产品。如果产品不需要描述,就其作为产品的本质而言,请勿将描述定义为产品的属性。

最终,我们遇到了没有说明的组件。因此,我们开始在那些对象中使​​用空描述。不可为空。空值。总是。对于X ...或更高版本Y ...和N的任何产品。

这严重违反了《里斯科夫换人原则》。

DRY永远不要将自己放在可能会在某个地方修复错误而忘记在其他地方完成的位置。这不会发生,因为您有两个具有相同属性的对象。决不。所以不用担心。

如果类的上下文表明您应该这样做(这很少见),那么只有这样,您才应该考虑一个公共的父类。但是,如果它是属性,请考虑为什么不使用接口。


4

继承是实现多态行为的方法。如果您的类没有任何行为,则继承是无用的。在这种情况下,我什至不考虑它们的对象/类,而是结构。

如果您希望能够在行为的不同部分中放置不同的结构,请考虑使用具有get / set方法或属性的接口,例如IHasName,IHasAge等。这将使设计更加整洁,并使这些结构更好地组合一种等级制。


3

这不是接口的作用吗?具有不同功能但具有相似属性的不相关类。

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;

1

您的问题似乎是在不支持多重继承但没有特别指定的语言环境下进行的。如果使用支持多种继承的语言,则仅通过单个继承级别即可轻松解决这一问题。

这是一个如何使用多重继承解决此问题的示例。

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
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.