《代码大全2》读书笔记 第五章 软件构建中的设计。

5 软件构建中的设计

设计的层次

  1. 软件系统

    • 比起直接从系统层次开始设计类,从子系统或包这些类的更高组织层次来思考往往会更好。
  2. 分解为子系统和包

    • 如数据库、用户界面、业务规则、命令解释器、报表引擎等。
    • 注意不同子系统之间的通讯规则,限制过多通信。
    • 简化子系统之间的交互关系。由简单到复杂依次是:

      • 子系统A调用子系统B的子程序。
      • 子系统A包含子系统B的类。
      • 子系统A继承自子系统B。
    • 子系统应该是无环的。

    • 程序较小时,谨慎跳过这一层的设计。
    • 常见子系统:

      • 业务规则。
      • 用户界面。
      • 数据库访问。
      • 对系统的依赖性。
  3. 分解为包中的类

    • 尤其要确定好接口。
  4. 分解为类中的数据和子程序

    • 细化出类的私有子程序。
  5. 子程序内部的设计

    • 编写伪代码。
    • 选择算法。
    • 组织子程序内部的代码块。
    • 代码编写。

设计构造块:启发式方法(Design Building Blocks: Heuristics)

找到现实中的对象

  • 步骤:

    • 辨识对象及其属性。
    • 定义可对对象执行的操作。
    • 确定每个对象可以对其他对象进行的操作。(包含、继承)
    • 确定对象的哪些部分对其他对象可见。
    • 定义每个对象的接口。(public接口:对其他所有对象,protected接口:对继承对象。)
  • 迭代方向:

    • 高层次的系统组织结构上。
    • 对定义好的类进行细化。

形成一定的抽象

  • 子程序接口的层次上。
  • 类接口的层次上。
  • 包接口的层次上。

封装实现细节

  • 除了从高层的细节来看待一个对象,你不能看到它的其他细节层次。
  • 封装帮你管理复杂度的方法是不让你看到那些复杂度。

当继承能简化设计时就继承

  • 继承的好处在于它能很好地辅佐抽象地概念。
  • 技能能简化编程的工作。
  • 使用不当也会带来很大弊端。

隐藏秘密(信息隐藏)

  • 设计类时,一个关键性的决策就是确定类的哪些特性应该对外可见,哪些应该隐藏。
  • 类的接口应该尽可能少地暴露其内部工作机制。
  • 设计类的接口也是一个迭代的过程。
  • 一个例子:typedef int IdType;
  • 信息隐藏所说的秘密主要分为两大类:隐藏复杂度、隐藏变化源。

信息隐藏的障碍

  • 信息过度分散。如常量未使用宏或常量定义。
  • 循环依赖。如类A和类B调用彼此的子程序。
  • 把类内数据误认为全局数据
  • 性能损耗。编码阶段,不用担心信息隐藏带来的性能损耗。

信息隐藏的价值

  • 方便修改。
  • 启发设计。对比:使用面向对象的思想会定义IdType类,使设计复杂化。
  • 有助于设计类的公开接口。不要为了追求方便暴露类的私有数据,多写代码来保护类的秘密。

找出容易改变的区域

  • 好的程序设计要适应变化,措施:

    • 找出看起来容易变化的项目。
    • 把容易变化的项目分离出来,单独划分成类,或者集合成类。
    • 把看起来容易变化的项目隔离开来。
  • 容易发生变化的区域:

    • 业务规则。
    • 对硬件的依赖性。
    • 输入和输出。
    • 非标准的语言特性。
    • 困难的设计区域和构建区域。
    • 状态变量。

      • 将布尔换作枚举。
      • 使用访问器子程序(access routine)取代对状态变量的直接检查。
    • 数据量的限制。

  • 预料不同程度的变化:

    • 让这些变化的影响或范围与发生该变化的可能性成正比。
    • 优秀的设计者还能预料应对变化所需的成本。
    • 好方法:找出程序中可能对用户有用的最小子集,接下来用微小的步伐扩充这个系统。

保持松散耦合

  • 耦合标准:

    • 规模。指模块之间的连接数,如:参数个数,公有方法个数。
    • 可见性。如:通过参数表传递优于修改全局数据。
    • 灵活性。指模块间的连接是否容易改动,包括新增一个模块使用这个连接。
  • 耦合的种类:

    • 简单的数据参数耦合:通过简单数据类型的参数来传递数据。
    • 简单对象耦合:一个模块实例化一个对象。
    • 对象参数耦合:对象A要求对象B传给它一个对象C。这种耦合关系比较紧密,要求对象B也了解对象C。
    • 语义上的耦合:最难缠。一个模块使用了另一个模块的语法元素,还使用了它内部工作细节的语义知识。如:

      • A向B传递一个控制标志,用它告诉B该做什么。(A需要了解B对该标志的使用)
      • B在A修改了某个全局数据之后使用该全局数据。(B假设A的修改符合B的要求,且A已经在恰当的时间被调用过)
      • A的接口要求A.init()必须先于A.proc()调用,B知道A.proc()无论如何都会调用A.init(),就没去调用A.init()。
      • A向B传C,因为A知道B只需要C的部分信息,就没有完全初始化C。
      • A向B传基类,B知道A实际传的是派生类,就直接当作派生类来用。
  • 松散耦合使得你对一个模块的使用不用同时关注几件事——内部工作细节、全局数据修改、不确定的功能点等。否则就失去了抽象的意义,模块具有的管理复杂度的功能就丧失了。

查阅常用的设计模式

  • 设计模式的益处:

    • 通过提供现成的抽象来减少复杂度。
    • 把常见解决方案的细节予以制度化来减少出错。
    • 通过提供多种设计方案带来启发。
    • 把设计对话提升一个层次来简化交流。
  • 潜在陷阱:

    • 强迫代码适用于某个模式。
    • 为了模式而模式。

其他启发式方法

  • 高内聚性。使类的代码集中在一个中心目标。更容易记住代码功能。
  • 构造分层结构
  • 严格描述类契约。“如果你承诺提供数据x,y,z,且答应让这些数据具有特征a,b,c,我就承诺基于约束8,9,10,来执行操作1,2,3。”。
  • 分配职责
  • 为测试而设计
  • 避免失误
  • 有意识地选择绑定时间。做早绑定的代码通常比较简单,但也缺乏灵活性。
  • 创建中央控制点
  • 考虑使用蛮力突破
  • 画一个图
  • 保持设计的模块化

设计实践