是否可以拥有一个不代表实体的父类,并且与其子类没有“Is-A”关系?

我想拥有一个可以容纳许多类数据类型元素的数组,并且每个类都有一个名为 printInfo()的方法。

我可以这样做的方法是让一个名为 SomeParentClass 的父类有一个名为 printInfo()的虚方法,然后我会创建可以的类数据类型存在于数组中的数组继承自此类并覆盖 printInfo()方法,然后我可以创建一个类型 SomeParentClass * 的数组。

这种方法有什么问题吗?

我问,因为我已经读过一个类应该代表一个实体(例如: BankAccountCarHouseStudent 等),但 SomeParentClass 不代表实体。我还读过父类和子类之间的关系是一个“Is-A”关系,但是从 SomeParentClass 继承的类不是 SomeParentClass (与 CarVehicle 的方式相同。

1
为什么你说你的父班没有'是-a'关系?如果您调用父类 Printable ,则可以说 BankAccountPrintable 对象。
额外 作者 Unixmonkey,
额外 作者 Ben Cottrell,

6 答案

我可以这样做的方法是让一个名为 SomeParentClass 的父类具有一个名为 printInfo()的虚方法,然后我将使类数据类型可以存在于数组中的数组继承自此类并覆盖 printInfo()方法,然后我可以创建一个类型 SomeParentClass * 的数组。

     

这种方法有什么问题吗?

根本没有什么问题(尽管我强烈建议避免使用指针和数组,而是选择 std :: dequestd :: unique_ptr 或者 std :: shared_ptr ,这样你就不会编写需要处理内存管理的代码了。

从语言无关的“OO”角度来看,这正是预期将继承的场景。

我问,因为我已经读过一个类应该代表一个实体(例如: BankAccountCarHouse ,<�代码>学生等),但 SomeParentClass 不代表实体。我还读过父类和子类之间的关系是一个“Is-A”关系,但是从 SomeParentClass 继承的类不是 SomeParentClass (就像 CarVehicle 一样。)

我可以想到一些可能是合理建议的场景,尽管我可以想到这个建议有误导性或错误的更多场景。多年来写的书籍,博客和教程太多,试图用这样的术语来解释OO,而且大多数时候它们是由自己对OO的理解被误导的人写的(至少从观点来看)它与Alan Kay对“面向对象编程”一词的原始定义不符,后者与实体无关)。

Firstly, there's absolutely no requirement whatsoever that a class should represent a data entity. Sticking to this kind of mantra as a hard rule can very easily lead towards a very narrow and misguided way of thinking about code structure where any kind of behaviour which may happen to be related to a particular data entity belongs to that entity, resulting in heavily bloated classes containing dozens of methods which have little or no relationship to each other. This mindset frequently results in the "God Object" anti-pattern as described here: https://en.wikipedia.org/wiki/God_object

类设计与实体建模完全不同 - 如果将“实体”与“类”混淆,那么您也有可能失去代码中不同层和模块之间的逻辑分离。

例如,在分层/分层体系结构中,您可能具有不包含任何行为的简单实体结构,并且仅用于支持与持久性存储的CRUD操作。通常,包含核心业务逻辑的类应该不了解持久性。如果您遵循类应该是实体的视图,那么您最终可以使用“业务逻辑”类,这些类还包含许多与业务逻辑无关的方法,例如 ReadSave ,此时代码丢失了任何清晰的结构或分离。

面向对象编程的关键在于围绕逻辑分离和相关行为(即函数/方法)的分组设计类。

也就是说,在许多(但不是全部)情况下,将类的使用限制为仅代表实体并不会为代码提供有用的结构,有时会产生适得其反的效果。

如果一个类包含的方法彼此之间没有任何关系并且提供完全不相关的要求,那么这表明你有一个类正在做太多事情 - 但这正是你可能最终得到的结果。如果您将实体视为类。大多数情况下,获得干净的模块化代码的方法是在不同的类中拆分不相关的方法,这些方法可能不会整齐地映射到问题域中的任何实体。

另一方面,实体建模是关于识别和分组逻辑相关的数据属性以及分组数据之间的逻辑关系。在许多情况下,在决定是否在实体中创建属性分组时可能使用的标准(例如3NF/BCNF)与决定在类中创建行为分组时的标准完全不同;因此,最好避免模糊数据实体概念与类概念之间的界限。

6
额外
@amon好评 - 这不是我最好的措辞。我没有意识到究竟是谁最初开创了代表真实/物理实体的对象的想法,但忽略了OO/not-OO问题,我经常发现试图遵循该指南的代码似乎在实际之前这样做了考虑任何功能要求。我认为它无论如何都不是一个无用的指导方针,并且有些地方绝对有意义,但我发现这种思考“OO”的方式可能非常严格,而且通常对管理复杂性没有帮助
额外 作者 Ben Cottrell,
“面向对象的编程与数据完全无关” - WAT?一个共同的观点是对象=数据+行为。但我明白你的观点是行为更重要。关于凯的“原始定义”,请注意,他后来创造了一词,在开发Simula语言时发现了OOP作为技术。 Nygaard 坚持认为对象应该字面上代表一个物理模型,即代表实体。当然,它可以认为这个定义是错误的,自60年代以来最佳实践已发生变化。
额外 作者 amon,

我建议你创建一个接口类型的数组(c ++中的抽象类)。只需使用您希望它们实现的方法声明一个接口(定义一个契约),而不是让它们从基类继承。通过这种方式,您可以通过电子邮件实现 IPrintInterface 的类来满足该合同。

如果您只希望数组中的项具有 printInfo()方法,那将是我的方法。

继承是类之间最强的关系,你的问题表明数组类不需要这样的“强度”

4
额外
由于C ++没有 interface 关键字,因此值得一提的是,在C#和Java等语言使用接口的情况下,C ++使用抽象基类(和多重继承)。它是一个接口的事实应该来自抽象基类的命名
额外 作者 Dee,
问题上的标签说它与C ++有关。 C ++不像Java和C#这样的现代语言那样区分接口和实现继承。
额外 作者 Jules,

由于C ++没有Object的基类,例如Java和其他语言,我认为你是在正确的轨道上。前提是您为父类提供了一个合适的名称,该名称描述了您的数组的全部内容。

但是我对你需要那种阵列这个事实感到有些困惑。它让我觉得这个架构不对,或者你正在与框架斗争。只是想到你有答案。

1
额外
@Christopher它不一定是错的,但我有点好奇你需要一个阵列中的所有东西,而不是使用其他数据结构,你可以按需访问。但我不知道目前的架构,这就是我好奇的原因......
额外 作者 Gordon Wilson,
“但我对你需要那种数组这个事实感到有些困惑”一个采用多种数据类型的数组出了什么问题(我认为像PHP这样的其他语言默认允许这样做) ?
额外 作者 Kevin McDonough,
哦!我不是在做一个实际的程序,我只是想了解OOP。
额外 作者 Kevin McDonough,

...将从SomeParentClass继承的类不是SomeParentClass(例如,与Car是车辆的方式相同)。

是。但是拥有这种继承关系并没有错。您可以在许多库和框架中看到这一点。例如,在Java中,两个流行的列表实现 ArrayListLinkedList 大致来自 AbstractList

唯一要记住的是我们不应该将具体的列表实例( ArrayListLinkedList )引用为 AbstractList 。我们将它们称为 List 以获取多态关注,或者 ArrayListLinkedList 以用于数据结构关注。当我们实现两个或多个类似的类时,这些超类提供了便利,并且在引用这些类时,这样的超类几乎是无用的。

另一个极端的例子是空的 EventListener 。事件侦听器接口可能有两个或三个抽象方法,如 onClick()onLongClick()onDoubleClick()。框架还可以为它们提供默认的空实现。当我们只对一两个事件感兴趣时,此实现将非常有用。

0
额外

这种方法没有错! 那么,您应该询问大多数子类中的打印是否相似,并在基类中进行。

class Print {                            //renamed for clarity.
    char const *    m_sInfo;
public:      Print(CS s) : m_sInfo(s)  {}
    virtual ~Print() {}
    virtual void print()     const     { cout << "Info: " << m_sInfo; }//universal
    virtual void printData() const = 0;  //abstract method
    virtual void printMisinformation() {}//called less often, thus using longer name
};
class Car : public Print {
    Engine       m_Engine;
public:         Car() : Print("Car"), m_Engine("V8") { }
    virtual    ~Car() {}
    virtual void printData() const     { m_Engine.print(); }//non-universal print
};
0
额外

你没有(必然)拥有is-a关系,你的数组中对象的类型可能是多种多样的。这是使用接口而不是继承的标志,如Badulake建议的那样。

对于符合is-a关系的类型,您仍然可以使用虚方法实现此类,但遍历数组项的代码会将它们视为IPrintable对象(IPrintable将是您的接口的合适名称)。

但是你的类框架可能已经内置了一个虚拟方法ToString(),每个对象都支持它。如果是这样,这也可能非常适合此目的,因为无论数组中的对象多么多样,总会有一个ToString()可以覆盖。在这种情况下,满足is-a要求:所有对象都是“stringifiables”。

0
额外
问题是标记为C ++。该语言不支持接口,默认情况下,也没有带有全局超类的类框架。
额外 作者 Jules,
@Jules他可能正在使用C ++ .NET。我不知道他正在使用什么图书馆,他所有的对象可能都有我所知道的相同基础。此外,他可以使用具有多重继承的抽象基类来实现相同的效果,这就是C ++上下文中接口的含义。接口不一定是指C ++中不存在的关键字。
额外 作者 Martin Maat,