继承与具有空值的其他属性


12

对于具有可选字段的类,使用继承或可为空的属性更好吗?考虑以下示例:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

要么

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

要么

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

取决于这三个选项的代码为:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

要么

if (book.getColor() != null) { book.getColor(); }

要么

if (book.getColor().isPresent()) { book.getColor(); }

第一种方法对我来说看起来更自然,但由于必须进行转换,因此可能可读性较差。还有其他方法可以实现此行为吗?


1
恐怕解决方案取决于您对业务规则的定义。我的意思是,如果您所有的书都可能有颜色,但是颜色是可选的,那么继承就显得过分,实际上是不必要的,因为您希望所有书都知道存在color属性。另一方面,如果您既可以拥有对色彩一无所知的书籍,又可以拥有具有色彩的书籍,那么创建专门的类也不错。即使那样,我也可能会考虑继承而不是继承。
安迪

为了更具体一点-有2种类型的书籍-一种是彩色的,另一种是彩色的。因此,并非所有书籍都可能有颜色。
Bojan VukasovicTest,

1
如果确实存在书完全没有颜色的情况,那么在您的情况下,继承是扩展彩色书的属性的最简单方法。在这种情况下,您似乎不会通过继承基簿类并向其添加color属性来破坏任何内容。您可以阅读有关liskov替换原理的信息,以了解有关允许扩展类和不允许扩展类的情况的更多信息。
安迪

4
您能从涂色书中识别出您想要的行为吗?您能否在有色书和无色书之间发现一种常见的行为,即有色书的行为应略有不同?如果是这样,这对于具有不同类型的OOP是一个很好的情况,并且其想法是将行为移到类中,而不是从外部询问属性的存在和价值。
埃里克·艾德

1
如果提前知道可能的书本颜色,那么BookColor的Enum字段和BookColor.NoColor的选项如何?
格雷厄姆

Answers:


7

这要视情况而定。特定的示例不现实,因为您不会BookWithColor在真实程序中调用子类。

但是通常,仅对某些子类有意义的属性应仅存在于那些子类中。

例如,如果Book具有PhysicalBookDigialBook作为后代,则PhysicalBook可能具有一个weight属性和DigitalBook一个sizeInKb属性。但是DigitalBook不会weight,反之亦然。Book将不具有任何属性,因为类仅应具有所有后代共享的属性。

一个更好的例子是查看真实软件中的类。该JSlider组件具有字段majorTickSpacing。由于只有一个滑块具有“滴答声”,因此该字段仅对JSlider它及其后代有意义。如果其他同级组件(​​例如字段)JButton有一个majorTickSpacing字段,那将非常令人困惑。


或您可以权衡一下两者的重量,但这可能太多了。
Walfrat '16

3
@Walfrat:让同一个字段代表不同子类中完全不同的东西是一个非常糟糕的主意。
JacquesB '16

如果一本书同时具有物理和数字版本,该怎么办?您必须为每个具有或不具有每个可选字段的排列创建一个不同的类。我认为最好的解决方案只是根据书本的规定允许某些字段被忽略或不被忽略。您已经引用了一种完全不同的情况,即两个类在功能上会有所不同。这不是这个问题的含义。他们只需要对数据结构建模。
4castle

@ 4castle:由于每个版本的价格也可能不同,因此每个版本都需要单独的类。您无法使用可选字段解决此问题。但是我们无法从示例中知道操作员是否希望彩色或“无色”书籍的行为不同,因此在这种情况下不可能知道什么是正确的建模。
JacquesB '16

3
同意@JacquesB。您无法为“建模书籍”提供一个普遍正确的解决方案,因为它取决于您要解决的问题以及您的业务是什么。我认为这是相当肯定地说,定义,你违反Liskov的类层次结构是断然错误(TM)虽然(是啊是啊,你的情况可能是非常特殊和工作的需要(虽然我怀疑))
萨拉

5

似乎没有提到的重要点:在大多数语言中,类的实例不能更改它们所属的类。因此,如果您有一本没有颜色的书,并且想要添加颜色,那么如果使用不同的类,则需要创建一个新对象。然后,您可能需要将对旧对象的所有引用替换为对新对象的引用。

如果“无色书”和“有色书”是同一类的实例,那么添加颜色或删除颜色将不再是问题。(如果您的用户界面显示“有色书”和“无色书”的列表,则该用户界面将必须进行明显更改,但是我希望无论如何您都需要处理该内容,类似于“红色列表”图书”和“绿色图书清单”)。


这真的很重要!它定义了是否应该考虑一组可选属性而不是类属性。
chromanoid

1

将JavaBean(例如您的Book)视为数据库中的记录。可选列是null没有价值的,但完全合法。因此,您的第二个选择是:

class Book {
    private String name;
    private String color; // null when not applicable
}

是最合理的。1个

但是,请谨慎使用Optional类。例如,它不是Serializable,通常是JavaBean的特征。这是Stephen Colebourne的一些提示:

  1. 不要声明任何类型的实例变量Optional
  2. 使用null一类的私人范围内表示可选数据。
  3. 使用Optional为访问可选字段干将。
  4. 不要Optional在setter或构造函数中使用。
  5. 使用Optional作为返回类型为有一个可选的结果的任何其他业务逻辑方法。

因此,类,你应该使用null,以表示该字段不存在,但是当color Book(作为返回类型)应该与包裹Optional

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

这提供了清晰的设计和更易读的代码。


1如果您打算BookWithColor封装一整本“类”的书,这些书具有比其他书更专业的功能那么使用继承是有意义的。


1
谁对数据库说了什么?如果所有OO模式都必须适合关系数据库范式,那么我们将不得不更改许多OO模式。
亚历山大

我认为添加“以防万一”的未使用属性是最不明确的选择。就像其他人所说的那样,这取决于用例,但是最理想的情况是不要随身携带一些外部应用程序需要知道是否实际使用了存在的属性的属性。
Volker Kueffel

@Volker我们同意不同意,因为我可以列举几个Optional为此目的而在将类添加到Java中发挥作用的人。它可能不是面向对象的,但肯定是有效的观点。
4castle

@Alexanderbird我专门处理JavaBean,该JavaBean应该是可序列化的。如果我没有提出JavaBeans,那是我的错误。
4castle

@Alexanderbird我不希望所有模式都可以与关系数据库匹配,但是我确实希望Java Bean能够
4castle

0

您应该使用接口(而不是继承)或某种属性机制(甚至可以像资源描述框架那样工作)。

所以要么

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

要么

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

澄清(编辑):

只需在实体类中添加可选字段

尤其是对于Optional-wrapper(编辑:请参见4castle的答案),我认为这(在原始实体中添加字段)是一种小规模添加新属性的可行方法。这种方法的最大问题是它可能会抑制低耦合。

想象一下,您的书类是在针对您的域模型的专用项目中定义的。现在,您添加了另一个使用域模型执行特殊任务的项目。此任务在book类中需要一个附加属性。您要么获得继承(参见下文),要么必须更改公共域模型以使新任务成为可能。在后一种情况下,您可能最终得到一堆全部依赖于添加到book类的属性的项目,而book类本身以某种方式依赖于这些项目,因为没有这些,您将无法理解book类。其他项目。

为什么在提供其他属性的方式上继承会引起问题?

当我看到示例类“书”时,我想到的是一个域对象,该对象通常最终具有许多可选字段和子类型。试想一下,您想为包含CD的书籍添加属性。现在有四种书籍:书籍,彩色书籍,带CD的书籍,彩色 CD的书籍。您无法用Java中的继承描述这种情况。

使用接口可以避免此问题。您可以通过接口轻松地组成特定书籍类的属性。委托和撰写将使您轻松获得所需的课程。对于继承,您通常会在类中最终得到一些可选属性,因为它们是同级类所需要的。

进一步阅读为什么继承通常是一个有问题的想法:

为什么OOP支持者通常将继承视为一件坏事

JavaWorld:为什么扩展是邪恶的

实现一组接口以组成一组属性的问题

当您使用接口进行扩展时,只要您只有一小部分接口就可以了。特别是当您的对象模型被其他开发人员使用和扩展时,例如在您公司中,接口的数量将会增加。最终,您最终创建了一个新的正式“属性接口”,为您的同事添加了一种方法,该方法已经在您的同事中为客户X使用,用于一个完全不相关的用例-Ugh。

编辑:gnasher729提到了另一个重要方面。您通常希望将可选属性动态添加到现有对象。使用继承或接口,您将不得不使用另一个非可选类来重新创建整个对象。

当您期望对对象模型进行扩展时,可以通过对动态扩展的可能性进行显式建模来更好。我提议类似上面的内容,其中每个“扩展”(在本例中为属性)都有自己的类。该类用作扩展名的名称空间和标识符。相应地设置包命名约定时,这种方式允许无限数量的扩展,而不会污染原始实体类中方法的名称空间。

在游戏开发中,您经常会遇到想要组合多种变化的行为和数据的情况。这就是为什么架构模式实体-组件-系统在游戏开发界非常受欢迎的原因。当您期望对象模型有许多扩展时,这也是您可能想研究的一种有趣的方法。


它并没有解决“如何在这些选项之间做出决定”的问题,这只是另一种选择。为什么这总是比OP提出的所有选项都要好?
亚历山大

您说得对,我将改善答案。
chromanoid

@ 4castle在Java接口中没有继承!实现接口是遵守合同的一种方法,而继承类基本上是在扩展其行为。您可以实现多个接口,而不能在一个类中扩展多个类。在我的示例中,当您使用继承继承原始实体的行为时,将使用接口进行扩展。
chromanoid

说得通。在您的示例中,PropertiesHolder可能是抽象类。但是您建议为getProperty或实施getPropertyValue?数据仍然必须存储在某类实例字段中的某处。还是使用a Collection<Property>来存储属性而不是使用字段?
4castle

1
为了清楚起见,我进行了Book扩展PropertiesHolder。在PropertiesHolder通常应在需要此功能的所有类的成员。该实现将包含a Map<Class<? extends Property<?>>,Property<?>>或类似的内容。这是实现的丑陋部分,但它提供了一个非常好的框架,甚至可以与JAXB和JPA一起使用。
chromanoid

-1

如果Book类是可派生的而没有如下颜色属性

class E_Book extends Book{
   private String type;
}

那么继承是合理的。


我不确定我是否理解您的示例?如果color是私有的,那么任何派生类都不必关心color。只需添加一些完全Book忽略的构造函数,color它将默认为null
4castle
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.