9

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

所以我不是真的问如何,我问的是何时以及为什么。示例代码始终是学习的好方法,因此我将不胜感激。另外,强调设计的进展,而不是简单地给出一个何时和为什么的一句话。

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

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

4

5 回答 5

11

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


建立基础班。

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

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

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

请注意,为了缩短代码,我省略了所有 getter 和 setter 以及其他方法,但您也必须包含这些方法。我也希望我们有Point课程Size,通常我们必须自己构建它们。


进入下一堂课。

现在我们已经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,它指向我们控制所在的窗口(或面板等)。

另一件事是,如果我们后来意识到,Slider我们Button还需要TextBox,我们可以创建一个文本框控件class TextBox : public Control { ... },只写文本框特定的成员变量,而不是在每个类上一遍又一遍地写大小、位置和父级.


最后的想法。

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

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

于 2013-04-21T18:17:03.507 回答
1

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

在面向对象的编程中,类是用于创建自身实例的构造——称为类实例、类对象、实例对象或简称为对象。一个类定义了使它的实例具有状态和行为的组成成员。数据字段成员(成员变量或实例变量)使类实例能够保持状态。其他类型的成员,尤其是方法,使类实例的行为成为可能。类定义其实例的类型

您经常会看到使用狗、动物、猫等的示例。但是,让我们来点实际的东西。

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

创建 HTTP 请求时需要什么?服务器,端口,协议,标头,URI ...您可以将所有这些都放入 dict 中,{'server': 'google.com'}但是当您为此使用 class 时,您只需明确说明您需要将这些属性放在一起,并且您将使用它们来做这是一项特殊的任务。

对于方法。您可以再次创建 method 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()

它不是很好读吗?


抽象类/接口

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

因此,您提出了接口(由抽象类表示),每个数据库连接器都应实现该接口,然后依赖应用程序中的通用方法。假设您使用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 异常。看看那里的层次结构。

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

当添加到表单时对象必须是实例时,您可以在.NET 表单上更好地实现这一点Control,但实际上可以是其他任何东西。重点是对象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

我试图让它尽可能简短,所以请随时在评论中提问。

于 2013-04-21T19:06:39.287 回答
1

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

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

  2. 构建类的方式是对扩展开放对修改关闭(O)。确定类的依赖项(您在其接口和实现中使用的类型或常量)中可能发生的变化。您希望接口足够完整,以便可以扩展,但您希望它的实现足够健壮,这样就不必为此进行更改。

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

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

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

  5. 随着您的层次结构进一步增长,当您绘制它时,它可能看起来形成了一个金字塔,上面是基本类,下面是它们的子类或组合。这将意味着您的更高级别的应用程序层将依赖于较低级别的细节。您可以通过让高层和低层都依赖于抽象(即接口,在 C++ 中,例如可以实现为抽象类模板参数)。这种依赖倒置(D)再次有助于放松应用程序各个部分之间的耦合。

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

于 2013-04-21T19:59:21.110 回答
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

相反,在 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"

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

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

于 2013-04-21T15:57:29.777 回答
1

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

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

这里有两个类,狗和猫,它们具有类的属性animal。在这里,继承发挥了它的作用。

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

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

于 2013-04-21T15:42:33.767 回答