软件设计
软件设计所要做的,就是构建出一个模型,使用这个模型连接需求与解决方案。同时根据模型定义规范,使用规范去约束模型的实现
- 战术编程:赶紧完成需求 能跑就行
- 战略编程:预先做好良好的设计
关注点分离
对问题进行分解,每个模块只关心自己的问题,这是一种降低复杂性的原则,可以通过模块化、解耦合、单一职责原则和分层架构等手段来实现
复杂性
越复杂的系统,就越难理解和修改它
复杂性是一点一点积累起来的
症状
- 变更放大:似简单的变更需要在许多不同地方进行代码修改
- 认知负荷:开发人员需要多少知识才能完成一项任务
- 未知的未知:不知道修改哪些代码才能完成需求
原因
- 依赖:难以管理的依赖性增加复杂性
- 模糊:意义不明的实体也会增加复杂性
降低
在进行模块开发时,要不留出简单的接口,要不留出简单的实现
前者方便了用户,后者方便了开发者自己
像配置参数就是可以避免处理重要问题从而将其留给用户的一个例子
模块
通过分离接口与实现,接口是实现的抽象,越抽象的接口代表的模块越深,通过更高层级的抽象来隐藏复杂性
了解一个系统,从:模型(做了什么事) --> 接口(根据做的什么找主线接口) --> 实现(为什么这么实现、用了什么关键技术)
信息隐藏
每个模块应封装一些知识,这些知识代表设计决策。该知识嵌入在模块的实现中,但不会出现在其对外提供的接口中
在设计模块时,应专注于执行每个任务所需的知识,而不是任务发生的顺序
如果模块隐藏了很多信息,则往往会增加模块提供的功能,同时还会减少其接口
通用模块
- 为当前设计还是为未来设计
更通用的模块会隐藏更多的信息
通用接口往往更简单,使用的方法更少
层
直通方法
一种不执行任何操作的方法,只是将其参数传递给另一个方法
public class TextDocument ... {
private TextArea textArea;
private TextDocumentListener listener;
...
public Character getLastTypedCharacter() {
return textArea.getLastTypedCharacter();
}
public int getCursorOffset() {
return textArea.getCursorOffset();
}
public void insertString(String textToInsert, int offset) {
textArea.insertString(textToInsert, offset);
}
}
直通方法使类变浅
接口复制
直通方法导致了许多接口复制,但在一些情况下,接口复制也有其存在的意义,如方法的动态委派、装饰器模式等
接口与实现
接口与实现通常不同,如果两者相似,则表明该类很浅
一连串接口之间通常需要传递变量,如果这些变量在被调用接口用不到,就会增加复杂性,为了解决这个问题,可以通过引入上下文对象来屏蔽这些用不到变量
通过能在统一的地方中获取到这个上下文对象,来减少复杂性
分开还是合并
- 给定两个功能,它们应该在同一位置一起实现,还是应该分开实现?
- 选择一种结构,它可以隐藏最佳的信息,最少的依赖关系和最深的接口
如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开
- 两者是否信息共享
- 合并两者是否简化接口
- 合并是否消除重复代码
- 分离通用代码与专用代码
长方法并不总是坏的,以方法长度作为方法需要拆分的依据太过极端
异常处理
- 异常处理是大部分复杂性的来源
使用异常来避免处理困难:与其想出一种干净的方法来处理它,不如抛出一个异常并将问题平移给调用者
消除异常处理复杂性的最好方法:设计良好的对外接口,不抛出异常,将异常屏蔽在模块之内,会使类更深。但这种方式仅在模块外部不需要异常信息时才有意义
统一异常处理:在一个统一的位置处理异常,使用统一的方式,但对特定异常一无所知,这对于异常恢复成本较高的操作很有用
快速崩溃:在大多数应用程序中,有些异常是不值得恢复的,恢复机制将给程序增加相当大的复杂性
特殊情况会使代码更加复杂,为避免特殊情况,设计一种普通情况,这种方式可以自动处理特殊情况而无需任何额外的代码
多次设计
设计不同的方案,比较其之间的优劣
组件间交互
- CQS command query speration 命令查询分离
- SOC separation of concerns 关注点分离
框架设计趋势
使用 DSL 来描述程序行为,通过 DSL 分离意图和实现
- 约定优于配置
软件发展趋势
面向对象
继承分为实现继承和接口继承
- 尽量避免实现继承 使用组合替代实现继承
敏捷开发
追求以快为主,是一种战术编程思维
单元测试
- 重构的基石
测试驱动开发
- 一段对测试友好的代码并不意味着设计良好
设计模式
- 很强,但是很容易被过度使用
设计的兼容性
协议兼容
在消息头或者消息类型编号预留一定的后续空间,这样后续对协议进行功能升级时,可以直接利用这些预留的编号
并且可以通过高版本协议兼容低版本协议,使用版本号的方式,当高版本的协议的代码检测到数据来源于低版本时,则能通过执行不同的代码,从而实现兼容的效果
API兼容
软件的演进会伴随着提供多更的接口,此时可以保留老接口,定义一个新接口,老接口的内部实现则转发到新接口上,老接口对外不变
数据兼容
对于功能升级,若存在老数据,需要根据业务规则调整这些数据的表现,无论是物理上的刷数据库,还是视图层面兜底展示