08有关类设计和实现的问题(类的结构关系)

一. 类内部的设计和实现

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

1. 包含(“有一个...”关系)——“has a”

? 包含是一个非常简单的概念,它表示一个类含有一个基本数据元素或对象。包含是面向对象编程中的主力技术。

1.1 通过包含来实现“有一个 / has a”的关系
1.2 警惕有超过约 7 个数据成员的类
2. 继承(“是一个...”关系)—— “is a”

? 继承的概念是说一个类是另一个类的一种特化。继承的目的在于,通过“定义能为两个或多个派生类提供共有元素的基类”的方式写出更精简的代码。其中的共有元素可以使子程序接口、内部实现、数据成员或数据类型等。继承能把这些共有的元素集中在一个基类中,从而有助于避免在多处出现重复的代码和数据。

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

  • 对于每一个成员函数而言,它应该对派生类可见吗?它应该由默认的实现吗?这一默认的实现能被覆盖吗?
  • 对于每一个数据成员而言(包括变量、具名常量、枚举等),它应该对派生类可见吗?
    2.1 用 public 继承来实现“是一个...”的关系

? 当程序员决定通过继承一个现有类的方式创建一个新类时,他是在表明这个新的类是现有类的一个更为特殊的版本。基类既对派生类将会做什么设定类预期,也对派生类能怎么运作提出了限制。

? 如果派生类不 准备完全遵守由基类定义的同一个接口契约,继承就不是正确的实现技术了。请考虑换用包含的方式,或者对继承体系的上层做修改。

2.2 要么使用继承并进行详细说明,要么就不要用它

? 继承给程序增加了复杂度,因此它是一种危险的技术。“要么使用继承并进行详细说明,要么就不要用它”。

2.3 遵循 Liskov 替换原则

? Barbara Liskov 在一篇面向对象编程的开创性论文中提出,除非派生类真的“是一个”更特殊的基类,否则不应该从基类继承。即“派生类必须能够通过基类的接口而被使用”,且使用者无须了解两个之间的差异。换句话说,对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同的。

? 如果程序遵循 Liskov 替换原则,继承就能成为降低复杂度的一个强大工具,因为它能让程序员关注与对象的一般特性而不必担心细节。若谷程序员必须要不断地思考不同派生类的实现在语义上的差异,那继承就只会增加复杂度了。

2.4 确保只继承需要继承的部分

? 派生类可以继承成员函数的接口和/或实现。

  • 抽象且可覆盖的子程序(如纯虚函数)是指派生类只继承了该子程序的接口,但不继承其实现。
  • 可覆盖的子程序(如非纯虚函数)是指派生类继承了该子程序的接口及默认实现,并且可以覆盖该默认实现。
  • 不可覆盖的子程序(如 override final 标识的虚函数)是指派生类继承了该子程序的接口及其默认实现,但不能覆盖该默认实现。

当你选择通过继承的方式来实现一个新的类时,请针对每一个子程序仔细考虑你所希望的继承方式.仅仅是因为要继承接口所以才继承实现,或仅仅是因为要继承实现所以才继承接口,这两种情况都值得注意.如果你只是想使用一个类的实现而不是接口,那么就应该采用包含方式,而不是继承。

2.5 不要“覆盖”一个不可覆盖的成员函数

? C++ 和 Java 两种语言都允许程序员“覆盖”那些不可覆盖的成员函数。如果一个成员函数在基类中时私有的,其派生类可以创建一个同名的成员函数。对于阅读 派生类代码的程序员来说,这个函数是令人困惑的,因为它看上去似乎应该是多态的,但事实上缺非如此,只是同名而已。

2.6 把共用的接口、数据及操作放到继承树中尽可能高的位置

? 接口、数据和操作在继承体系中的位置越高,派生类使用它们的时候就越容易。多高就算太高了呢?根据抽象性来决定以吧。如果你发现一个子程序移到更高的层次后会破坏该层对象的抽象性,就该停手了。

2.7 只有一个实例的类是值得怀疑的

? 只需要一个实例,这可能表名设计中把对象和类混为一谈了。考虑一下能否只创建一个新的对象而不是一个新的类。派生类中的差异能否用数据而不是新的类来表达呢?单例模式(Singleton)则是本条指导方针的一个特例。

2.8 只有一个派生类的基类也值得怀疑

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

2.9 派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑

? 这通常表明基类的设计中有错误。举例来说,假设你有一个 Cat 类,它有一个 Scratch() 成员函数,可是最终你发现有些猫的爪尖儿没了,不能抓了。你可能想从 Cat 类派生一个叫 ScratchlessCat 的类,然后覆盖 Scratch() 方法让它什么都不做。但这种做法有一下你个问题:

  • 它修改了 Cat 类接口所表达的语义,因此破坏了 Cat 类所代表的抽象(即接口契约)。
  • 当你从它进一步派生出其他派生类时,采用这一做法会迅速失控。如果你又发现有只猫没有尾巴了怎么办?
  • 采用这种做法一段时间后,代码会逐渐变得混乱而难以维护,因为基类的接口和行为几乎无法让人理解其派生类的行为。

修正这一问题的位置不是在派生类,而是在最初的 Cat 类中。应该创建一个 Claw 类并让 Cat 类包含它。问题的根源在于做了所有猫都能抓的假设,因此应该从源头上理解这个问题,而不是到发现问题的地方修补。

2.10 避免让继承体系过深

? 面向对象的编程方法提供了大量可以用来管理复杂度的技术。然而每种强大的工具都有其危险之处,甚至有些面向对象技术还有增加 —— 而不是降低 —— 复杂度的趋势。

? Arthur Riel 建议把继承层次限制在最多 6 层之内。 Arthur 是基于 “神奇数字 7 +- 2” 这一理论得出这一建议的,但我觉得这样过于乐观了。依我的经验,大多数人在脑中同时应付超过 2 到 3 层继承时就有麻烦了。

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

2.11 尽量使用多态,避免大量的类型检查

? 频繁重复出现的 case 语句有时是在暗示,采用继承可能是中更好的设计选择 —— 尽管并不总是如此。下面就是一段迫切需要采用更为面向对象的方法的典型代码示例:

//多半应该用多态替代的 case 语句
switch (shape.type){
    case Shape_Circle:
        shape.DrawCircle();
        break;
    case Shape_Circle:
        shape.DrawSquare();
        break;
    ...
}

? 在这个例子中,对 shape.DrawCircle() 和 shape.DrawSquare() 的调用应该用一个叫 shape.Draw() 的方法来替代。因为无论形状是圆还是放都可以调用这个方法来绘制

? 另外,case 语句有时也用来把种类确实不同的对象或行为分开。下面就是一个在面向对象编程中合理采用 case 语句的例子:

//也许不该用多态来替代的 case 语句
switch (ui.command()){
    case Command_OpenFile:
        OpenFile();
        break;
    case Command_Print:
        Print();
        break;
    case Command_Exit:
        ShutDown();
        break;
    ...
}

? 此时也可以创建一个基类并派生一些派生类,再用多态的 DoCommand() 方法来实现每一种命令(就像 Command 模式的做法一样)。但在项这个例子一样简单的场合中,DoCommand() 意义实在不大,因此采用 case 语句才是更容易理解的方案。

2.12 让所有数据都是 private ( 而非 protected)

? 正如 Joshua Bloch 所言,“继承会破坏封装”。当你从一个对象继承时,你就拥有可能够访问该对象中的 protected 数据的特权。如果派生类真的需要访问基类的属性,就应该提供 protected 访问器函数。

2.13 多重继承

? 继承是一种强大的工具。就像用电锯取代手锯伐木一样,当小心使用时,它非常有用,但在还没能了解应该注意的事项的人手中,他也会变得非常危险。

? 如果把继承比作是电锯,那么多重继承就是 20 世纪 50 年代 的那种既没有防护罩,也不能自动停机的危险电锯。有时这种工具的确有用,但在大多数情况下,你最好还是把它放在仓库里为妙 —— 至少在这儿它不会造成任何破坏。

? 虽然有些专家建议广泛使用多重继承,但以我个人经验而言,多重继承的用途主要是定义“混合体”,也就是一些能给对象增加一组属性的简单类。之所以称其为混合体,是因为他们可以把一些属性“混合”到派生类里面。“混合体”可以是行如 Displayable (可显示), Persistant (持久化),serializable (可序列化) 或 Sortable (可排序)这样的类。它们几乎总是抽象的,也不打算独立于其他对象而被单独实例化。

? 混合体需要使用多重继承,但只要所有的混合体之间保持完全独立,他们也不会导致典型的菱形继承问题。通过把一类属性夹在一起,还能使设计方案更容易理解。程序员会更容易理解一个用了 Displayable 和 Peristent 混合体的对象 —— 因为这样只需要实现两个属性即可 —— 而较难理解一个需要 11 个更具体的子程序的对象。

? Java 和 VB 语言也都认可混合体的价值,因为它们允许多重继承,但只能继承一个类的实现。而 C++ 则同时支持接口和实现的多重继承。程序员在决定使用多重继承之前,应该仔细地考虑其他方案,并谨慎地评估它可能对系统的复杂度和可理解性产生的影响。

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

? 这一节给出了许多规则,它们能帮你远离与继承相关的麻烦。所有这些规则背后的前台词都是在说,继承往往会让拟合程序员的首要技术使命(即管理复杂度)背道而驰。从控制复杂度的角度来说,你应该对继承持有非常歧视的态度。下面来总结一下何时可以使用继承,何时又该使用包含:

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

二. 成员函数和数据成员

###### 1. 让类中子程序的数量尽可能少

? 一份针对 C++ 程序的研究发现,类里面的子程序的数量越多,则出错率也就越高。然而,也发现其他一些竞争因素产生的影响更显著,包括过深的集成体系、在一个类中调用了大量的子程序,以及类之间的强耦合等。请在保持子程序数量最少和其他这些因素之间评估利弊。

2. 禁止隐式地产生你不需要的成员函数和运算符

? 有时你会发现应该禁止某些成员函数 —— 比如说你想禁止赋值,或不想让某个对象被构造。你可能会觉得,既然编译器是自动生成这些运算符的,你也就只能对它们放行。当时在这种情况下,你完全可以通过把构造函数、赋值运算符或其他成员函数或运算符定义为 private,从而禁止调用方代码访问它们(把构造函数定义为 private 也是定义单件类时所有的标准技术)。

3. 减少类所调用的不同子程序的数量

? 一份研究发现,类里面的错误数量与类所调用的子程序的总数是统计相关的。统一研究还发现,类所用到的其他类的数量越高,其出错率也会越高。

4. 对其他类的子程序的间接调用要尽可能少

? 直接的关联已经够危险了。而间接的关联 —— 如 account.ContactPerson().DaytimeContactInfo().PhoneNumber() —— 往往更加危险。研究人员就此总结出了一条 “Demeter 法则”,基本上就是说 A 对象 可以任意调用它自己的所有子程序。如果 A 对象创建了一个 B 对象,它也可以调用 B 对象的任何 (公用)子程序,但是它应该避免再调用由 B 对象所提供的对象中的子程序。在前面 account 这个例子中,就是说 account.ContactPerson() 这一调用是合适的,但 account.ContactPerson().DaytimeContactInfo() 这一调用则不合适。

一般来说,应尽量减小类和类之间相互合作的范围 —— 即尽量让下面这几个数字最小。
  • 所实例化的对象的种类
  • 在被实例化对象上直接调用的不同子程序的数量
  • 调用由其他对象返回的对象的子程序的数量

三. 构造函数

1. 如果可能,应该在所有的构造函数中初始化所有的数据成员

? 在所有的构造函数中初始化所有的数据成员是一个不难做到的防御式编程时实践。

2. 用私有(private)构造函数来强制实现单件模式

? 如果你想定义一个类,并需要强制规定它只能有唯一一个对象实例的话,可以把该类所有的构造函数都隐藏起来,然后对外界提供一个 static 的 GetInstance() 子程序来访问该类的唯一实例。

3. 优先采用深拷贝(deep copues), 除非论证可行,才采用浅拷贝(shallow copies)

? 在设计复杂对象时,你需要做成一项主要决策,即应为对象实现深拷贝(得到深层复本)还是浅拷贝(得到浅层复本)。对象的深层复本是对象成员数据逐项复制的结果;而其浅层复本则往往只是指向或引用同一个实例对象,当然 “深” 和 “浅” 的具体含义可以有些出入。

? 实现浅层复本的动机一般是为了改善性能。尽管把大型的对象复制出多份复本从美学上看十分令人不快,但这样做很少会导致显著的性能损失。某几个对象可能会引起性能问题,但众所周知,程序员们很不擅长推测真正招致问题的代码。

? 为了不确定的性能提高而增加复杂度是不妥的,因此,在面临选择实现深拷贝还是浅拷贝时,一种合理的方式便是优先实现深拷贝 —— 除非能够论证浅拷贝更好。

? 深层复本在开发和维护方面都要比浅层复本简单。实现浅拷贝除了要用到两种方法都需要的代码之外,还要增加很多代码用于引用计数、确保安全的复制对象、安全地比较对象以及安全地删除对象等。而这些代码时很容易出错的,除非你有充分地理由,否则就应该避免他们。

原文地址:https://www.cnblogs.com/rock-cc/p/9894347.html

时间: 2024-10-05 04:56:00

08有关类设计和实现的问题(类的结构关系)的相关文章

《C++沉思录》——类设计核查表、代理类、句柄类

<C++沉思录>集中反映C++的关键思想和编程技术,讲述如何编程,讲述为什么要这么编程,讲述程序设计的原则和方法,讲述如何思考C++编程. 一.类设计核查表 1.你的类需要一个构造函数吗? 2.你的数据成员都是私有的合理吗? 3.你的类需要一个无参的构造函数吗? 是否需要生成类对象的数组! 4.你的每一个构造函数都初始化所有的数据成员了吗? 虽然这种说法未必总是正确,但是要积极思考! 5.你的类需要析构函数吗? 6.你的类需要一个虚析构函数吗? 7.你的类需要一个拷贝构造函数吗? 8.你的类需

MFC的窗口分割的设计与实现以及CSplitterWnd 类分析

1 引言 在Microsoft VC++ 6.0 中,基于MFC 的应用程序一般分为以下几种:多文档界面(MDI). 单文档界面(SDI)以及基于对话框的应用程序.其中单文档又可分为单视图的和多视图的, 一般情况下,单文档仅需要单视图就够了,如Windows 自带的记事本.画图程序等等,但 在一些情况下,单文档需要多视图支持,比如同时观察文档的不同部分,同时从不同的角度 观察同一文档等. 在MFC 的框架下,文档对象(CDocument)有一个保存其所有视图的列表,并提供了 增加视图(AddVi

BaseMongo基类设计

为进一步完善框架应用,本次系列文章主要是介绍如何完善架构功能,以及如何应用架构做一些具体的应用开发.本系列课程可以在github上找到相应资源,具体每篇文章中都会提供链接. 本次介绍的主要是mongo基类的设计,以及应用.相关请查看文章下面链接下载http://5xpan.com/fs/7hueanfgd6h350fe4/(下载链接有收益,请原谅有广告). 如果你嫌弃慢的话,也可以直接去github(https://github.com/tnodejs/BaseMongodb) 主要函数结构 私

Android基类设计方法详解

1 为什么要设计基类 为什么要给程序设计基类呢?主要是出于2个原因,一是方便代码编写,减少重复代码和冗余逻辑,优化代码:二是优化程序架构,降低耦合度,方便拓展.修改. ok,编写代码是程序员的第一步,那么第二步就是要编写高质量的代码,代码能实现功能是一方面,写的优美则是另一方面,这也是我们所有攻城狮们应该追求的境界. 2 设计基类的基本思路 那么,哪些东西我们需要抽象到基类中呢? 2.1 重复的代码:如果一个逻辑是大多数子类都需要使用的 2.2 臭而长的代码:典型的findviewbyid.To

【Android应用开发技术:用户界面】自定义View类设计

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells 设计良好的类总是相似的,它使用一个易用的接口来封装一个特定的功能,它能有效的使用CPU和内存,我们在设计View类时,通常会考虑以下因素: 遵循Android标准规则 提供自定义的风格属性值并能够被Android XML Layout所识别. 发出可访问的事件 能够兼容And

状态机在类设计中的应用

引言 在面向对象程序设计与分析的过程中最终都将把需要求分解为多个类再进行程序编码,因此类的设计是程序设计的基础,对于多数项目而言可以划分MVC的三层模型来进行实现.对应类的描述而言可以分如下三种类型的类设计 1>边界类:负责与用户进行交互,对于MVC中的View部分 2>控制类:负责业务逻辑处理,对于MVC中的C部分 3>实体类:负责对数据的抽象与存储部分,通常对应于数据库的表,对于MVC中的M部分

类设计中几种继承方式

 通过继承能够从已有的类派生出新的类,而派生类继承了基类的特征,包括方法.正如继承一笔财产要比自己白手起家容易一样,通过继承派生出的类通常比设计新类要容易得多.下面是可以通过继承完成的一些工作. ①可以在已有类的基础上添加功能. ②可以给类添加数据. ③可以修改类方法的行为. C++有三种继承方式:公有继承.保护继承和私有继承. 一.公有继承 公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行. ①公有继承不建立

四类设计人员

转载:四类设计人员 个人觉得设计人员可以分为四种类型:模块设计人员.框架设计人员.专业领域设计人员.系统设计人员,这四种类型的设计人员并没有什么绝对的谁强谁弱,只能说各有千秋吧,但一定程度上来讲,四种类型之间还是存在着一些关联,来看看这四类设计人员的专注点和关联吧:1.模块设计人员      模块设计人员更加专注于模块的详细设计方面,这是个细活来着,模块设计人员需要对基于架构的模块实现有充足的考虑,而这就要求模块设计人员在代码的实现上有充足的经验,需要把握在模块代码实现上可能碰到的问题,在设计时

iOS控制器之基类设计

题记 在进入新公司后.经过这一个月的重构项目,终于把项目做到了个人相对满意的程度(还有一种不满意的叫老板的需求,提过多次意见也没用= =!).在这次重构中按照以前的思路设计出了个人觉得比较适用的一个基类.在这里笔者会把此基类基本的设计说明一遍. 基类设计需求 1.在我们搭建框架之初一般会设计一个ViewController基类,并在基类ViewDidLoad中设置一个随机的背景颜色.并通过touch手势来进行界面的跳转,以此来设计最开始的一个界面跳转框架,并通过界面颜色的变幻来验证我们界面跳转是