- Published on
代码大全读书笔记2
- Authors
- Name
- hpoenixf
软件构件中的设计
设计中的挑战
软件设计的定义
软件设计是将需求分析和编码调试连接在一起的活动。
设计是一个险恶的问题
- 险恶的问题是指只有通过解决或部分解决才能明确的问题。
- 例子:设计大桥时考虑了结实度,却忽视了风的影响,最终导致坍塌。
设计是一个无章法的过程
- 设计过程中会经历错误和误入歧途。
- 犯错并改正比在编码后发现问题代价小得多。
- 很难判断设计何时算足够好,因为设计过程永无止尽。
设计是确定取舍和调整顺序的过程
- 设计的关键是衡量冲突的设计特性并寻找平衡。
设计受到诸多限制
- 一部分创造可能性,另一部分限制可能性。
设计是不确定的
- 对同一套程序可能有多种可行的设计方案。
设计是启发式的过程
- 设计具有探索性,不能保证可重复性结果,总有试验和犯错误。
设计是自然形成的
- 设计不是突然出现,而是在评估、讨论、试验和修改中演化而成的。
关键的设计概念
软件的首要技术使命:管理复杂度
- 本质问题:事物的核心属性,例如汽车的轮子和引擎。
- 偶然问题:事物的非必要属性,例如汽车的涡轮增压发动机。
- 复杂性的根源:项目因技术复杂度失控而失败。
管理复杂度的方法:
- 将本质复杂度减到最少。
- 避免偶然复杂度无谓增长。
高代价低效率的设计的三种根源:
- 用复杂方法解决简单问题。
- 用简单但错误的方法解决复杂问题。
- 用不恰当的复杂方法解决复杂问题。
理想的设计特征
- 最小复杂度:设计简单易于理解。
- 易于维护:方便维护人员工作。
- 松散耦合:减少模块之间的关联。
- 可扩展性:局部改动不影响整体。
- 可重用性:模块和类可复用。
- 高扇入:多个类使用同一个工具类。
- 低扇出:每个类仅适量调用其他类。
- 可移植性:适应不同环境。
- 精简性:避免冗余。
- 层次性:清晰的系统层级结构。
- 标准技术:遵循通用标准。
设计的层次
- 软件系统:整体架构。
- 子系统或包:划分主要子系统,定义子系统之间的通信规则。
- 类:识别类及其接口。
- 子程序:细分类为具体子程序。
- 子程序内部设计:选择算法、组织代码。
设计构造块:启发式方法
使用对象设计
- 辨识对象及其属性。
- 确定对象间的操作。
- 定义对象的公开接口。
形成一致的抽象
- 抽象是忽略细节专注核心概念的能力。
- 设计好的接口能隐藏内部细节。
封装实现细节
- 不让外部访问对象的内部细节。
使用继承简化设计
- 继承能简化代码并支持抽象概念。
隐藏秘密
- 通过信息隐藏减少外部对类内部的依赖。
- 两种秘密:隐藏复杂性和变化源。
信息隐藏的障碍:
- 信息过度分散。
- 循环依赖。
- 全局数据的误用。
- 性能损耗。
保持松散耦合
耦合标准
- 规模:尽量少参数和公用方法。
- 可见性:使用参数传递而非全局数据。
- 灵活性:设计模块易于被调用。
耦合的种类
- 简单数据参数耦合。
- 简单对象耦合。
- 对象参数耦合。
- 语义耦合(需避免)。
查阅常用的设计模式的好处
- 提供现成的抽象。
- 减少错误。
- 带来启发性的价值。
- 提升交流效率。
其他启发性方法
- 高内聚性
- 分层结构
- 严格描述类契约
- 分配职责
- 为测试而设计
- 避免失误
- 中央控制点
- 模块化设计
设计实践
迭代设计
- 从高层和底层同时思考,持续改进。
分而治之
- 将问题分解为不同关注领域。
自上而下与自下而上
- 自上而下:逐步分解问题。
- 自下而上:从小功能入手,逐步构建整体。
建立实验性原型
- 用少量代码验证设计方案。
合作设计
- 团队合作迭代设计。
记录设计成果
- 在代码中插入设计文档。
- 使用 Wiki 或 UML 图。
- 用相机保留设计挂图。
- 使用 CRC 卡片。
可以工作的类
类的定义
类是数据和子程序的集合,它们共同承担一组内聚的、明确定义的职责。
类的基础:抽象数据类型(ADT)
什么是抽象数据类型
- 抽象数据类型(ADT)是数据和对这些数据操作的集合。
- 它既描述了数据是如何工作的,也允许程序修改这些数据。
使用 ADT 的例子
- 控制字体、字号、文本属性的程序:
- 不使用 ADT:
currentFont.size = 16
- 使用 ADT:
currentFont.size = PointsToPixels(12)
- 不使用 ADT:
使用 ADT 的益处
- 隐藏实现细节:程序的改动不会影响整体。
- 清晰的接口:语句更明确。
- 性能优化更容易:只需修改子程序。
- 提升正确性:更具自我说明性。
- 减少数据传递:无需到处传递数据。
- 更贴近现实世界:操作实体而非底层实现。
ADT 和类
- 类可以被看作是 ADT 加上继承和多态。
良好的类接口
好的抽象
- 类的接口应该展现一致的抽象层次。
- 每个类应实现一个 ADT,并仅实现一个 ADT。
- 提供成对的服务(如添加与删除、开灯与关灯)。
- 避免包含不相关的信息。
- 尽量让接口可编程,而非仅表达语义。
注意事项
- 修改时不要破坏接口的抽象。
- 不添加与接口抽象不一致的公共成员。
- 同时关注抽象性和内聚性。
良好的封装
- 抽象让你忽略细节,封装阻止你看到细节。
- 尽可能限制类和成员的访问权限:
- 不公开暴露成员数据。
- 避免将私有实现放入类的接口。
- 不对类的使用者做出任何假设。
- 避免紧耦合,保护类的封装性。
设计和实现的问题
包含(“有一个”关系)
- 用包含实现“有一个”关系(如员工有一个名字、一个电话号码)。
- 在必要时使用
private
继承实现“有一个”关系。
继承(“是一个”关系)
- 决策继承时需考虑:
- 成员函数是否对派生类可见?
- 成员函数是否可以被覆盖?
- 数据成员是否对派生类可见?
- 遵循 Liskov 替换原则:
- 派生类必须通过基类接口使用,使用者无需了解差异。
- 避免以下情况:
- 继承体系过深。
- 只有一个实例的类。
- 只有一个派生类的基类。
- 派生后覆盖了某个子程序但未执行任何操作。
- 优化继承:
- 使用
public
继承实现“是一个”关系。 - 将公共接口、数据和操作放到继承树的高层。
- 尽量使用多态,减少类型检查。
- 所有数据成员设为
private
。
- 使用
何时使用继承
- 多个类共享行为而非数据时。
- 基类控制接口时使用继承;自己控制接口时使用包含。
成员函数和数据成员
注意事项
- 尽量减少子程序数量。
- 禁止隐式生成不需要的成员函数和运算符。
- 降低类调用的不同子程序数量。
- 减少对其他类的子程序的间接调用。
创建类的原因
- 模拟现实世界的对象。
- 建模抽象对象。
- 降低复杂度,隔离实现细节。
- 限制变动的影响范围。
- 隐藏全局数据,简化参数传递。
- 建立中心控制点。
- 提高代码可复用性。
- 规划程序族。
- 封装相关操作。
- 实现特定重构。
应该避免的类
- 万能类:功能过于庞杂。
- 无关紧要的类:只有数据没有行为。
- 动词命名的类:只有行为没有数据。
高质量的子程序
差劲的子程序特征
- 差劲的名字。
- 没有文档。
- 缺乏代码规范和布局。
- 输入变量被改变。
- 读写全局变量。
- 没有单一的目的。
- 未防范错误数据。
- 使用魔法数。
- 有未使用的参数。
- 参数过多(超过 7 个)。
- 参数顺序混乱,没有注释。
创建子程序的正当理由
- 降低复杂度。
- 引入中间、易懂的抽象。
- 避免代码重复。
- 支持子类化。
- 隐藏顺序和指针操作。
- 提高可移植性。
- 简化复杂的布尔判断。
- 方便性能优化。
- 确保子程序尽可能小。
- 类似于创建类的原因。
子程序设计要点
内聚性
- 功能内聚:子程序仅执行一项操作。
- 顺序内聚:按顺序执行共享数据的操作。
- 通信内聚:使用同一组数据。
- 临时内聚:将相关操作分解为独立子程序。
避免以下内聚问题
- 过程内聚:子程序为完成某个任务而组合不相关操作。
- 逻辑内聚:使用控制标志执行不同操作。
好的程序命名规则
- 描述子程序的所有操作。
- 避免模糊或无意义的动词。
- 根据需要调整名称长度。
- 名称中包含返回值描述。
- 使用动词加宾语的形式(如
printDocument
)。 - 对仗命名示例:
- add/remove
- open/close
- create/destroy
- start/stop
- show/hide
子程序长度
- 子程序的长度建议在 100-200 行,以减少错误率。
子程序参数使用规则
- 按 输入-修改-输出 的顺序排列参数。
- 保持多个子程序参数排序一致。
- 使用所有参数。
- 将状态和错误变量放在最后。
- 不将参数作为工作变量使用。
- 在接口中说明参数假定:
- 参数的输入、修改或输出。
- 参数单位。
- 状态代码和错误值的含义。
- 接受值范围。
- 数量限制(建议不超过 7 个)。
防御式编程
子程序应对错误输入的保护
- 检查外部数据的值。
- 检查子程序所有输入参数的值。
- 决定如何处理错误数据:
- 返回默认值。
- 使用合法的替代值。
- 记录警告信息。
断言的使用
- 检查前提条件和后置条件,例如:
- 输入或输出参数在预期范围内。
- 只读变量未被修改。
- 容器状态和指针非空。
- 断言指导原则:
- 用错误处理代码处理预期问题,用断言处理不应发生的状况。
- 避免在断言中放入实际执行代码。
- 先断言,再处理错误。
错误处理技术
- 返回中立值(如数值计算返回
0
)。 - 换用下一个正确数据。
- 返回与前次相同的数据。
- 使用最接近的合法值。
- 记录警告日志。
- 返回错误码或调用错误处理对象。
- 显示错误信息或关闭程序。
异常处理的建议
- 仅在真正例外的情况下抛出异常。
- 在适当的抽象层次抛出异常。
- 异常消息中包含完整信息。
- 避免空的
catch
语句。 - 集中管理异常报告。
- 标准化异常的使用。
- 隔离程序,防止错误扩散。
辅助调试的代码
- 在开发阶段引入调试代码。
- 计划移除开发版限制。
- 保留重要错误检查代码。
- 让技术支持人员记录错误信息。
伪代码编程过程
伪代码指导原则
- 用接近自然语言的语句描述操作。
- 避免目标编程语言的语法。
- 在意图层面编写伪代码。
- 细化到足够低的层次。
好处
- 方便评审和迭代优化。
- 减少注释工作量。
- 比设计文档更易维护。
创建子程序的步骤
- 设计子程序:
- 定义问题。
- 确定子程序的输入、输出和隐藏信息。
- 考虑错误处理与效率问题。
- 编写伪代码:
- 检查伪代码是否可行。
- 在伪代码中试验想法。
- 实现代码:
- 转换伪代码为注释,再填充代码。
- 检查代码是否需要进一步分解。
- 检查代码:
- 测试并优化子程序。
替代方案
- 测试驱动开发。
- 重构。
- 契约式开发。
- 灵活的模块化设计。