关于C++:如何构造一个基本的类层次结构?

How should a basic class hierarchy be constructed?

我知道如何编码和使用简单的类,我甚至知道继承如何工作和如何使用它。但是,关于如何实际设计类层次结构,甚至如何设计简单类的指南数量非常有限?另外,我应该何时和为什么继承(或使用)一个类?

所以我不是真的在问如何,我是在问什么时候,为什么。示例代码总是一种很好的学习方法,所以我很感激它们。此外,强调设计的进展,而不是简单地给出一句关于时间和原因的句子。

我主要用C++,C语言和Python编程,但是我可能会理解大多数语言中的简单例子。

如果任何一个术语看起来有点混淆,请随意编辑我的问题。我不是土生土长的人,我不太确定所有的话。


我将使用C++作为示例语言,因为它非常依赖继承和类。下面是一个关于如何为简单操作系统(如Windows)构建控件的简单指南。控件包括窗口上的简单对象,如按钮、滑块、文本框等。好的。

建立一个基本类。好的。

本指南的这一部分适用于(几乎)任何班级。记住,精心策划是成功的一半。我们在上什么课?它有哪些属性,需要什么方法?这些是我们需要考虑的主要问题。好的。

我们在这里讨论操作系统控件,所以让我们从一个简单的类开始,它应该是Button。现在,按钮上的属性是什么?很明显,它需要在窗户上放一个position。另外,我们不希望每个按钮的大小完全相同,所以size是另一个属性。按钮还"需要"一个label(按钮上绘制的文本)。这就是您对每个类所做的,您设计它,然后对它进行编码。现在我知道我需要哪些属性,所以让我们构建这个类。好的。

1
2
3
4
5
6
7
class Button
{
    private:
        Point m_position;
        Size m_size;
        std::string m_label;
}

请注意,为了缩短代码,我遗漏了所有getter、setter和其他方法,但您也必须包含这些方法。我也希望我们有Pointsize类,通常我们必须自己构造它们。好的。

继续下一节课。好的。

现在我们已经完成了一个类(Button),我们可以转到下一个类。让我们来看看Slider,这个条可以帮助你上下滚动网页。好的。

让我们像在button上那样开始,我们的slider类需要什么?它在窗口上有位置(position和滑块的size。另外,它还有最小值和最大值(最小值意味着滚动条被设置到滑块的顶部,最大值意味着它在底部)。我们还需要当前值,即滚动条当前所在的位置。这已经足够了,我们可以建立我们的类:好的。

1
2
3
4
5
6
7
8
9
class Slider
{
    private:
        Point m_position;
        Size m_size;
        int m_minValue;
        int m_maxValue;
        int m_currentValue;
}

创建基类。好的。

既然我们有了两个类,我们注意到的第一件事就是我们刚刚在两个类上定义了Point m_position;Size m_size;属性。这意味着我们有两个具有公共元素的类,我们只写了两次相同的代码,如果我们只写一次代码,并告诉我们的两个类使用该代码而不是重写代码,这不是很好吗?好吧,我们可以。好的。

如果我们有两个具有共同属性的类似类,那么创建一个基类是"总是"(也有例外,但初学者不必担心),在这种情况下,ButtonSlider。它们都控制着我们的操作系统,包括sizeposition。由此我们得到了一个新的类,叫做Control:好的。

1
2
3
4
5
6
class Control
{
    private:
        Point m_position;
        Size m_size;
}

从公共基类继承类似类。好的。

现在我们得到了我们的Control类,它包括每个控件的公共项,我们可以告诉我们的ButtonSlider从它继承。这将节省我们的时间、计算机的内存,最终节省时间。这是我们的新课程:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Control
{
    private:
        Point m_position;
        Size m_size;
}

class Button : public Control
{
    private:
        std::string m_label
}

class Slider : public Control
{
    private:
        int m_minValue;
        int m_maxValue;
        int m_currentValue;
}

现在有人可能会说,写两次Point m_position; Size m_size;比写两次: public Control和创建Control类容易得多。在某些情况下可能是这样,但仍然建议不要两次编写相同的代码,尤其是在创建类时。好的。

此外,谁知道我们最终会发现多少共同特征呢?稍后,我们可能会意识到我们需要Control* m_parent成员到Control类,它指向我们控制的窗口(或面板等)。好的。

另一件事是,如果我们后来认识到,在SliderButton之上,我们还需要TextBox,我们可以通过说class TextBox : public Control { ... }来创建一个文本框控件,并且只在每个类中反复编写文本框特定的成员变量,而不是大小、位置和父对象。好的。

最后的想法。好的。

基本上,当您有两个具有公共属性或方法的类时,应该创建一个基类。这是基本的规则,但是你可以使用你自己的大脑,因为可能会有一些例外。好的。

我自己也不是一个专业的编码员,但我正在学习,我已经像我的教育者教给我的那样教你所有的东西。我希望你(或至少有人)会发现这个答案有用。尽管有人说python和其他duck类型语言甚至不需要使用继承,但它们是错误的。使用继承可以节省您在大型项目上的大量时间和金钱,并且最终您会感谢自己创建了基类。您的项目的可重用性和管理将变得容易上亿倍。好的。好啊。


由于您主要对全局感兴趣,而不是类设计的机制,因此您可能希望熟悉面向对象设计的S.O.L.I.D.原则。这不是一个严格的程序,而是一套或多条规则来支持你自己的判断和品味。

  • 本质上,一个类代表一个单一的责任。它只做一件事而且做得很好。它应该表示一个抽象,最好是表示应用程序逻辑的一部分(封装行为和数据以支持该行为)。它也可以是多个相关数据字段的聚合抽象。类是此类封装的单元,负责维护抽象的不变量。

  • 构建类的方法是既开放于扩展,又封闭于修改(O)。识别类依赖项中可能发生的更改(在其接口和实现中使用的类型或常量)。您希望接口足够完整以便可以扩展,但是您希望它的实现足够健壮,这样就不必为此而更改。

    这是关于类作为基本构造块的两个原则。现在开始构建表示类关系的层次结构。

  • 层次结构是通过继承或组合建立的。这里的关键原则是只使用继承来建模严格的liskov可替换性(L)。这是一种花哨的说法,即您只将继承用于IS-A关系。对于其他任何事情(除了一些技术异常以获得一些小的实现优势),您都使用组合。这将使您的系统尽可能松散地耦合。

  • 在某种程度上,许多不同的客户机可能由于不同的原因而依赖于您的类。这将增加您的类层次结构,并且层次结构中较低的一些类可能会得到过大的("fat")接口。当这种情况发生时(实际上,这是一个品味和判断的问题),您将通用类接口划分为许多特定于客户机的接口(i)。

  • 随着层次结构的进一步发展,当您在上面绘制基本类,并在下面绘制它们的子类或组合时,它可能看起来像一个金字塔。这意味着更高级别的应用程序层将依赖于较低级别的详细信息。您可以避免这样的脆性(例如,通过大的编译时间或非常小的重构之后的非常大的级联),通过让高级层和下层层都依赖于抽象(即,C++中的接口,例如可以实现为抽象类或模板参数)。这种依赖性倒置(D)再次帮助放松应用程序各个部分之间的耦合。

  • 就是这样:五条可靠的建议或多或少是语言独立的,经得起时间的考验。软件设计很难,这些规则是为了让你远离最经常发生的类型的麻烦,其他一切都是通过实践来实现的。


    我将从维基百科中的类定义开始:

    In object-oriented programming, a class is a construct that is used to
    create instances of itself – referred to as class instances, class
    objects, instance objects or simply objects. A class defines
    constituent members which enable its instances to have state and
    behavior. Data field members (member variables or instance variables)
    enable a class instance to maintain state. Other kinds of members,
    especially methods, enable the behavior of class instances. Classes
    define the type of their instances

    你经常看到使用狗、动物、猫等的例子。但让我们来做一些实际的事情。

    当您需要一个类时,第一个也是最直接的情况是当您需要(或者更确切地说,您应该)将某些函数和方法封装在一起时,因为它们只是一起有意义。让我们想象一下简单的事情:HTTP请求。

    创建HTTP请求时需要什么?服务器、端口、协议、头、URI…你可以把所有这些放到dict中,比如{'server': 'google.com'},但是当你使用类来完成这个任务时,你只需要明确地说明你需要这些属性在一起,你将使用它们来完成这个特定的任务。

    对于方法。您可以再次创建方法fetch(dict_of_settings),但是整个功能都绑定到HTTP class的属性,没有它们就没有意义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class HTTP:
        def __init__(self):
            self.server = ...
            self.port = ...
            ...

        def fetch(self):
            connect to self.server on port self.port
            ...

    r1 = HTTP(...)
    r2 = HTTP(...)
    r1.port = ...
    data = r1.fetch()

    它是不是好看又可读?


    抽象类/接口

    这一点,很快…假设您希望在项目中针对这个特定的情况实现依赖注入:您希望应用程序独立于数据库引擎。

    因此,您提出了一个接口(由抽象类表示),每个数据库连接器都应该实现这个接口,然后依赖应用程序中的通用方法。假设您定义了EDOCX1 9(),您不必在Python中实际定义,但是在提出接口时,在C++和C中使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class DatabaseConnectorAbstract:
        def connect(): raise NotImplementedError(  )
        def fetch_articles_list(): raise NotImplementedError(  )
        ...

    # And build mysql implementation
    class DatabaseConnectorMysql(DatabaseConnectorAbstract):
       ...

    # And finally use it in your application
    class Application:
        def __init__(self,database_connector):
            if not isinstanceof(database_connector, DatabaseConnectorAbstract):
                raise TypeError()

            # And now you can rely that database_connector either implements all
            # required methods or raises not implemented exception


    类层次结构

    python异常。看看那边的等级制度。

    ArithmeticError是通用的Exception,在某些情况下,它可以像FloatingPointError那样特别。这在处理异常时非常有用。

    当对象必须是Control的实例才能添加到表单时,您可以在.NET表单上更好地认识到这一点,但实际上可以是其他任何对象。重点是,对象是DataGridView,而对象仍然是Control(实现所有方法和属性)。这与抽象类和接口紧密相连,许多现实生活中的例子之一可能是HTML元素:

    1
    2
    3
    4
    5
    class HtmlElement: pass # Provides basic escaping
    class HtmlInput(HtmlElement): pass # Adds handling for values and types
    class HtmlSelect(HtmlInput): pass # Select is input with multiple options
    class HtmlContainer(HtmlElement): pass # div,p... can contain unlimited number of HtmlElements
    class HtmlForm(HtmlContainer): pass # Handles action, method, onsubmit

    我试着尽量简短,所以请随时发表评论。


    这取决于语言。

    例如,在Python中,通常不需要很多继承,因为可以将任何对象传递给任何函数,如果对象实现了正确的方法,那么一切都会很好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Dog:
        def __init__(self, name):
            self.name = name

        def sing(self):
            return self.name +" barks"

    class Cat:
        def __init__(self, name):
            self.name = name

        def sing(self):
            return self.name +" meows"

    在上面的代码中,DogCat是不相关的类,但是您可以将其实例传递给使用name并调用方法sing的函数。

    在C++中,您将被迫添加基类(例如EDCOX1(0)),并将这两个类声明为派生类。

    当然,继承也在Python中实现并很有用,但是在很多情况下,在C++或Java中都是必要的,你可以通过"鸭子打字"来避免它。

    但是,如果您希望例如继承某些方法的实现(在本例中是构造函数),那么继承也可以与python一起使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Animal:
        def __init__(self, name):
            self.name = name

    class Dog(Animal):
        def sing(self):
            return self.name +" barks"

    class Cat(Animal):
        def sing(self):
            return self.name +" meows"

    继承的黑暗面是类将更加耦合,在您现在无法预见的其他上下文中更难重用。

    有人说,有了面向对象编程(实际上是面向类编程),有时候你只需要一根香蕉,结果你得到了一只大猩猩拿着一根香蕉,整个丛林里都是香蕉。


    当有两个类,包含单个类的属性,或者有两个类,其中一个依赖于另一个类时,需要使用继承。EG)

    1
    2
    3
    4
    5
    6
    class animal:
        #something
    class dog(animal):
        #something
    class cat(animal):
        #something

    这里有两个类,dog和cat,它们具有类animal的属性。在这里,继承扮演着它的角色。

    1
    2
    3
    4
    class parent:
        #something
    class child(parent):
        #something

    在这里,父类和子类是两个类,其中子类依赖于父类,其中子类具有父类的属性和它自己独特的属性。所以,这里使用继承。