《代码大全2》读书笔记 第六章 可以工作的类

6 可以工作的类

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

使用 ADT 的益处

  • 隐藏实现细节。
  • 增加功能和改动不会影响到整个程序。
  • 让接口提供更多的信息。
  • 更容易提高性能。
  • 让程序的正确性显而易见。
  • 程序更具有自我说明性。
  • 无须在程序内到处传递数据。
  • 你可以像在现实世界中那样操作实体,而不用在底层实现上操作它。

指导建议

  • 把常见的底层数据类型创建为 ADT 并使用他们,而不再使用底层数据类型。
  • 把像文件这样的常用对象当成 ADT。
  • 简单的事物也可当作 ADT。
  • 不要让 ADT 依赖于存储介质。

如何在非面向对象环境中使用 ADT 处理多分数据实例

  • 做法一:每次使用 ADT 服务子程序时都传入实例的ID。(需要进行一次查询)
  • 做法二:传入整个实例。(暴露了不需要的数据)
  • 做法三:使用隐含实例,执行前调用SetCurrentInstance(instID);。(不推荐)

6.2 良好的类接口

好的抽象

  • 类的接口应该展现一致的抽象层次。
  • 一定要理解类所实现的抽象是什么。(一些类非常相像。)
  • 提供成对的服务。(有开就有关。)
  • 把不相关的信息转移到其他类中。
  • 尽可能让接口可编程,而不是表达语义。(一个接口中任何无法通过编译器强制实施的语义部分,就是一个可能被误用的部分,需要用注释或者断言指出。)
  • 谨防在修改时破坏接口的抽象。
  • 不要添加与接口抽象不一致的共用成员。
  • 同时考虑抽象性和内聚性。

良好的封装

  • 尽可能地限制类和成员地可访问性。
  • 不要公开暴露成员数据。
  • 避免把私用的实现细节放入类的接口。
    • 如果你甚至不想把private的字段放到头文件中暴露给其他人开,可以使用一个XXXImplement类的指针。
  • 不要对类的使用者作出任何假设。
  • 多数场合下应避免使用友元类。
  • 不要因为一个程序里仅使用公用子程序,就把它归入公开接口。
  • 让阅读代码比编写代码更方便。
  • 要格外警惕从语义上破坏封装性。
  • 留意过于紧密的耦合关系。

6.3 有关设计和实现的问题

  • 包含才是面向对象编程中的主力技术,而不是继承。

包含(Containment):... has a ...

  • 包含用于实现has a关系。
  • 万不得已时才通过private继承来实现has a关系。
  • 警惕有超过约7个数据成员的类。(7±2)

继承(Inheritance):...is a ...

  • 继承用于实现is a关系。基类对派生类将会做什么既设定了预期,也给出了限制。
  • 要么使用继承并进行详细说明,要么就不要用它。
  • 遵循里氏代换原则(LSP,Liskvo Substitution Principle)。“派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。”
  • 确保只继承需要继承的部分。根据是否可覆盖、是否提供默认实现,可以分为:

    • 抽象且可覆盖的子程序。
    • 可覆盖的子程序。
    • 不可覆盖的子程序。
  • 不要覆盖一个不可覆盖的成员函数。

  • 把共用的接口、数据及操作放到继承树中尽可能高的位置。
  • 只有一个实例的类是值得怀疑的。(使用单件模式)
  • 只有一个派生类的基类也是值得怀疑的。(提前设计)
  • 派生后覆盖了某个子程序,但在其中没做任何操作(基类中有操作),这种情况也值得怀疑。(基类的设计问题。)
  • 避免让继承体系过深。
  • 尽量使用多态,避免大量的类型检查。(如果出现较为复杂的switch,就考虑一下多态。)
  • 让所有数据都是private而非protected。“继承会破坏封装”,如果真的需要私有数据,就提供protected访问器函数。
  • 慎用多重继承,设计良好的多重继承是能避免菱形继承的。

使用场景

  • 如果多个类共享数据而非行为,应该共用对象,被这些类包含。
  • 如果多个类共享行为而非数据,应该定义基类,被这些类继承。
  • 如果多个类既共享数据又共享行为,应该定义基类,基类中定义共用数据和子程序,被这些类继承。
  • 当你想由基类控制接口时,使用继承。
  • 当你想自己控制接口时,使用包含。

成员函数和数据成员

  • 让类中子程序的数量尽可能少。
  • 尽量隐式地产生你不需要地成员函数和运算符。
  • 减少类所调用的不同子程序的数量。
  • 对其他类的子程序的间接调用要尽可能少。
  • 一般来说,应尽量减少类和类之间相互合作的范围。包括:

    • 所实例化的对象的种类。
    • 在被实例化对象上直接调用的不同子程序的数量。
    • 调用由其他对象返回的对象的子程序的数量。

构造函数

  • 如果可能,应该在所有的构造函数中初始化所有的数据成员。(防御式编程)
  • 用私有构造函数来强制实现单件属性。
  • 优先采用深层复本(deep copies),除非论证可行,才采用浅层副本(shallow copies)。

    • 深层:开发和维护较为简单,性能也往往不会损失太多。
    • 浅层:更快,但是增加了复杂度,容易出错。

6.4 创建类的原因

创建原因

  • 为现实世界中的对象建模。
  • 为抽象的对象建模。
  • 降低复杂度。
  • 隔离复杂度。
  • 隐藏实现细节。
  • 限制变动的影响范围。
  • 隐藏全局变量。(使用访问器子程序 access routine)
  • 让参数传递更顺畅。
  • 建立中心控制点。
  • 让代码更易于重用。
  • 为程序族做计划。
  • 将相关操作包装到一起。
  • 实现某种特定的重构。

应该避免的类

  • 避免创建万能类。
  • 消除无关紧要的类。
  • 避免用动词命名的类。

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

不同语言之间可能有差异的地方:

  • 在继承层次中被覆盖的构造函数、析构函数的行为。
  • 在异常处理时构造函数、析构函数的行为。
  • 默认构造函数的重要性。
  • 析构函数的调用时机。
  • 覆盖运算符相关。
  • 对象被创建和销毁时,或被声明时,或退出作用域时,处理内存的方式。