如何构建基本的类层次结构?

时间:2013-04-21 15:30:32

标签: c++ python class inheritance class-hierarchy

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

所以我真的不是在问 ,我在问 为什么。示例代码始终是一种很好的学习方式,所以我很感激它们。此外,强调设计的进步,而不是简单地给出一个句子的时间和原因。

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

如果任何条款似乎混淆了,请随时编辑我的问题。我不是本地人,我不确定所有的话。

5 个答案:

答案 0 :(得分:10)

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


建立基础课程。

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

我们正在处理操作系统控件,所以让我们从一个简单的类开始,它应该是Button。现在,我们按钮上的属性是什么?显然,窗口上需要position。此外,我们不希望每个按钮的大小完全相同,因此size是另一个属性。按钮也“需要”label(按钮上绘制的文字)。这是你对每个类做的,你设计它然后编码它。现在我知道我需要哪些属性,所以让我们构建类。

class Button
{
    private:
        Point m_position;
        Size m_size;
        std::string m_label;
}

请注意我为了更短的代码而省略了所有getter和setter以及其他方法,但是你也必须包含它们。我也期望我们有PointSize类,通常我们必须自己构建它们。


转到下一堂课。

现在我们完成了一个班级(Button),我们可以转到下一个班级。 我们选择Slider,例如,帮助您上下滚动网页。

让我们像按钮一样开始,滑块类需要什么? 它位于窗口上的位置(position)和滑块的size。此外,它具有最小值和最大值(最小值表示滚动条设置在滑块顶部,最大值表示它位于底部)。我们还需要当前值,即卷轴在此刻的位置。这已经足够了,我们可以建立我们的课程:

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的新课程:

class Control
{
    private:
        Point m_position;
        Size m_size;
}

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

现在我们获得了Control类,其中包含每个控件的常用项,我们可以告诉我们ButtonSlider继承它。这将节省我们的时间,计算机的内存和最终的时间。这是我们的新课程:

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,我们可以通过说{{1}来创建文本框控件并且只在每个类上反复写入文本框特定的成员变量,而不是大小,位置和父级。


最后的想法。

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

我自己也不是一名专业的程序员,但我正在学习,而且我教过你一切,因为我的教育工作者已经教给我了。我希望你(或至少某人)会发现这个答案很有用。 即使有人说python和其他鸭子打字语言甚至不需要使用继承,但它们是错误的。 使用继承将为您在大型项目上节省大量时间和金钱,最终您会感谢您创建基类。 您项目的可重用性和管理将变得十亿倍。

答案 1 :(得分:1)

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

class animal:
    #something
class dog(animal):
    #something
class cat(animal):
    #something

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

class parent:
    #something
class child(parent):
    #something

这里,父和子是两个类,其中子项依赖于父项,其中子项具有父项的属性及其自己的唯一属性。所以,这里使用了继承。

答案 2 :(得分:1)

这取决于语言。

在Python中,例如,您通常不需要大量的继承,因为您可以将任何对象传递给任何函数,如果对象实现了正确的方法,一切都会没问题。

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的函数。< / p>

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

当然,继承是在Python中实现和使用的,但在许多情况下,在C ++或Java中你需要它,你可以通过“鸭子打字”来避免它。

但是,如果你想要继承某些方法的实现(在这种情况下是构造函数),那么继承也可以与Python一起使用

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"

继承的黑暗面是你的类会更加耦合,更难以在你现在无法预见的其他环境中重用。

有人说,面向对象编程(实际上是面向类的编程)有时你只需要一个香蕉,而你会得到一个拿着香蕉和整个丛林的大猩猩。

答案 3 :(得分:1)

我从wikipedia开始定义班级:

  

在面向对象的编程中,类是用于的结构   创建自己的实例 - 称为类实例,类   对象,实例对象或简单对象。一个类定义   构成成员,使其实例具有状态和   行为。数据字段成员(成员变量或实例变量)   使类实例能够维持状态。其他类型的会员,   特别是方法,启用类实例的行为。类   定义实例的类型

通常你会看到使用狗,动物,猫等的例子。但是,让我们做一些实际的事情。

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

创建HTTP请求时需要什么?服务器,端口,协议,标题,URI ......您可以将所有内容放入像{'server': 'google.com'}这样的字典中,但是当您使用类时,您只需明确表示您需要这些属性,并且您和#39;将使用它们来执行这项特定任务。

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

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()

它不是很好看且可读吗?


Abstract classes/Interfaces

这一点,快点......假设你想在项目中为这个特殊情况实现dependency injection:你希望你的应用程序独立于数据库引擎。

因此,您建议接口(由抽象类表示),每个数据库连接器应该实现,然后依赖于应用程序中的泛型方法。让我们假设您定义DatabaseConnectorAbstract(您不必在python中实际定义,但是在提出接口时使用C ++ / C#)和方法:

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 exceptions。只需在那里的层次结构上看一下。

ArithmeticError 通用Exception,在某些情况下,它可以像FloatingPointError一样具体。这在处理异常时非常有用。

当对象在添加到表单时必须是Control的实例时,您可以在.NET forms上更好地实现这一点,但实际上可以是任何其他内容。整点是对象是DataGridView,同时仍然是Control(并实现所有方法和属性)。这与抽象类和接口密切相关,许多现实示例中的一个可能是HTML元素:

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

我试图让它尽可能简短,所以请随时发表评论。

答案 4 :(得分:1)

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

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

  2. 构建类的方法是对扩展开放,对修改(O)。确定类的依赖关系中可能发生的更改(您在其接口和实现中使用的类型或常量)。您希望界面完整足以使其扩展,但您希望其实施足够强大,以便不必为此进行更改。

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

  3. 层次结构是通过继承组合构建的。这里的关键原则是你只使用继承来模拟严格的 Liskov-substitutability (L)。这是一种奇特的方式,表示您只使用 is-a 关系的继承。对于其他任何事情(除了一些技术例外以获得一些小的实现优势),您使用组合。这将使您的系统尽可能松散耦合

  4. 在某些时候,由于各种原因,许多不同的客户可能会依赖您的课程。这会增加您的类层次结构,层次结构中较低的某些类可能会变得过大(“fat”)接口。当这种情况发生时(并且在实践中这是一个品味和判断的问题),你将你的通用类接口分割为许多特定于客户端的接口(I)。

  5. 随着层次结构的进一步增长,当您使用顶部的基本类及其下面的子类或组合绘制它时,它可能会形成金字塔。这意味着您的更高级别的应用层将依赖于较低级别的详细信息。您可以避免这种脆弱性(例如,通过较大的编译时间或在次要重构后的非常大的变化级别中表现出来),让更高级别的层和更低级别的层依赖于抽象(即接口,在C ++中,例如可以实现为抽象类模板参数)。这种依赖倒置(D)再次有助于放松应用程序各个部分之间的耦合。

  6. 就是这样:five solid pieces of advice或多或少与语言无关,经得起时间的考验。软件设计很难,这些规则可以让你摆脱最常出现的麻烦,其他一切都是通过练习来实现的。