How should a basic class hierarchy be constructed?
我知道如何编码和使用简单的类,我甚至知道继承如何工作和如何使用它。但是,关于如何实际设计类层次结构,甚至如何设计简单类的指南数量非常有限?另外,我应该何时和为什么继承(或使用)一个类?
所以我不是真的在问如何,我是在问什么时候,为什么。示例代码总是一种很好的学习方法,所以我很感激它们。此外,强调设计的进展,而不是简单地给出一句关于时间和原因的句子。
我主要用C++,C语言和Python编程,但是我可能会理解大多数语言中的简单例子。
如果任何一个术语看起来有点混淆,请随意编辑我的问题。我不是土生土长的人,我不太确定所有的话。
我将使用C++作为示例语言,因为它非常依赖继承和类。下面是一个关于如何为简单操作系统(如Windows)构建控件的简单指南。控件包括窗口上的简单对象,如按钮、滑块、文本框等。好的。
建立一个基本类。好的。
本指南的这一部分适用于(几乎)任何班级。记住,精心策划是成功的一半。我们在上什么课?它有哪些属性,需要什么方法?这些是我们需要考虑的主要问题。好的。
我们在这里讨论操作系统控件,所以让我们从一个简单的类开始,它应该是
1 2 3 4 5 6 7 | class Button { private: Point m_position; Size m_size; std::string m_label; } |
请注意,为了缩短代码,我遗漏了所有getter、setter和其他方法,但您也必须包含这些方法。我也希望我们有
继续下一节课。好的。
现在我们已经完成了一个类(
让我们像在button上那样开始,我们的slider类需要什么?它在窗口上有位置(
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; } |
创建基类。好的。
既然我们有了两个类,我们注意到的第一件事就是我们刚刚在两个类上定义了
如果我们有两个具有共同属性的类似类,那么创建一个基类是"总是"(也有例外,但初学者不必担心),在这种情况下,
1 2 3 4 5 6 | class Control { private: Point m_position; Size m_size; } |
从公共基类继承类似类。好的。
现在我们得到了我们的
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; } |
现在有人可能会说,写两次
此外,谁知道我们最终会发现多少共同特征呢?稍后,我们可能会意识到我们需要
另一件事是,如果我们后来认识到,在
最后的想法。好的。
基本上,当您有两个具有公共属性或方法的类时,应该创建一个基类。这是基本的规则,但是你可以使用你自己的大脑,因为可能会有一些例外。好的。
我自己也不是一个专业的编码员,但我正在学习,我已经像我的教育者教给我的那样教你所有的东西。我希望你(或至少有人)会发现这个答案有用。尽管有人说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中,比如
对于方法。您可以再次创建方法
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异常。看看那边的等级制度。
当对象必须是
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" |
在上面的代码中,
在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,它们具有类
1 2 3 4 | class parent: #something class child(parent): #something |
在这里,父类和子类是两个类,其中子类依赖于父类,其中子类具有父类的属性和它自己独特的属性。所以,这里使用继承。