【问题标题】:Difference between an instance of a class and a class representing an instance already?类的实例和表示实例的类之间的区别?
【发布时间】:2018-06-14 00:24:48
【问题描述】:

我以 Java 为例,但这更像是一个与 OOP 设计相关的一般问题。

让我们以 Java 中的IOExceptions 为例。例如,为什么会有 FileNotFoundException 类?这不应该是IOException 的一个实例,其中原因FileNotFound?我会说FileNotFoundExceptionIOException 的一个实例。这在哪里结束? FileNotFoundButOnlyCheckedOnceException, FileNotFoundNoMatterHowHardITriedException..?

我还在我工作的项目中看到过代码,其中存在 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 {
}

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

如果您同意这些类代表实例而不是蓝图,您认为创建代表实例的类是一种好习惯还是我看待这个和我的陈述的方式完全错误“类代表实例” 只是一堆废话?

【问题讨论】:

  • 您缺少的是,在选项 1 中,如果您希望 Bulldog 的行为与 Poodle 不同,您总是必须编写像 if( dogBreed == Bulldog) {} ... 这样的代码,而在第二个中您可以覆盖方法。但仍然存在选项 1 非常适合的情况。只是不适用于问题中描述的语义。换句话说:选项 1 是“has-a”关系,而选项 2 是“is-a”关系。
  • 顺便说一句:“我会说 FileNotFoundException 是 IOException 的一个实例” 它不是 instance 但它已经“是一个”,因为遗产。因此,如果您catch(IOException ioe),您将捕获 FileNotFoundExceptions。
  • @Fildor 是的,也许我的例子不是一个好例子,但考虑到问题的其余部分,我认为它强化了我想问的问题。
  • 让我们扭转局面:如果我听从您的建议,那么您将彻底消灭继承。任何类都只是一个带有字段的对象......因为最终任何类都只是“对象”类的一个“实例”......
  • @Fildor 是的,也许。我没有权利知道真的。只是想学习。也许答案很简单:classes are sometimes instances really..

标签: class oop


【解决方案1】:

已编辑

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

注意0
关于继承实例术语的说明。
首先让我将 Development Scope 命名为开发生命周期,当我们对系统进行建模和编程时,以及 Runtime Scope 命名为有时我们的系统正在运行。

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

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

另一方面,当我们谈论类和对象时,我们应该澄清我们关于Development ScopeRuntime Scope观点

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

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

此外,开发范围内没有任何对象。因此,开发范围内没有任何 Instances。这只是 Is-AHas-A 的关系。

因此,当我们说:

我会说FileNotFoundExceptionIOException实例

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

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

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

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

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

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

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

在实践中,继承层次不应该比 一个普通人可以保存在他或她的短期记忆中。一个受欢迎的 此深度的值为六。

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

注意 3
我们什么时候使用了错误的继承?
基于:

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

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

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

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

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

注意4
我们应该怎么做?当我们使用了错误的继承时?

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

解决方案2:我们应该对模块化进行设计重构。在这次重构中,也许我们包的某些类会传递给其他包。

解决方案 3: 使用 组合优于继承
我们可以使用这种技术有很多原因。 动态层次结构是我们更喜欢组合而不是继承的普遍原因之一。
参见 Tim Boudreau(来自 Sun)的笔记 here

对象层次结构无法缩放

解决方案 4: 在子类上使用实例

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

什么时候可以使用:

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

  2. (提示 2:) 考虑注释 2,如果我们有很多子类(半类或相同类)稍微扩展了父类,并且我们可以在没有继承的情况下控制这个扩展。请注意,要这样说并不容易。我们应该证明它没有违反其他面向对象的原则,如开闭原则。

我们应该怎么做?
Martin Fowler 推荐(Book 1 第 232 页和Book 2 第 251 页):

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

我们可以使用其他技术,例如enum,就像上面提到的问题一样。

【讨论】:

    【解决方案2】:

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

    来自 Oracle 文档:

    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 更加specific,而不是general

    假设我们不是在编写库,并且下面的 Dog 类的功能是特定于我们的业务需求的:

    enum DogBreed {
        Bulldog, Poodle;
    }
    
    class Dog {
        DogBreed dogBreed;
        public Dog(DogBreed dogBreed) {
           this.dogBreed = dogBreed;
        }
    }
    

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

    【讨论】:

      【解决方案3】:

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

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

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

      这基本上就是SOLID中的O,对扩展开放,对修改关闭。

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

      【讨论】:

        【解决方案4】:

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

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

        测试字段的值,查看是否处理异常,意味着跳出标准语言结构并编写一些样板保护代码(例如,测试值并在不感兴趣时​​重新抛出)。

        (此外,异常需要跨 DLL(编译)边界进行扩展。当我们使用枚举时,在不修改引入(和其他消耗)枚举的源的情况下扩展设计可能会遇到问题。)

        当涉及到异常以外的事情时,今天的智慧鼓励组合而不是继承,因为这往往会导致设计不太复杂且更易于维护。

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

        如果您同意这些类代表实例而不是蓝图,您认为创建代表实例的类是一种好习惯,还是我看待这个的方式和我的陈述“代表实例的类”是完全错误的只是一堆废话?

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

        没有提供覆盖、没有新状态、没有新方法的类与其基类没有特别的区别。所以声明这样一个类没有什么好处,除非我们试图对它进行实例测试(就像异常处理语言结构在幕后所做的那样)。从这个示例中我们无法真正判断出这些子类是否有任何附加值,但它似乎没有。

        不过,需要明确的是,还有很多更糟糕的继承示例,例如教师或学生等(前)职业从 Person 继承。这意味着教师不能同时成为学生,除非我们参与添加更多课程,例如TeacherStudent,也许是使用多重继承..

        我们可以称之为类爆炸,因为有时我们最终需要一个类矩阵,因为不适当的 is-a 关系。 (添加一个新类,你需要一个全新的行或列的分解类。)

        使用遭受类爆炸的设计实际上为使用这些抽象的客户创造了更多的工作,因此这是一种松散的情况。
        这里的问题在于我们对自然语言的信任,因为当我们说某人是学生时,从逻辑的角度来看,这不是相同的永久“是”/实例关系(子类化),而是Person 正在扮演的潜在临时角色:Person 可能同时扮演的许多可能角色之一。在这些情况下,组合显然优于继承。

        然而,在您的场景中,BullDog 不太可能是 BullDog 以外的任何东西,因此永久的 is-a 子类关系保持不变,虽然增加的价值很小,但至少这种层次结构不会冒类爆炸的风险.

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

        如果您控制枚举并且可以根据需要将代码重新编译为一个单元来处理新的狗类型,那么您不需要这种可扩展性。

        【讨论】:

          【解决方案5】:

          首先,通过将异常问题与一般系统设计问题一起包含在内,您实际上是在问两个不同的问题。

          异常只是复杂的。他们的行为是微不足道的:提供信息,提供原因等。而且他们自然是分层的。顶部有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 {} 并在Dog 中添加一个具有此类型的final 成员。

          然后通过填写其余的行为来创建具体的类:

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

          观察:对行为的需求 - 狗必须能够返回其品种 - 我们决定在抽象基类中实现“获取品种”行为导致存储的枚举值。

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

          【讨论】:

          • 苹果和橙子混合有什么问题?它们都是水果?
          • @KorayTugay 很抱歉使用了本地表达式。这意味着它们是不同的。对一个人下结论并不一定能告诉你关于另一个人的任何事情。
          【解决方案6】:

          您提出的两个选项实际上并没有表达我认为您想要表达的意思。您要区分的是组合和继承。

          作曲是这样的:

          class Poodle {
              Legs legs;
              Tail tail;
          }
          
          class Bulldog {
              Legs legs;
              Tail tail;
          }
          

          两者都有一组共同的特征,我们可以将其聚合以“组合”一个类。我们可以在需要的地方对组件进行专门化,但可以期望“腿”主要像其他腿一样工作。

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

          也就是说,FileNotFoundException 是一种(即extendsIOException,并且只允许基于超类的身份进行处理(尽管您可以指定特殊处理,如果您愿意的话)。

          选择组合而不是继承的论点已被其他人充分排练过,并且可以通过搜索“组合与继承”轻松找到。

          【讨论】:

            猜你喜欢
            • 2020-04-21
            • 1970-01-01
            • 2010-11-15
            • 1970-01-01
            • 2011-09-17
            • 1970-01-01
            • 1970-01-01
            • 2023-03-19
            相关资源
            最近更新 更多