类的实例与表示实例的类之间的区别是什么?

时间:2018-01-04 09:40:15

标签: class oop

我使用Java作为示例,但这更像是与OOP设计相关的一般问题。

让我们以Java中的IOException为例。为什么有一个班级FileNotFoundException?那不应该是IOException原因FileNotFound的实例吗?我会说FileNotFoundExceptionIOException的一个实例。这到底在哪里? FileNotFoundButOnlyCheckedOnceExceptionFileNotFoundNoMatterHowHardITriedException ..?

我还看到过我在FirstLineReaderLastLineReader等类中工作的项目中的代码。对我来说,这些类实际上代表了实例,但我在很多地方看到了这样的设计。以Spring Framework源代码为例,它带有数百个这样的类,每次看到一个我都看到一个实例而不是蓝图。这些课程不是蓝图吗?

我想问的是,如何在这两个非常简单的选项之间作出决定:

选项1:

enum DogBreed {
    Bulldog, Poodle;
}

class Dog {
    DogBreed dogBreed;
    public Dog(DogBreed dogBreed) {
        this.dogBreed = dogBreed;
    }
}

选项2:

class Dog {}

class Bulldog extends Dog {
}

class Poodle extends Dog {
}

第一个选项为调用者提供了配置正在创建的实例的要求。在第二个选项中,类已经表示了实例本身(正如我所看到的,这可能是完全错误的......)。

如果您同意这些类代表实例而不是蓝图,您是否认为创建表示实例的类是一个好习惯,或者我正在看这个和我的语句&#34的方式完全错误;表示实例" 的类只是加载废话?

6 个答案:

答案 0 :(得分:18)

被修改

首先:我们知道继承定义,我们可以在SO和互联网上找到很多例子。但是,我认为我们应该看起来深入和更多科学

注0
澄清继承实例术语 首先,让我为开发生命周期命名开发范围,当我们对系统进行建模和编程时,以及运行时范围,有时我们的系统正在运行。

我们在开发范围中有类和建模并开发它们。和运行时范围内的对象。 开发范围中没有对象

在面向对象中,实例的定义是:从类创建对象。

另一方面,当我们谈论类和对象时,我们应该澄清关于开发范围运行时范围观点

所以,通过这个介绍,我想澄清继承
继承是类之间的关系,非对象 继承可以存在于开发范围中,而不是存在于运行时范围中。运行时范围中没有继承。

运行我们的项目后,父项和子项之间没有任何关系(如果子类和父类之间只有继承)。所以,问题是:什么是super.invokeMethod1()super.attribute1,它们不是孩子和父母之间的关系。父节点的所有属性和方法都传输给子节点,这只是访问从父节点传输的部分的符号。

此外,开发范围中没有任何对象。因此,开发范围中没有任何实例。它只是 Is-A Has-A 关系。

因此,当我们说:

  

我会说FileNotFoundExceptionIOException实例

我们应该澄清我们的范围(发展和运行时间) 例如,如果FileNotFoundExceptionIOException的实例,那么运行时特定FileNotFoundException异常(对象)与FileNotFoundException之间的关系是什么。它是实例的实例吗?

注1
为什么我们使用继承?继承的目标是扩展父类功能(基于相同的类型)。

  • 可以通过添加新属性或新方法来实现此扩展。
  • 或覆盖现有方法。
  • 此外,通过扩展父类,我们也可以达到可重用性。
  • 我们不能限制父类功能(Liskov Principle)
  • 我们应该能够将孩子替换为系统中的父母(Liskov Principle)
  • 等。

注2
继承层次结构的宽度和深度
继承的宽度和深度可能与许多因素有关:

  • 项目:项目的复杂性(类型复杂性)及其架构和设计。项目规模,班级数量等。
  • 团队:团队在控制项目复杂性方面的专业知识。
  • 等。

但是,我们有一些启发式方法。 (面向对象的设计启发式,Arthur J. Riel)

  

理论上,继承层次结构应该更深层次,越深越好。

     

在实践中,继承层次结构不应该深入   普通人可以记住他或她的短期记忆。很受欢迎   这个深度的值是六。

请注意,它们是启发式的,基于短期记忆编号(7)。也许团队的专业知识会影响这个数字。但在许多层次结构中使用了组织结构图。

注3
当我们使用错误的继承?
基于:

  • 注1:继承的目标(扩展父类功能)
  • 注2:继承的宽度和深度

在这种情况下,我们使用错误的继承:

  1. 我们在继承层次结构中有一些类,没有扩展父类功能。 扩展应该是合理的,应该足以创建一个新类。从Observer的角度来看,这是合理的手段。观察者可以是项目建筑师或设计师(或其他建筑师和设计师)。

  2. 我们在继承层次结构中有很多类。它称为过度专业化。一些原因可能导致这种情况:

    • 也许我们没有考虑注1 (扩展父功能)
    • 也许我们的模块化(打包)不正确。我们将许多系统用例放在一个包中,我们应该进行设计重构。
  3. 他们是其他原因,但这个答案并不完全相关。

  4. 注4
    我们应该做什么?当我们使用错误的继承时?

    解决方案1:我们应该执行设计重构来检查类的值,以便扩展父功能。在这种重构中,可能删除了许多类系统。

    解决方案2:我们应该执行设计重构以进行模块化。在这个重构中,我们的包的一些类可能会传输到其他包。

    解决方案3:使用合成而不是继承 我们可以使用这种技术有很多原因。 动态层次结构是我们更喜欢使用Composition而不是继承的常见原因之一 见Tim Boudreau(太阳报)注意here

      

    对象层次结构不会缩放

    解决方案4:使用子类

    上的实例

    这个问题是关于这种技术的。让我将其命名为实例,而不是子类

    我们何时可以使用

    1. (提示1):当我们没有完全扩展父类功能时,请考虑注1。或者扩展名不够合理。

    2. (提示2:)考虑注2,如果我们有很多子类(半类或相同的类),它们扩展了父类一点,我们可以控制此扩展程序而不继承。请注意,这并不容易。我们应该证明它并没有违反开放式原则等其他面向对象原则。

    3. 我们该怎么做?
      Martin Fowler建议(Book 1第232页和Book 2第251页):

        

      用字段替换子类,将方法更改为超类字段并删除子类。

      我们可以使用其他技术,如enum作为上述问题。

答案 1 :(得分:4)

首先,通过将异常问题与一般系统设计问题一起包括在内,您真正提出了两个不同的问题。

例外只是复杂的。他们的行为是微不足道的:提供信息,提供原因等等。他们自然是等级的。顶部有Throwable,其他例外情况反复专门化。层次结构通过提供自然的过滤机制简化了异常处理:当你说catch (IOException...时,你知道你会发现关于i / o的一切都很糟糕。不能比那更清楚。测试对于大对象层次结构来说可能很难看,对于例外来说没有问题:在值中测试很少或没有。

因此,如果您使用简单行为设计类似的复杂值,则高继承层次结构是一个合理的选择:不同种类的树或图节点构成了一个很好的示例。

你的第二个例子似乎是关于行为更复杂的对象。这有两个方面:

  • 需要测试行为。
  • 随着系统的发展,具有复杂行为的对象通常会改变彼此之间的关系。

这些是经常听到的口头禅"构成而非继承的原因。" 自90年代中期以来,人们已经很好地理解了小作品与大型对象的大型继承层次结构相比,对象通常更容易测试,维护和更改。

话虽如此,您提供的实施选择却忽视了这一点。你需要回答的问题是"我感兴趣的狗的行为是什么?"然后用界面描述这些,并编程到界面。

interface Dog {
  Breed getBreed();
  Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek);
  void emitBarkingSound(double volume);
  Food getFavoriteFood(Instant asOfTime);
}

当您了解行为时,实施决策就会变得更加清晰。

然后,实现的经验法则是将简单的常见行为放在抽象基类中:

abstract class AbstractDog implements Dog {
  private Breed breed;
  Dog(Breed breed) { this.breed = breed; }
  @Override Breed getBreed() { return breed; }
}

您应该能够通过创建最小的具体版本来测试这些基类,这些版本只为未实现的方法抛出UnsupportedOperationException并验证已实现的方法。需要任何更高级的设置是代码气味:你已经在基地放了太多。

这样的实现层次结构可能有助于减少样板,但超过2深是代码气味。如果您发现自己需要3个或更多级别,那么您很可能并且应该从辅助类中的低级类中包装常见行为的块,这些类将更容易测试并可用于整个系统的组合。例如,不是在基类中提供protected void emitSound(Mp3Stream sound);方法供继承者使用,最好是创建一个新的class SoundEmitter {}并在final中添加Dog成员。 {1}}。

然后通过填写剩下的行为来制作具体的类:

class Poodle extends AbstractDog {
  Poodle() { super(Breed.POODLE); }
  Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek) { ... }
  Food getFavoriteFood(Instant asOfTime) { ... }
}
  

观察:需要一种行为 - 狗必须能够归还其品种 - 以及我们决定实施'#34;获得品种&#34;抽象基类中的行为导致存储的枚举值。

我们最终采用了更接近您的选项1的内容,但这不是先验选择。它源于对行为的思考以及实施它们的最简洁方式。

答案 2 :(得分:3)

以下评论的条件是子类实际上并未扩展其超类的功能。

来自Oracle doc:

Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations.

它表示IOException是一个一般异常。如果我们有一个原因枚举:

enum cause{
    FileNotFound, CharacterCoding, ...;
}

如果我们的自定义代码中的原因未包含在枚举中,我们将无法抛出IOException。换句话说,它使IOException更加特定而不是一般

假设我们没有编写库,并且以下类Dog的功能在我们的业务需求中是特定的:

enum DogBreed {
    Bulldog, Poodle;
}

class Dog {
    DogBreed dogBreed;
    public Dog(DogBreed dogBreed) {
       this.dogBreed = dogBreed;
    }
}

我个人认为使用枚举很好,因为它简化了类结构(更少的类)。

答案 3 :(得分:3)

选项1必须在申报时列出所有已知原因。

可以通过创建新类来扩展选项2,而无需触及原始声明。

当框架完成基本/原始声明时,这很重要。如果有100个已知的,固定的,I / O问题的原因,枚举或类似的东西可能有意义,但如果新的通信方式可能也应该是I / O异常,那么类层次结构更有意义。您添加到应用程序的任何类库都可以扩展I / O异常,而不会触及原始声明。

这基本上是SOLID中的O,为扩展打开,关闭以进行修改。

但这也是为什么,例如,DayOfWeek类型的枚举存在于许多框架中。西方世界有一天突然醒来并且决定去14个独特的日子,或者8个或6个,这种情况不太可能。所以为那些人上课可能有点过分。这些东西更固定在石头上(敲木头)。

答案 4 :(得分:3)

您引用的第一个代码涉及异常。

继承非常适合异常类型,因为用于区分try-catch语句中感兴趣的异常的语言提供的构造是通过使用类型系统。这意味着我们可以轻松选择仅处理更具体的类型(FileNotFound)或更一般的类型(IOException)。

测试字段的值,以查看是否处理异常,意味着逐步退出标准语言构造并编写一些样板值保护代码(例如测试值和重新抛出,如果不感兴趣)。< / p>

(此外,异常需要在DLL(编译)边界之间可扩展。当我们使用枚举时,我们可能在扩展设计时遇到问题而不修改引入(以及其他消耗)枚举的源。)

当涉及除例外以外的事情时,今天的智慧鼓励构成而不是继承,因为这往往会导致设计更简单,更易于维护。

您的选项1更像是一个组合示例,而您的选项2显然是一个继承示例。

  

如果您同意这些类代表实例而不是蓝图,您是否认为创建表示实例的类是一个好习惯,或者我正在看这个和我的语句&#34;类表示完全错误实例&#34;只是胡说八道?

我同意你的意见,并不会说这代表了良好的做法。所示的这些类不是特别可定制的,也不代表附加值。

没有提供覆盖,没有新状态,没有新方法的类与其基础没有特别的区别。所以声明这样一个类几乎没有什么好处,除非我们试图对它进行实例测试(就像异常处理语言结构一样)。我们无法从这个例子中得出真正的结论,这个例子是为了提出问题而设计的,这些子类中是否有任何附加价值,但它并没有出现。

很明显,有许多更糟糕的继承示例,例如教师或学生之类的(职业)职业继承自人。这意味着教师不能同时成为学生,除非我们参与增加更多的课程,例如TeacherStudent,也许使用多重继承..

我们可能会把这个类称为爆炸,因为有时我们最终需要一个类矩阵,因为它们之间的关系不合适。 (添加一个新类,您需要一个全新的爆炸类行或列。)

使用遭受类爆炸的设计实际上会为消费这些抽象的客户创造更多的工作,因此这是一个松散的情况。
在这里,问题在于我们对自然语言的信任,因为当我们说某人是学生时,从逻辑的角度来看,这不是同一个永久性的关系(属于一个&#34; /实例) sublassing),而是扮演一个潜在的临时角色,Person:Person可能同时扮演的许多角色之一。在这些情况下,成分明显优于遗传。

然而,在你的场景中,BullDog不太可能是除了BullDog以外的任何东西,所以永久性是 - 子类化保持的关系,并且在增加很少的价值时,至少这个层次结构不会冒险类爆炸

请注意,使用枚举方法的主要缺点是枚举可能无法扩展,具体取决于您使用的语言。如果你需要任意可扩展性(例如,由他人而不改变你的代码),你可以选择使用可扩展但更弱类型的东西,比如字符串(拼写错误没有被捕获,重复没有被捕获等等)。 。),或者你可以使用继承,因为它提供了良好的可扩展性和更强的类型。其他例外需要这种可扩展性而不需要修改和重新编译原件和其他原因,因为它们是跨越DLL边界使用的。

如果你控制enum并且可以根据需要重新编译代码以处理新的狗类型,那么你就不需要这种可扩展性。

答案 5 :(得分:1)

您提出的两个选项实际上并未表达我认为您想要达到的目标。你想要区分的是构图和继承。

作品的作用如下:

class Poodle {
    Legs legs;
    Tail tail;
}

class Bulldog {
    Legs legs;
    Tail tail;
}

两者都有一组共同的特征,我们可以将它们聚合起来“组合”一个类。我们可以根据需要专门设计组件,但可以期望“Legs”大部分都像其他组件一样工作。

Java为IOExceptionFileNotFoundException选择了继承而不是组合。

也就是说,FileNotFoundException是一种(即extendsIOException并且仅允许基于超类的标识进行处理(尽管如果您选择,您可以指定特殊处理到)。

选择组合而不是继承的论据是由其他人精心排练的,可以通过搜索“组合与继承”轻松找到。