Design is there to enable you to keep changing the software easily in the long term.
软件设计要关注长期变化,需要应对需求规模的膨胀。
软件最核心的三个部分:
分离关注点(Separation of concerns)
可测试性
软件设计过程中会将软件拆分成一个个小模块,要保证每个小模块的正确性,需要保证每个模块在开发阶段能够测试,而这就需要在设计过程中就保证每个模块是可测试的,这就是可测试性。
在设计一个 函数 / 模块 / 系统时,必须将可测试性纳入考量,以便于能够完成不同层次的测试,减少对集成环境的依赖。
如何了解一个软件的设计
软件设计中三者的关系:
- 模型:存在哪些类,以及它们间的关系
- 接口:具体的类中提供了哪些方法
- 实现:具体方法是怎么实现的
从操作系统的进程管理来理解:
- 进程管理的核心模型就包括进程模型和调度算法
- 接口包括,进程的创建、销毁以及调度算法的触发等
- 不同的调度算法就是一个个具体的实现
如何分析一个软件的模型
理解一个模型的关键在于,要了解这个模型设计的来龙去脉,知道它是如何解决相应的问题
如何分析一个软件的接口
如何分析一个软件的实现
理解一个实现,是以对模型和接口的理解为前提的。
程序设计语言
语言模型
程序设计语言本身也是一个软件,包含模型、接口和实现。 学习程序设计语言主要是为了学习程序设计语言提供的编程模型
语言的接口
语言的实现:运行时
做设计真正的地基,并不是程序设计语言,而是运行时,有了对于运行时的理解,我们甚至可以做出语言本身不支持的设计。
领域特定语言(Domain Specific Language, DSL)
从软件设计的角度看,DSL 最终呈现出来的语法只是一种接口,最重要的是它包裹的模型。
编程范式(Programming paradigm)
结构化编程(structured programming)
一旦构建起新的模型,底层实现是可以不断优化的。
面向对象编程(object-oriented programming)
- 封装是面向对象的根基,软件就是靠各种封装好的对象逐步组合出来的
- 继承给了继承体系内的所有对象一个约束,让它们有了统一的行为
- 多态让整个体系能够更好地应对未来的变化
封装
封装的要点是行为,数据只是实现细节
有了封装,对象就成了一个个可以组合的单元,也形成了一个个可以复用的单元。面向对象编程的思考方式就是组合这些单元,完成不同的功能。
方法的命名,体现的是意图,而不是具体怎么做。
将意图与实现分离开来
// 在说做什么
public void setPassword(final String password){}
// 体现意图
public void changePassword(final String password){}
减少暴露接口
最小化接口暴露,利于系统修改维护
class Service {
public void shutdownTimerTask() {
// 停止定时器任务
}
public void shutdownPollTask() {
// 停止轮询服务
}
}
// 调用处
class Application {
private Service service;
public void onShutdown() {
service.shutdownTimerTask();
service.shutdownPollTask();
}
}
仅暴露一个接口时
class Service {
public void shutdownTimerTask() {
// 停止定时器任务
}
public void shutdownPollTask() {
// 停止轮询服务
}
public void shutdown() {
this.shutdownTimerTask();
this.shutdownPollTask();
}
}
// 调用处
class Application {
private Service service;
public void onShutdown() {
service.shutdown();
}
}
继承
- 实现继承:尽可能用组合的方式替代
- 接口继承:多态
面向组合编程
分解是设计的第一步,分解的粒度越小越好。分解出关注点,而每个关注点就应该是一个独立的模块。 最终的类是由这些一个一个的小模块组合而成,这种编程的方式就是面向组合编程。即类是由多个小模块组合而成。
多态(Polymorphism)
只使用封装和继承的编程方式,称为基于对象(Object Based)编程,只有把多态加进来,才称为面向对象(Object Oriented)编程。
一个接口,多种形态
interface Shape {
// 绘图接口
void draw();
}
class Square implements Shape {
void draw() {
//画正方形
}
}
class Circle implements Shape {
void draw() {
// 画圆形
}
}
多态需要构建出一个抽象,而这需要寻找事物的共同点,地基则在于分离关注点
在构建抽象上,通过接口将变的部分和不变的部分分隔开。不变的部分是接口的约定,变的则是子类各自的实现。
接口是一个边界。无论是什么样的系统,清晰界定不同模块的职责很关键,而模块之间彼此通信最重要的就是通信协议。这种通信协议对应到代码层面上,就是接口。
函数式编程(functional programming)
函数是函数式编程的一等公民(first-class citizen):
- 可以按需创建
- 可以存储在数据结构中
- 可以当作实参传给另外一个函数
- 可以作为另一个函数的返回值
组合性
模型提供者提供出一个又一个的构造块,以及它们的组合方式。由使用者根据自己需要将这些构造块组合起来,提供出新的模型,供其他开发者使用。 模型之间一层一层地逐步叠加,构建起整个应用。
不变性
当需要改变时,返回一个新对象,而不是修改已有字段,这样能够避免原本引用该对象的位置发生变化。
设计原则与模式
单一职责原则(single responsibility principle)
单一职责原则和一个类只干一件事之间,最大的区别是,将变化纳入考量,更进一步就是将变化的来源纳入考量。 一个软件系统的最佳结构高度依赖于使用这个软件的组织的内部结构。
开放封闭原则(Open-closed principle)
开放封闭原则向我们描述的是一个结果,我们可以不修改代码而仅凭扩展就完成新功能。这个结果的前提是要在软件内部留好扩展点,每一个扩展点都是一个需要设计的模型。
Liskov 替换原则(Liskov substitution principle)
如果你发现了任何做运行时类型识别的代码,很有可能已经破坏了 LSP,因为它们没有统一的处理接口。
void handle(final Handler handler) {
if (handler instanceof ReportHandler) {
// 生成报告
((ReportHandler)handler).report();
return;
}
if (handler instanceof NotificationHandler) {
//发送通知
((NotificationHandler)handler).sendNotification();
}
}
接口隔离原则(Interface segregation principle)
识别对象的不同角色,设计小接口
在接口中,不要放置使用者用不到的方法。
如果一个接口修改了,依赖它的所有代码全部会受到影响,而这些代码往往也有依赖于它们实现的代码,这样一来,一个修改的影响就传播出去了。