《代码大全2》第6章 可以工作的类

目录

前言

本章主题

6.1 类的基础:抽象数据类型(ADTs)

6.1.1 更多的ADT示例

1. 指导性建议

6.2 良好的类接口

6.2.1 好的抽象

1. 指导性建议

6.2.2 良好的封装

1. 指导性建议

6.3 有关设计和实现的问题

6.3.1 包含(“有一个••••”的关系)

6.3.2 继承( 是一个••••关系)

1.使用继承前需要考虑的事情

2.详细解释如何考虑

3. 多重继承

4. 为什么有这么多关于继承的规则

5.何时可以使用继承,何时又该使用包含

6.3.2 构造函数

6.4 创建类的原因

6.4.1 应该避免的类

6.5 与具体编程语言相关的问题

6.6 超越类:包


《Code_Complete_2》持续更新中......_@来杯咖啡的博客-CSDN博客这本书有意设计成使你既可以从头到尾阅读,也可以按主题阅读。1. 如果你想从头到尾阅读,那么你可以直接从第2章“用隐喻来更充分地理解软件开发”开始钻研。2. 如果你想学习特定的编程技巧,那么你可以从第6章“可以工作的类”开始,然后根据交叉引用的提示去寻找你感兴趣的主题。3. 如果你不确定哪种阅读方式更适合你,那么你可以从第3章3.2节“辦明你所从事的软件的类型”开始。.....................https://blog.csdn.net/qq_43783527/article/details/126275083

前言

        在计算时代的早期,程序员基于语句思考编程问题。到了20世纪七八十年代,程序员开始基干子程序去思考编程。进入21世纪,程序员为基础思考编程问题

        类是由一组数据子程序构成的集合。

本章主题

        本章将就如何创建高质量的类提供一些精辟的建议。

6.1 类的基础:抽象数据类型(ADTs)

        抽象数据类型 (ADT,abstract data type) 是指一些数据以及对这些数据所进行的操作的集合。一个 ADT 可能是一个图形窗体以及所有能影响该窗体的操作;也可以是一个文件以及对这个文件进行的操作;或者是一张保险费率表以及相关操作等。
        要想理解面向对象编程,首先要理解 ADT。不懂 ADT 的程序员开发出来的类只是名义上的“类”而己——实际上这种“类”只不过就是把一些稍有点儿关系的数据和子程序堆在一起。然而在理解 ADT 之后,程序员就能写出在一开始很容易实现、日后也易于修改的类来。

6.1.1 更多的ADT示例

下面再举一些抽象数据类型以及它们可能提供的操作:

1. 指导性建议

        通过研究这些例子,你可以得出一些指导建议,下面就来说明这些指导建议:
       1、把常见的底层数据类型创建为 ADT 并使用这些 ADT,而不再使用底层数据类型。  从前面的例子中可以看到,堆栈、列表、队列以及几乎所有常见的底层数据类型都可以用 ADT 来表示。你可能会问:“这个堆栈、列表或队列又是代表什么呢?” 

  • 如果堆栈代表的是一组员工,就该把它看做是一些员工而不是堆栈
  • 如果列表代表的是一个出场演员名单,就该把它看做是出场演员名单而不是列表
  • 如果队列代表的是电子表格中的一组单元格,就该把它看做是一组单元格而不是一个一般的队列。
  • 也就是说,要尽可能选择最高的抽象层次。

        2、把像文件这样的常用对象当成 ADT。 大部分编程语言中都包含有一些抽象数据类型,你可能对它们己经比较熟悉了,而只是可能并未将其视作 ADT。

  • 你可以把文件读、文件写、文件刷新等操作放到一个ADT中。那么业务方不需要关注文件读、写等操作可能发生什么异常或者其他的事情,业务方只需要调用文件相关的接口即可。
  • 同理,如果项目中大量用到json相关的操作,那么你可以把fastjson或者jackson的相关操作放到ADT中。

        3、简单的事物也可当做 ADT 为了证明抽象数据类型的实用价值,你不一定非要使用庞杂的数据类型。

  • 在前面的一组例子中,有一益只支持两种操作(开启、关闭)的灯。你可能会觉得把简单的“开”、“关”操作放到单独的子程序中有些浪费功夫,不过即使这样简单的操作也可以通过使用 ADT 而获益。把灯和与之相关的操作放到一个 ADT 里,可以提高代码的自我说明能力,让代码更易于修改,还能把改动可能引起的后果封闭在 TurnLighton() 和 Turntightoff(两个子程序内,并减少了需要到处传递的数据的项数。

        4、不要让 ADT 依赖于其存储介质。 假设你有一张保险费率表,它太大了,因此只能保存到磁盘上。你可能想把它称做一个“费率文件〞然后编出类似RateFile. Read() 这样的访问器子程序(access routine)。然而当你把它称做一个“文件”时,已经暴露了过多的数据信息。一旦对程序进行修改,把这张表存到内存中而不是磁盘上,把它当做文件的那些代码将变成不正确,而且产生误导并使人迷惑。因此,请尽量让类和访问器子程序的名字与存储数据的方式无关,并只提及抽象数据类型本身,比如说“保险费率表”。这样一来,前面这个类和访问器子程序的名字就可能是 raterable. Read(),或更简单的 rates. Read()。

6.2 良好的类接口

        创建高质量的类,第一步,可能也是最重要的一步,就是创建一个好的接口。这也包括了创建一个可以通过接口来展现的合理的抽象,并确保细节仍被隐藏在抽象背后

6.2.1 好的抽象

        正如第 5.3 节“形成一致的抽象”中所述,抽象是一种以简化的形式来看待复杂操作的能力类的接口为隐藏在其后的具体实现提供了一种抽象。类的接口应能提供一组明显相关的子程序。

1. 指导性建议

        为了追求设计优秀,这里给出一些创建类的抽象接口的指导建议:
        1、类的接口应该展现一致的抽象层次在考虑类的时候有一种很好的方法,就是把类看做一种用来实现抽象数据类型(ADT,见第 6.1 节)的机制。每一个类应该实现一个 ADT,并且仅实现这个 ADT。如果你发现某个类实现了不止一个ADT,或者你不能确定究竟它实现了何种 ADT,你就应该把这个类重新组织为一个或多个定义更加明确的 ADT。
        在下面这个例子中,类的接口不够协调,因为它的抽象层次不一致:

        这个类展现了两个 ADT: Employee 和 ListContainer。出现这种混合的抽象,通常是源于程序员使用容器类或其他类库来实现内部逻辑,但却没有把“使用类库”这一事实隐藏起来。请自问一下,是否应该把使用容器类这一事实也归入到抽象之中?这通常都是属于应该对程序其余部分隐藏起来的实现细节,就像下面这样: 

        有的程序员可能会认为从 ListContainer 继承更方便,因为它支持多态,可以传递给以 ListContainer 对象为参数的外部查询函数或排序函数来使用。然而这一观点却经不起对“继承”合理性的主要测试:“继承体现了‘是一个⋯(is a)’关系吗?   如果从 ListContainer 中继承,就意味着 Bmoloveecensus “是一个”ListContainer,这显然不对。如果 Employeecensus 对象的抽象是它能够被搜索或排序,这些功能就应该被明确而一致地包含在类的接口之中。
         在修改程序时,混杂的抽象层次会让程序越来越难以理解,整个程序也会逐步堕落直到变得无法维护。 

        2、一定要理解类所实现的抽象是什么 一些类非常相像,你必须非常仔细地理解类的接口应该捕捉的抽象到底是哪一个

  •         我曾经开发过这样一个程序,用户可以用表格的形式来编辑信息。我们想用一个简单的栅格(grid)控件,但它却不能给数据输入单元格换颜色,因此我们决定用一个能提供这一功能的电子表格(spreadsheet)控件。
  •         电子表格控件要比栅格控件复杂得多,它提供了 150 个子程序,而栅格控件只有15个。由于我们的目标是使用一个栅格控件而不是电子表格控件,因此我们让一位程序员写一个包裏类(wrapper class),隐藏起“把电子表格控件用做栅格控件”这一事实。这位程序员强烈抱怨,认为这样做是在毫无必要地增加成本,是官僚作风,然后就走了。几天以后,他带来了写好的包裹类,而这个类竟然忠实地把电子表格控件所拥有的全部150 个子程序都暴露出来了!
  •         这并不是我们想要的。我们要的是一个栅格控件的接口,这个接口封裝了“背后实际是在用一个更为复杂的电子表格控件”的事实。那位程序员应该只暴露那15 个栅格控件的子程序,再加上第 16 个支持设置单元格颜色的子程序。他把全部150 个子程序都暴露出来,也就意味着一旦想要修改底层实现细节,我们就得支持150 个公用子程序。这位程序员没有实现我们所需要的封装,也给他自己带来了大量无谓的工作。
  •         根据具体情况的不同,正确的抽象可能是一个电子表格控件,也可能是一个栅格控件。当你不得不在两个相似的抽象之间做出选择时,请确保你的选择是正确的

        3、提供成对的服务。 大多数操作都有和其相应的、相等的以及相反的操作。

  • 如果有一个操作用来把灯打开,那很可能也需要另一个操作来把灯关闭。
  • 如果有一个操作用来向列表中添加项目,那很可能也需要另一个操作来从列表中删除项目。
  • 如果有一个操作用来激活菜单项,那很可能也需要另一个操作来屏蔽菜单项。
  • 在设计一个类的时候,要检查每一个公用子程序,决定是否需要另一个与其互补的操作。不要盲目地创建相反操作,但你一定要考虑,看看是否需要它

        4、把不相关的信息转移到其他类中 。有时你会发现,某个类中一半子程序使用着该类的一半数据,而另一半子程序则使用另一半数据。这时你其实已经把两个类混在一起使用了,把它们拆开吧!
        5、尽可能让接口可编程,而不是表达语义。 每个接口都由一个可编程(program-matic)的部分和一个语义(semantic)部分组成。可编程的部分由接口中的数据类型和其他属性构成,编译器能强制性地要求它们(在编译时检查错误)。而语义部分则由“本接口将会被怎样使用”的假定组成,而这些是无法通过编译器来强制实施的。语义接口中包含的考虑比如“Routine入 必须在 RoutineB 之前被调用”或“如果 dataMember 未经初始化就传给 RoutineA 的话,将会导致 RoutineA 崩溃〞。语义接口应通过注释说明,但要尽可能让接口不依赖于这些说明。一个接口中任何无法通过编译器强制实施的部分,就是一个可能被误用的部分。要想办法把语义接口的元素转换为编程接口的元素,比如说用 Asserts(断言)或其他的技术。

        6、谨防在修改时破坏接口的抽象。 在对类进行修改和扩展的过程中,你常常会发现额外所需的一些功能。这些功能并不十分适应于原有的类接口,可看上去却也很难用另一种方法来实现。举例来说,你可能会发现 Employee 类演变成了下面这个样子:

        在雇工和检查邮政编码、电话号码或职位的子程序之间并不存在什么逻辑上的关联,那些暴露 SQL 语句查询细节的子程序所处的抽象层次比 Employee 类也要低得多,它们都破坏了 Employee 类的抽象。 

        7、不要添加与接口抽象不一致的公用成员。 每次你向类的接口中添加子程序时,问问“这个子程序与现有接口所提供的抽象一致吗?”如果发现不一致,就要换另一种方法来进行修改,以便能够保持抽象的完整性。
        8、同时考虑抽象性和内聚性。 抽象性和内聚性这两个概念之间的关系非常紧密——一个呈现出很好的抽象的类接口通常也有很高的内聚性。而具有很强内聚性的类往往也会呈现为很好的抽象,尽管这种关系并不如前者那么强。

        我发现,关注类的接口所表现出来的抽象,比关注类的内聚性更有助于深入地理解类的设计。如果你发现某个类的内聚性很弱,也不知道该怎么改,那就换一种方法,问问你自己这个类是否表现为一致的抽象。

6.2.2 良好的封装

设计精良的模块和设计糟糕的模块的唯一最大区别,就是对其他模块隐藏本模块内部数据和其他实现细节的程度。--Joshua Bloch

       封装填补了抽象留下的空白。抽象是说:“可以让你从高层的细节来看待一个对象。”而封装则说:“除此之外,你不能看到对象的任何其他细节层次。” 要么就是封装与抽象两者皆有,要么就是两者皆失。除此之外没有其他可能。
        关于房屋材质的比喻:封装是说,你可以从房屋的外面看,但不能靠得太近去把门的细节都看清楚。可以让你知道哪里有门,让你知道门是开着的还是关着的,但不能让你知道门是木质的、纤维玻璃的、钢质的还是其他什么材质的,当然就更不能让你看到每一根木纤维
        如图 5-8 所示,封裝帮助你管理复杂度的方法是不让你看到那些复杂度:

1. 指导性建议

        1、尽可能地限制类和成员的可访问性让可访问性 (accessibilty)尽可能低是促成封装的原则之一。当你在犹豫某个子程序的可访问性应该设为公用(public)、私用(private)抑或受保护(protected)时,经验之举是应该采用最严格且可行的访问级别(Meyers 1998, Bloch 2001)。

  • 我认为这是一个很好的指导建议,但我认为还有更重要的建议,即考虑“采用哪种方式能最好地保护接口抽象的完整性?”如果暴露一个子程序不会让抽象变得不一致的话,这么做就很可能是可行的。如果你不确定,那么多隐藏通常比少隐藏要好。

        2、不要公开暴露成员数据暴露成员数据会破坏封裝性,从而限制你对这个抽象的控制能力。

        3、避免把私用的实现细节放入类的接口中 做到真正的封装以后,程序员们是根本看不到任何实现细节的。无论是在字面上还是在喻意上,它们都被隐藏了起来。

        4、不要对类的使用者做出任何假设。 类的设计和实现应该符合在类的接口中所隐含的契约。它不应该对接口会被如何使用或不会被如何使用做出任何假设一除非在接口中有过明确说明。像下面这样一段注释就显示出这个类过多地假定了它的使用者:请把x,y和2初始化为1.0,因为如果把它们初始化为0.0 的话,Derivedclass 就会崩溃.
        5、避免使用友元类 (friend class)。有些场合下,比如说 State 模式中,按照正确的方式使用友元类会有助于管理复杂度(Gamma et al. 1995)。但在一般情况下友元类会破坏封装,因为它让你在同一时刻需要考虑更多的代码量,从而增加了复杂度。
        6、不要因为一个子程序里仅使用公用子程序,就把它归入公开接口。 一个子程序仅仅使用公用的子程序这一事实并不是十分重要的考虑要素。相反,应该问的问题是,把这个子程序暴露给外界后,接口所展示的抽象是否还是一致的
        7、让阅读代码比编写代码更方便阅读代码的次数要比编写代码多得多,即使在开发的初期也是如此。因此,为了让编写代码更方便而降低代码的可读性是非常不经济的

  • 尤其是在创建类的接口时,即使某个子程序与接口的抽象不很相配,有时人们也往往把这个子程序加到接口里,从而让正开发的这个类的某处调用代码能更方便地使用它。然而,这段子程序的添加正是代码走下坡路的开始,所以还是不要走出这一步为好。

        8、要格外警惕从语义上破坏封装性

        比较起来,语义上的封裝性和语法上的封装性二者的难度相差无几。从语法的角度说,要想避免窥探另一个类的内部实现细节,只要把它内部的子程序和数据都声明为 private 就可以了,这是相对容易办到的。然而,要想达到语义上的封装性就完全是另一码事儿了。下面是一些类的调用方代码从语义上破坏其封裝性的例子。

  • 不去调用 A 类的 Initializeoperations()子程序,因为你知道A 类的Performgirstoperation()子程序会自动调用它。
  • 不在调用 employee.Retrive (database)之前去调用 database. Connect ()子程序,因为你知道在未建立数据库连接的时候 employee. Retrieve()会去连接数据库的。
  • 不去调用 A 类的 Terminate()子程序,因为你知道A 类的 PerformFinal-Operation()子程序已经调过它了。
  • 即便在 ObjectA 离开作用域之后,你仍去使用由 ObjectA 创建的、指向ObjectB 的指针或引用,因为你知道 objectA 把 ObjectB 放置在静态存储空间中了,因此 ObjectB 肯定还可以用。
  • 使用 ClassB. MAXIMUM_ELEMENTS 而不用 ClassA. MAXIMUM_EL,EMENTS,因为你知道它们两个的值是相等的。

        上面这些例子的问题都在于,它们让调用方代码不是依赖于类的公开接口,而是依赖于类的私用实现每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是在针对接口编程了,而是在透过接口针对内部实现编程。如果你透过接口来编程的话,封装性就被破坏了,而一旦封装性开始遭到破坏,抽象能力也就快遭殃了。

        如果仅仅根据类的接口文档还是无法得知如何使用一个类的话,下确的做法不是拉出这个类的源代码,从中查看其内部实现。这是个好的初衷,但却是个错误的决断。正确的做法应该是去联系类的作者,告诉他“我不知道该怎么用这个类。”而对于类的作者来说,正确的做法不是面对面地告诉你答案,而是从代码库中 check out(签出)类的接口文件,修改类的接口文档,再把文件 check in(签入)回去,然后告诉你“看看现在你知不知道该怎么用它了。”你希望让这一次对话出现在接口代码里,这样就能留下来让以后的程序员也能看到。你不希望让这一次对话只存在于自己的脑海里,这样会给使用该类的调用方代码烙下语义上的徽妙依赖性。你也不想让这一次对话只在个人之问进行,这样只能让你的代码获.益,而对其他人没有好处。

--我认为这一点非常重要,当你去读源代码(java提供的,别人提供的),你最先应该关注的是api继而推测如何使用这个类,而不是先去关注内部实现。当你想要了解其内部实现的时候,再去了解。

6.3 有关设计和实现的问题

        给类定义合理的接口,对于创建高质量程序起到了关键作用。然而,类内部的设计和实现也同样重要。这一节就来论述关于包含、继承、成员函数和数据成员、类之间的耦合性、构造函数、值对象与引用对象等的问题。

6.3.1 包含(“有一个••••”的关系)

        包含是一个非常简单的概念,它表示一个类含有一个基本数据元素或对象与包含相比,关于继承的论述要多得多,这是因为继承需要更多的技巧,而且更容易出错,而不是因为继承要比包含更好。包含才是面向对象编程中的主力技术。

        1、通过包含来实现“有一个/has a”的关系 可以把包含想成是“有一个”关系。比如说,一名雇员“有一个”姓名、“有一个”电话号码、“有一个”税收DD等。通常,你可以让姓名、电话号码和税收 ID 成为 Bmployee 类的数据成员,从而建立这种关系。
        2、在万不得已时通过 private 继承来实现“有一个”的关系 这么做的主要原因是要让外层的包含类能够访问内层被包含类的 protected成员函数与数据成员。然而在实践中,这种做法会在派生类与基类之间形成一种过于紧密的关系,从而破坏了封装性。而且,这种做法也往往会带来一些设计上的错误,而这些错误是可以用“private 继承”之外的其他方法解决的。
        3、警惕有超过约7个数据成员的类 研究表明,人们在做其他事情时能记住的离散项目的个数是 7土2(Miller 1956)。如果一个类包含有超过约7个数据成员,请考虑要不要把它分解为几个更小的类(Riel 1996)。如果数据成员都是整型或字符串这种简单数据类型,你可以按7士2 的上限来考虑;反之,如果数据成员都是复杂对象的话,就应按7士2的下限来考虑了。

6.3.2 继承( 是一个••••关系)

用C++进行面向对象编程时的一个最重要的法则就是:public继承代表的是“是一个”的关系。请把这一法则印在脑中。— Scott Meyers

1.使用继承前需要考虑的事情

当决定使用继承时,你必须要做如下几项决策:

  1. 对于每一个成员函数而言,它应该对派生类可见吗?它应该有默认的实现吗?这一默认的实现能被覆盖 (override)吗?
  2. 对于每一个数据成员而言(包括变量、具名常量、枚举等),它应该对派生类可见吗?

2.详细解释如何考虑

下面就来详细解释如何考虑这些事项。

        用 public 继承来实现“是一个•••••”的关系。 当程序员决定通过继承一个现有类的方式创建一个新类时,他是在表明这个新的类是现有类的一个更为特殊的版本。基类既对派生类将会做什么设定了预期,也对派生类能怎么运作提出了限制(Meyers 1998)
        如果派生类不准备完全遵守由基类定义的同一个接口契约,继承就不是正确的实现技术了。请考虑换用包含的方式,或者对继承体系的上层做修改。
        要么使用继承并进行详细说明,要么就不要用它 。继承给程序增加了复杂度,如果某个类并末设计为可被继承,就应该把它的成员定义成 non-virtual (C++)、 final (Java)或 non-overridable (Microsoft VisualBasic),这样你就无法继承它了。
        遵循 Liskov 替换原则 (Liskov Substitution Principle, LSP)。除非派生类真的“是一个”更特殊的基类,否则不应该从基类继承 。Andy Hunt 和 Dave Thomas 把 LSP总结为:“派生类必须能通过基类的接口而被使用,且使用者无需了解两者之间的差异。”(Hunt and Thomas 2000)。换句话说,对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同

比如:如果你有一个 Account 基类以及 CheckingAccount、 SavingsAccount、AutoLoanaccount 三个派生类,那么程序员应该能调用这三个 Account 派生类中从 Account 继承而来的任何一个子程序,而无须关心到底用的是 Account 的哪一个派生类的对象

  •  如果程序遵循 Liskov 替换原则,继承就能成为降低复杂度的一个强大工具,因为它能让程序员关注于对象的一般特性面不必担心细节。如果程序员必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了
  • 假如说程序员必须要记得:“如果我调用的是 CheckingAccount 或 SavingsAccount 中的InterestRate()方法的话,它返回的是银行应付给消费者的利息;但如果我调用的是 AutoLoanaccount 中的 InterestRate()方法就必须记得变号,因为它返回的是消费者要向银行支付的利息。”根据 LSP,在这个例子中 AutoLoanAccount就不应该从 Account 继承而来,因为它的 InterestRate()方法的语义同基类中InterestRate()方法的语义是不同的

        不要“覆盖”一个不可覆盖的成员函数。 C++和 Java 两种语言都允许程序员“覆盖”那些不可覆盖的成员函数。如果一个成员函数在基类中是私用 (private)的话,其派生类可以创建一个同名的成员函数。对于阅读派生类代码的程序员来说,这个函数是令人困惑的,因为它看上去似乎应该是多态的,但事实上却非如此,只是同名而已。换种方法来说,本指导建议就是“派生类中的成员函数不要与基类中不可覆盖的成员函数的重名。”
        把共用的接口、数据及操作放到继承树中尽可能高的位置。 接口、数据和操作在继承体系中的位置越高,派生类使用它们的时候就越容易。多高就算太高了呢?根据抽象性来决定吧。如果你发现把一个子程序移到更高的层次后会破坏该层对象的抽象性,就该停手了。
        只有一个实例的类是值得怀疑的。 只需要一个实例,这可能表明设计中把对象和类混为一谈了。考虑一下能否只创建一个新的对象而不是一个新的类。派生类中的差异能否用数据而不是新的类来表达呢?单例(Singleton 〉模式则是本条指导方针的一个特例。

        只有一个派生类的基类也值得怀疑。 每当我看到只有一个派生类的基类时,我就怀疑某个程序员又在进行 “提前设计”了——也就是试图去预测未来的需要,而又常常没有真正了解未来到底需要什么。为未来要做的工作着手进行准备的最好方法,并不是去创建几层额外的、“没准以后哪天就能用得上的”基类,而是让眼下的工作成果尽可能地清晰、简单、直截了当。也就是说,不要创建任何并非绝对必要的继承结构。

        避免让继承体系太深。人们己经发现,过深的继承层次会显著导致错误率的增长(Basili, Briand andMelo 1996)。每个曾经调试过复杂继承关系的人都应该知道个中原因。过深的继承层次增加了复杂度,而这恰恰与继承所应解决的问题相反。请牢牢记住首要的技术使命。请确保你在用继承来避免代码重复并使复杂度最小。

        让所有数据都是 private(而非 protected)。 正如 Joshua Bloch 所言,“继承会破坏封装”(Bloch 2001)。当你从一个对象继承时,你就拥有了能够访问该对象中的 protected 子程序和 protected数据的特权。如果派生类真的需要访问基类的属性,就应提供 protected 访问器函数 (accessor function)

3. 多重继承

在C++的多重继承中有一个毋庸置疑的事实就是,它打开了一个潘多拉的盒子,里面是单继承所没有的复杂度。-Scott Meyers

        虽然有些专家建议广泛使用多重继承(Meyer1997),但以我的经验而言,多重继承的用途主要是定义“混合体(mixins)”,也就是一些能给对象增加一组属性的简单类
        程序员在决定使用多重继承之前,应该仔细地考虑其他替代方案,并谨慎地评估它可能对系统的复杂度和可理解性产生的影晌。

4. 为什么有这么多关于继承的规则

        这一节给出了许多规则,它们能帮你远离与继承相关的麻烦。所有这些规则背后的潜台词都是在说,继承往往会让你和程序员的首要技术使命(即管理复杂度)背道而驰。从控制复杂度的角度说,你应该对继承持有非常歧视的态度。

5.何时可以使用继承,何时又该使用包含

下面来总结一下何时可以使用继承,何时又该使用包含:

  • 如果多个类共享数据而非行为,应该创建这些类可以包含的共用对象。
  • 如果多个类共享行为而非数据,应该让它们从共同的基类继承而来,并在基类里定义共用的子程序。
  • 如果多个类既共享数据也共享行为,应该让它们从一个共同的基类继承而来,并在基类里定义共用的数据和子程序。
  • 当你想由基类控制接口时,使用继承;当你想自己控制接口时,使用包含

6.3.2 构造函数

        如果可能,应该在所有的构造函数中初始化所有的数据成员 。 在所有的构造函数中初始化所有的数据成员是一个不难做到的防御式编程实践。
        用私用(private)构造函数来强制实现单件属性(singleton property)。如果你想定义一个类,并需要强制规定它只能有唯一一个对象实例的话,可以把该类所有的构造函数都隐藏起来,然后对外提供一个 static 的 CetInstance() 子程序来访问该类的唯一实例。它的工作方式如下例所示:

         优先采用深层复本(deep copies),除非论证可行,才采用浅层复本 (shallowcopies)。在设计复杂对象的时候,你需要做出一项主要决策,即应为对象实现深拷贝(得到深层复本)还是浅拷贝(得到浅层复本)。对象的深层复本是对象成员数据逐项复制 (member-wise copy)的结果:而其浅层复本则往往只是指向或引用同一个实际对象,当然,“深”和“浅”的具体含义可以有些出入。

6.4 创建类的原因

6.4.1 应该避免的类

        尽管通常情况下类是有用的,但你也可能会遇到一些麻烦。下面就是一些应该避免创建的类:
        1、避免创建万能类(god class)。要避免创建什么都知道、什么都能干的万能类。如果一个类把工夫都花在用 Get()方法和 Set(方法向其他类索要数据(也就是说,深入到其他类的工作中并告诉它们该如何去做)的话,请考虑是否应该把这些功能组织到其他那些类中去,而不要放到万能类里(Riel 1996)。
        2、消除无关紧要的类如果一个类只包含数据但不包含行为的话,应该问问自己,它真的是一个类吗?同时应该考虑把这个类降级,让它的数据成员成为一个或多个其他类的属性。
        3、避免用动词命名的类只有行为而没有数据的类往往不是一个真正的类。请考虑把类似 DatabaseInitialization(数据库初始化)或 Strin串构造器)这样的类变成其他类的一个子程序。

6.5 与具体编程语言相关的问题

        不同编程语言在实现类的方法上有着很有意思的差别。请考虑一下如何在一个派生类中通过覆盖成员函数来实现多态。

  • 在 Java 中,所有的方法默认都是可以覆盖的方法必须被定义成 final 才能阻止派生类对它进行覆盖
  • 在C++中,默认是不可以覆盖方法的,基类中的方法必须被定义成 vinual 才能被覆盖。
  • 而在 VisualBasic 中,基类中的子程序必须被定义为 overidable,而派生类中的子程序也必须要用 overrides 关键字。

6.6 超越类:包

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@来杯咖啡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值