这篇博客用于记录软件构造课程的主要学习内容,其实也就是对《代码大全》部分内容的整理。
保障代码的鲁棒性和可扩展性,是成为一名合格的软件工程🦁的重要基础。
代码是用来读的 – 67
软件构造导论
首要技术使命:管理复杂度
构造前期工作的目标(复查性工作):降低风险
辨明软件项目的类型:
商业系统(高度迭代)
使命攸关
性命攸关(序列式)
软件构造:编码、调试、一部分的详细设计和单元测试
类设计的第一步:类的接口设计
要对外展示一致的抽象层次
- 一个类应该实现并仅实现是个ADT
- 当实现了多个ADT时,可以考虑重新组织类
- 抽象层次要到应用层次、到数据结构层次不够(不能混合)
- 理解类接口应该捕获的抽象到底是哪一个
- 提供成对的服务:不要盲目创建相反操作(getter/setter)
- 把不相关的信息转移到其他类
- 思考子程序和数据之间的引用关系
- 让接口可编程,而不是表达语义
- 可编程部分:编译器可检查,接口数据类型、属性类型
- 语义部分:编译器不可检查,接口怎样被使用
- 不要添加与接口抽象不一致的公用成员
- 同时考虑抽象性和内聚性
好的封装,不要暴露自身数据结构和实现细节
- 尽可能限制类和成员的可访问性
- 最严格的访问级别
- 保护接口抽象的完整性
- 不要公开暴露成员数据
- 不要对类的使用者做任何假设
- no友元类
- 不要因为一个子程序只使用公用子程序就把它纳入公开接口-始终保持抽象一致
- 警惕从语义上破坏封装性
解决方案:针对接口编程
留意过于紧密的耦合
- 尽可能限制访问权限
- 避免友元
- 避免在公开的接口中暴露成员数据结构
- 警觉迪米特法则
设计 & 实现
包含
面向对象设计的主力技术
实现has a 关系
警惕超过七个数据成员的类
继承
表示一个类是另外一个类的特例
目的是写出更精简的代码
用public继承实现是一个的关系 — 基类对派生类将做什么设定了预期
对不可继承的类明确禁止
遵循里氏替换原则 — 基类中的所有子程序,用在他的派生类中拥有相同的语义,子类能够替换基类完成相同的语义,子类方法的前置条件更松,后置条件更严
只继承需要继承的部分 – 如果仅使用实现而不是接口则考虑使用包含
不要覆盖不可覆盖的成员函数
把公用的内容放在继承🌲尽可能高的地方
以下情况值得怀疑:
1 | 只有一个实例的类,**只有一个派生类的基类(不要为未来做设计)** |
构造函数
尽可能在所有构造函数中初始化所有数据成员
优先采用深拷贝
单件属性的实现: private构造函数
应该避免的类
万能类
无关紧要的类:只包含数据没有行为
用动词命名的类:只有行为没有数据
子程序设计
为什么创建子程序
避免代码重复
支持子类化
隐藏顺序
隐藏指针操作
提高可移植性
简化复杂的布尔判断
改善性能
防御式编程
主要思想:子程序不因传入错误的数据而被破坏,哪怕是由其他子程序产生的错误数据
换句话说就是要承认程序都会有问题。
区别于检查错误–防御式编程并不能排除所有错误
区别于调试–是一种防卫方式而不是补救方式
区别于测试–测试不是防御式的,用来验证代码是否错误
断言Assertions
定义:
在开发期间使用的让程序在运行时进行自检的代码
–是对开发人员的警告
–通常是一个子程序或者是宏
–断言为真,表示程序运行正常;为假表示代码出现了意料之外的错误
构成(两个参数)
–布尔表达式:描述假设为真的情况
–显示的信息:断言为假时
demo:
1 | assert denominator != 0: "denominator is unexpectly set to 0" |
断言的用途
总结:判断一个子程序或一段代码的前后置条件是否符合预期
(各种参数和流的状态)
什么时候使用Assertion
主要用于开发和维护阶段
生成产品代码时并不编译进去
断言用来检查永远不应该发生的情况
错误处理用于处理来自系统外部的数据
不变式Invariants
永远都应该为真的条件
内部不变式 – 程序运行到特定时刻应该为真的事实 – 如:assert x > 0
控制流不变式 – 断言不会被运行到的代码,如assert false: suit; – 注意:将语句放置在编译器认为不会被运行到的地方会报错 (例如可以放到switch的default中)
类不变式 – 指类对象作为有效的类成员必须满足的条件 – 如:assert person.age >= 0 && person.age < 150; – 在即将从public方法和构造函数中返回时断言类的不变式
Assertions 可看做可执行注解
Assertions 不能有任何副作用
错误处理技术
错误处理用来处理那些预料中可能发生的错误
常用技术
返回中立值
–继续执行操作并简单地返回一个没有危害的数 值
1 | 数值计算可以返回0 |
换用下一个正确的数据
–在处理数据流的时候,返回下一个正确的数据 即可
返回与上一次相同的数据
–“重用上一次正确的结果” –如windows系统崩溃后用上一次配置重启动
换用最接近的合法值
–常出现在数值超出其正常设定的上下界的时候 –Velocity的例子
把警告信息记录到日志文件中
–在使用这种方法的时候需要对错误信息进行标示,或者 将警告信息单独存放,以便快速查询定位 –可以和其他技术结合使用
………
错误处理的最恰当方法是根据出现错误的软件类别而定
人身安全性命攸关的软件
消费类软件
异常
用异常通知程序的其他部分发生了不可不是的错误
–提供了一种无法被忽视的错误通知机制
–消除了错误向外扩散的可能
但是调用子程序代码需要了解其中可能抛出的异常,弱化的封装性,增加了复杂度
不能用异常来推卸责任,乐意在局部处理掉的就在局部处理
不要在构造函数和析构函数中抛出异常
在恰当的抽象层次抛出异常
隔栏
隔栏的使用使断言和错误处理有了清晰的区分 隔栏部分包含了“脏数据”
–隔栏外部的程序应使用错误处理技术,在那里对数 据做的任何假定都是不安全的
通过隔离部分之后的是“干净数据”
–隔栏内部的程序就应使用断言技术,因为传进来的 数据应该己在通过隔栏时被清理过了
–隔栏内子程序出现错误数据,就是程序里的问题
辅助调试代码
进攻式编程
主动暴露可能出现的错误
常用方式:
- 确保断言是程序终止运行
- 完全填充分配到的所有内存
- 完全填充已经分配到的文件和流
- 确保default和else都能产生严重错误
- 在删除一个对象前把它填满垃圾数据
- 错误日志
计划移除调试辅助代码
产品中保留多少防御式编程代码
- 保留检查重要错误的代码
- 去掉检查细微错误的代码
- 去掉可以导致程序硬性崩溃的代码
- 保留可以让程序稳妥的崩溃的代码
- 记录错误信息
- 保证错误信息是友好的
指针和语句的编写
指针的优势举例:
计算一个一元实函数的定积分:
1 | //java可以定义一个接口,然后一个函数去实现这个接口 |
关于指针的构造技巧
不要吝啬指针变量的使用
—在链表中插入一个新节点的例子(如果新建一个指针指向current->next, 代码逻辑会清晰许多)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//注意按照正确顺序删除链表中的指针
insertNode->next = currentNode->next;
insertNode->previous = currentNode;
if ( currentNode->next != NULL){
currentNode->next->previous = insertNode;
}
currentNode->next= insertNode;
//optimize
Node * followingNode = startNode->next;
newMiddkeNode->next = followingNode;
newMiddkeNode->previous = startNode;
if ( followingNode != NULL){
followingNode>previous = newMiddkeNode;
}
startNode->next= newMiddkeNode;避免指针强类型转换
删除与释放指针
- 指针消亡了不代表其指向的内存会释放, 要记得delete或free
- 动态内存释放了,不代表指针会消亡或置NULL, 要记得将指针置NULL
- 悬挂指针
- 多次释放
- 在删除变量前检查非法指针、在删除变量后将指针置NULL
- 指针使用的范例:auto_ptr(注意:拷贝和赋值会转移指针所有权,防止二次释放)
把指针操作独立在子程序中
语句
条件语句
使用条件语句的指导原则
- 首先先写正常路径, 再处理不常见情况
- 把正常情况的处理发到if后面而不要放到else后面
- 确保等量分支的正确(差一)
- 让if语句后面跟一个有意义的语句
- 检查else
利用布尔函数封装复杂的检测
复杂的判断会降低代码可读性
善于利用一些编程语言的布尔判断的短路求值
避免深层嵌套的方法
- 重复判断一些条件
- 用if-then-else替换if-else — 并且在布尔表达式中允许一些冗余来使逻辑更加清晰
- 转换为switch-case
- 将一部分嵌套提取成子程序(提取的抽象层次要一致)
- 使用OO的方法,例如编写一个简单工厂
循环语句
带退出的循环
如果把循环条件写在循环的开始或结束处,那就需要写出一个半循环代码
所谓半循环代码就是为了完成整个循环写在循环外的代码
注意事项:
- 把所有退出条件放在一处
- 用注释来阐明操作意图