软件设计

软件设计所要做的,就是构建出一个模型,使用这个模型连接需求与解决方案。同时根据模型定义规范,使用规范去约束模型的实现

关注点分离

对问题进行分解,每个模块只关心自己的问题,这是一种降低复杂性的原则,可以通过模块化、解耦合、单一职责原则和分层架构等手段来实现

复杂性

越复杂的系统,就越难理解和修改它

复杂性是一点一点积累起来的

症状

原因

降低

在进行模块开发时,要不留出简单的接口,要不留出简单的实现

前者方便了用户,后者方便了开发者自己

像配置参数就是可以避免处理重要问题从而将其留给用户的一个例子

模块

通过分离接口与实现,接口是实现的抽象,越抽象的接口代表的模块越深,通过更高层级的抽象来隐藏复杂性

了解一个系统,从:模型(做了什么事) --> 接口(根据做的什么找主线接口) --> 实现(为什么这么实现、用了什么关键技术)

信息隐藏

每个模块应封装一些知识,这些知识代表设计决策。该知识嵌入在模块的实现中,但不会出现在其对外提供的接口中

在设计模块时,应专注于执行每个任务所需的知识,而不是任务发生的顺序

如果模块隐藏了很多信息,则往往会增加模块提供的功能,同时还会减少其接口

通用模块

更通用的模块会隐藏更多的信息

通用接口往往更简单,使用的方法更少

直通方法

一种不执行任何操作的方法,只是将其参数传递给另一个方法

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);
    }
}

直通方法使类变浅

接口复制

直通方法导致了许多接口复制,但在一些情况下,接口复制也有其存在的意义,如方法的动态委派、装饰器模式等

接口与实现

接口与实现通常不同,如果两者相似,则表明该类很浅

一连串接口之间通常需要传递变量,如果这些变量在被调用接口用不到,就会增加复杂性,为了解决这个问题,可以通过引入上下文对象来屏蔽这些用不到变量

通过能在统一的地方中获取到这个上下文对象,来减少复杂性

分开还是合并

如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开

长方法并不总是坏的,以方法长度作为方法需要拆分的依据太过极端

异常处理

使用异常来避免处理困难:与其想出一种干净的方法来处理它,不如抛出一个异常并将问题平移给调用者

  1. 消除异常处理复杂性的最好方法:设计良好的对外接口,不抛出异常,将异常屏蔽在模块之内,会使类更深。但这种方式仅在模块外部不需要异常信息时才有意义

  2. 统一异常处理:在一个统一的位置处理异常,使用统一的方式,但对特定异常一无所知,这对于异常恢复成本较高的操作很有用

  3. 快速崩溃:在大多数应用程序中,有些异常是不值得恢复的,恢复机制将给程序增加相当大的复杂性

特殊情况会使代码更加复杂,为避免特殊情况,设计一种普通情况,这种方式可以自动处理特殊情况而无需任何额外的代码

多次设计

设计不同的方案,比较其之间的优劣

组件间交互

框架设计趋势

使用 DSL 来描述程序行为,通过 DSL 分离意图和实现

软件发展趋势

面向对象

继承分为实现继承和接口继承

敏捷开发

追求以快为主,是一种战术编程思维

单元测试

测试驱动开发

设计模式

设计的兼容性

协议兼容

在消息头或者消息类型编号预留一定的后续空间,这样后续对协议进行功能升级时,可以直接利用这些预留的编号

并且可以通过高版本协议兼容低版本协议,使用版本号的方式,当高版本的协议的代码检测到数据来源于低版本时,则能通过执行不同的代码,从而实现兼容的效果

API兼容

软件的演进会伴随着提供多更的接口,此时可以保留老接口,定义一个新接口,老接口的内部实现则转发到新接口上,老接口对外不变

数据兼容

对于功能升级,若存在老数据,需要根据业务规则调整这些数据的表现,无论是物理上的刷数据库,还是视图层面兜底展示