Published on

代码大全读书笔记2

Authors
  • avatar
    Name
    hpoenixf
    Twitter

软件构件中的设计

设计中的挑战

软件设计的定义

软件设计是将需求分析和编码调试连接在一起的活动。

设计是一个险恶的问题

  • 险恶的问题是指只有通过解决或部分解决才能明确的问题。
  • 例子:设计大桥时考虑了结实度,却忽视了风的影响,最终导致坍塌。

设计是一个无章法的过程

  • 设计过程中会经历错误和误入歧途。
  • 犯错并改正比在编码后发现问题代价小得多。
  • 很难判断设计何时算足够好,因为设计过程永无止尽。

设计是确定取舍和调整顺序的过程

  • 设计的关键是衡量冲突的设计特性并寻找平衡。

设计受到诸多限制

  • 一部分创造可能性,另一部分限制可能性。

设计是不确定的

  • 对同一套程序可能有多种可行的设计方案。

设计是启发式的过程

  • 设计具有探索性,不能保证可重复性结果,总有试验和犯错误。

设计是自然形成的

  • 设计不是突然出现,而是在评估、讨论、试验和修改中演化而成的。

关键的设计概念

软件的首要技术使命:管理复杂度

  • 本质问题:事物的核心属性,例如汽车的轮子和引擎。
  • 偶然问题:事物的非必要属性,例如汽车的涡轮增压发动机。
  • 复杂性的根源:项目因技术复杂度失控而失败。

管理复杂度的方法:

  1. 将本质复杂度减到最少。
  2. 避免偶然复杂度无谓增长。

高代价低效率的设计的三种根源:

  1. 用复杂方法解决简单问题。
  2. 用简单但错误的方法解决复杂问题。
  3. 用不恰当的复杂方法解决复杂问题。

理想的设计特征

  • 最小复杂度:设计简单易于理解。
  • 易于维护:方便维护人员工作。
  • 松散耦合:减少模块之间的关联。
  • 可扩展性:局部改动不影响整体。
  • 可重用性:模块和类可复用。
  • 高扇入:多个类使用同一个工具类。
  • 低扇出:每个类仅适量调用其他类。
  • 可移植性:适应不同环境。
  • 精简性:避免冗余。
  • 层次性:清晰的系统层级结构。
  • 标准技术:遵循通用标准。

设计的层次

  1. 软件系统:整体架构。
  2. 子系统或包:划分主要子系统,定义子系统之间的通信规则。
  3. :识别类及其接口。
  4. 子程序:细分类为具体子程序。
  5. 子程序内部设计:选择算法、组织代码。

设计构造块:启发式方法

使用对象设计

  1. 辨识对象及其属性。
  2. 确定对象间的操作。
  3. 定义对象的公开接口。

形成一致的抽象

  • 抽象是忽略细节专注核心概念的能力。
  • 设计好的接口能隐藏内部细节。

封装实现细节

  • 不让外部访问对象的内部细节。

使用继承简化设计

  • 继承能简化代码并支持抽象概念。

隐藏秘密

  • 通过信息隐藏减少外部对类内部的依赖。
  • 两种秘密:隐藏复杂性和变化源。

信息隐藏的障碍:

  1. 信息过度分散。
  2. 循环依赖。
  3. 全局数据的误用。
  4. 性能损耗。

保持松散耦合

耦合标准

  • 规模:尽量少参数和公用方法。
  • 可见性:使用参数传递而非全局数据。
  • 灵活性:设计模块易于被调用。

耦合的种类

  1. 简单数据参数耦合。
  2. 简单对象耦合。
  3. 对象参数耦合。
  4. 语义耦合(需避免)。

查阅常用的设计模式的好处

  • 提供现成的抽象。
  • 减少错误。
  • 带来启发性的价值。
  • 提升交流效率。

其他启发性方法

  • 高内聚性
  • 分层结构
  • 严格描述类契约
  • 分配职责
  • 为测试而设计
  • 避免失误
  • 中央控制点
  • 模块化设计

设计实践

迭代设计

  • 从高层和底层同时思考,持续改进。

分而治之

  • 将问题分解为不同关注领域。

自上而下与自下而上

  • 自上而下:逐步分解问题。
  • 自下而上:从小功能入手,逐步构建整体。

建立实验性原型

  • 用少量代码验证设计方案。

合作设计

  • 团队合作迭代设计。

记录设计成果

  • 在代码中插入设计文档。
  • 使用 Wiki 或 UML 图。
  • 用相机保留设计挂图。
  • 使用 CRC 卡片。

可以工作的类

类的定义

类是数据和子程序的集合,它们共同承担一组内聚的、明确定义的职责。


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

什么是抽象数据类型

  • 抽象数据类型(ADT)是数据和对这些数据操作的集合。
  • 它既描述了数据是如何工作的,也允许程序修改这些数据。

使用 ADT 的例子

  • 控制字体、字号、文本属性的程序:
    • 不使用 ADT: currentFont.size = 16
    • 使用 ADT: currentFont.size = PointsToPixels(12)

使用 ADT 的益处

  1. 隐藏实现细节:程序的改动不会影响整体。
  2. 清晰的接口:语句更明确。
  3. 性能优化更容易:只需修改子程序。
  4. 提升正确性:更具自我说明性。
  5. 减少数据传递:无需到处传递数据。
  6. 更贴近现实世界:操作实体而非底层实现。

ADT 和类

  • 类可以被看作是 ADT 加上继承和多态。

良好的类接口

好的抽象

  1. 类的接口应该展现一致的抽象层次。
  2. 每个类应实现一个 ADT,并仅实现一个 ADT。
  3. 提供成对的服务(如添加与删除、开灯与关灯)。
  4. 避免包含不相关的信息。
  5. 尽量让接口可编程,而非仅表达语义。

注意事项

  • 修改时不要破坏接口的抽象。
  • 不添加与接口抽象不一致的公共成员。
  • 同时关注抽象性和内聚性。

良好的封装

  1. 抽象让你忽略细节,封装阻止你看到细节。
  2. 尽可能限制类和成员的访问权限:
    • 不公开暴露成员数据。
    • 避免将私有实现放入类的接口。
    • 不对类的使用者做出任何假设。
  3. 避免紧耦合,保护类的封装性。

设计和实现的问题

包含(“有一个”关系)

  • 用包含实现“有一个”关系(如员工有一个名字、一个电话号码)。
  • 在必要时使用 private 继承实现“有一个”关系。

继承(“是一个”关系)

  • 决策继承时需考虑:
    • 成员函数是否对派生类可见?
    • 成员函数是否可以被覆盖?
    • 数据成员是否对派生类可见?
  • 遵循 Liskov 替换原则
    • 派生类必须通过基类接口使用,使用者无需了解差异。
  • 避免以下情况:
    • 继承体系过深。
    • 只有一个实例的类。
    • 只有一个派生类的基类。
    • 派生后覆盖了某个子程序但未执行任何操作。
  • 优化继承:
    • 使用 public 继承实现“是一个”关系。
    • 将公共接口、数据和操作放到继承树的高层。
    • 尽量使用多态,减少类型检查。
    • 所有数据成员设为 private

何时使用继承

  • 多个类共享行为而非数据时。
  • 基类控制接口时使用继承;自己控制接口时使用包含。

成员函数和数据成员

注意事项

  1. 尽量减少子程序数量。
  2. 禁止隐式生成不需要的成员函数和运算符。
  3. 降低类调用的不同子程序数量。
  4. 减少对其他类的子程序的间接调用。

创建类的原因

  1. 模拟现实世界的对象。
  2. 建模抽象对象。
  3. 降低复杂度,隔离实现细节。
  4. 限制变动的影响范围。
  5. 隐藏全局数据,简化参数传递。
  6. 建立中心控制点。
  7. 提高代码可复用性。
  8. 规划程序族。
  9. 封装相关操作。
  10. 实现特定重构。

应该避免的类

  1. 万能类:功能过于庞杂。
  2. 无关紧要的类:只有数据没有行为。
  3. 动词命名的类:只有行为没有数据。

高质量的子程序

差劲的子程序特征

  1. 差劲的名字。
  2. 没有文档。
  3. 缺乏代码规范和布局。
  4. 输入变量被改变。
  5. 读写全局变量。
  6. 没有单一的目的。
  7. 未防范错误数据。
  8. 使用魔法数。
  9. 有未使用的参数。
  10. 参数过多(超过 7 个)。
  11. 参数顺序混乱,没有注释。

创建子程序的正当理由

  1. 降低复杂度。
  2. 引入中间、易懂的抽象。
  3. 避免代码重复。
  4. 支持子类化。
  5. 隐藏顺序和指针操作。
  6. 提高可移植性。
  7. 简化复杂的布尔判断。
  8. 方便性能优化。
  9. 确保子程序尽可能小。
  10. 类似于创建类的原因。

子程序设计要点

内聚性

  1. 功能内聚:子程序仅执行一项操作。
  2. 顺序内聚:按顺序执行共享数据的操作。
  3. 通信内聚:使用同一组数据。
  4. 临时内聚:将相关操作分解为独立子程序。

避免以下内聚问题

  1. 过程内聚:子程序为完成某个任务而组合不相关操作。
  2. 逻辑内聚:使用控制标志执行不同操作。

好的程序命名规则

  1. 描述子程序的所有操作。
  2. 避免模糊或无意义的动词。
  3. 根据需要调整名称长度。
  4. 名称中包含返回值描述。
  5. 使用动词加宾语的形式(如 printDocument)。
  6. 对仗命名示例:
    • add/remove
    • open/close
    • create/destroy
    • start/stop
    • show/hide

子程序长度

  • 子程序的长度建议在 100-200 行,以减少错误率。

子程序参数使用规则

  1. 输入-修改-输出 的顺序排列参数。
  2. 保持多个子程序参数排序一致。
  3. 使用所有参数。
  4. 将状态和错误变量放在最后。
  5. 不将参数作为工作变量使用。
  6. 在接口中说明参数假定:
    • 参数的输入、修改或输出。
    • 参数单位。
    • 状态代码和错误值的含义。
    • 接受值范围。
    • 数量限制(建议不超过 7 个)。

防御式编程

子程序应对错误输入的保护

  1. 检查外部数据的值。
  2. 检查子程序所有输入参数的值。
  3. 决定如何处理错误数据:
    • 返回默认值。
    • 使用合法的替代值。
    • 记录警告信息。

断言的使用

  • 检查前提条件和后置条件,例如:
    • 输入或输出参数在预期范围内。
    • 只读变量未被修改。
    • 容器状态和指针非空。
  • 断言指导原则
    1. 用错误处理代码处理预期问题,用断言处理不应发生的状况。
    2. 避免在断言中放入实际执行代码。
    3. 先断言,再处理错误。

错误处理技术

  1. 返回中立值(如数值计算返回 0)。
  2. 换用下一个正确数据。
  3. 返回与前次相同的数据。
  4. 使用最接近的合法值。
  5. 记录警告日志。
  6. 返回错误码或调用错误处理对象。
  7. 显示错误信息或关闭程序。

异常处理的建议

  1. 仅在真正例外的情况下抛出异常。
  2. 在适当的抽象层次抛出异常。
  3. 异常消息中包含完整信息。
  4. 避免空的 catch 语句。
  5. 集中管理异常报告。
  6. 标准化异常的使用。
  7. 隔离程序,防止错误扩散。

辅助调试的代码

  1. 在开发阶段引入调试代码。
  2. 计划移除开发版限制。
  3. 保留重要错误检查代码。
  4. 让技术支持人员记录错误信息。

伪代码编程过程

伪代码指导原则

  1. 用接近自然语言的语句描述操作。
  2. 避免目标编程语言的语法。
  3. 在意图层面编写伪代码。
  4. 细化到足够低的层次。

好处

  1. 方便评审和迭代优化。
  2. 减少注释工作量。
  3. 比设计文档更易维护。

创建子程序的步骤

  1. 设计子程序
    • 定义问题。
    • 确定子程序的输入、输出和隐藏信息。
    • 考虑错误处理与效率问题。
  2. 编写伪代码
    • 检查伪代码是否可行。
    • 在伪代码中试验想法。
  3. 实现代码
    • 转换伪代码为注释,再填充代码。
    • 检查代码是否需要进一步分解。
  4. 检查代码
    • 测试并优化子程序。

替代方案

  1. 测试驱动开发。
  2. 重构。
  3. 契约式开发。
  4. 灵活的模块化设计。