设计模式
我们常提到的设计模式源自 1994 年出版的 Design Patterns: Elements of Reusable Object-Oriented Software 一书。
书中用 C++ 描述了 23 种常用的软件设计模式,这些模式可以分类如下:
创建型,关注对象如何创建
- 工厂方法 Factory Method
- 抽象工厂 Abstract Factory
- 建造者 Builder
- 原型 Prototype
- 单例 Singleton
结构型
- 适配器 Adapter
- 桥 Bridge
- 组合 Composite
- 装饰器 Decorator
- 门面 Facade
- 享元 Flyweight
- 代理 Proxy
行为型
- 责任链 Chain
- 命令 Command
- 解释器
- 迭代器
- 中介者
- 备忘录
- 观察者 Observer
- 状态 State
- 策略 Strategy
- 模版方法
- 访问者 Visitor
有些设计模式太常见,比如单例、迭代器。有些设计模式太偏,比如解释器、备忘录。
本文只会讲这些模式:工厂方法、装饰器、责任链、命令、中介者、状态、策略、访问者。
正文开始之前,需要说明的是:
有些设计模式是基于 C++/Java 的 OOP 模式而产生的,其他语言不一定适用,或者有别的实现方式。
本文旨在了解设计模式的思想,而不在于形式。
在编码时切忌生搬硬套,明白原理,再结合自己使用的编程语言,写出 SOLID 代码才是学习设计模式的目的。
设计模式的目的不是减少代码数量,而是用抽象层将常变和相对不常变的代码隔开,从而收敛代码改动范围。
引入抽象层则会让代码量增加。这也是很多人困惑的一点,怎么本来几行的代码用了设计模式之后增长到一百多行?
工厂方法
工厂方法是指用一个方法来构造对象,而不是直接调用构造方法。如下:
interface Vehicle {
}
class Benz implements Vehicle {}
class Camry implements Vehicle {}
class CarFactory {
public static Vehicle create(String brand) {
if ("camry".equals(brand)) {
return new Camry();
}
return new Benz();
}
}
class Main {
public static void main(String[] args) {
// 调用构造方法
Vehicle car = new Benz();
// 工厂方法
// camry 甚至可以通过 args 传递进来
Vehicle car2 = CarFactory.create("camry");
}
}
为什么使用工厂方法呢?
因为 new SomeClass
来构建对象本质上属于硬编码,写死了类型。
为了将 object 的构建和使用分开,才引入了工厂函数作为中间抽象。
上面的例子中,使用工厂方法之后,还可以将 CarFactory.create 的参数写在配置文件,或者通过命令行传递。这样如果需要改变车辆品牌,只需要修改配置或者参数即可,不用修改编译代码。
仔细想一下,工厂方法其实和下面的代码没有本质区别。
final int MAX = 100;
// 引入抽象
// getMax 返回 int 类型
final int MAX = getMax();
之前 MAX 是硬编码的某个值,比如 100;
之后引入了函数 getMax,至于 getMax 返回的值是怎么来的我们不关心,只要是 int 类型即可。
getMax 的角色和工厂方法是一样的。
工厂方法在 Go 代码中随处可见,因为 Go 不支持 new SomeStruct
创建 object。比如:
type Person struct {
}
func NewPerson(params Params) *Person {
return &Person{
// ...
}
}
装饰器模式
装饰器用于增强原有的类型,而不改变暴露的接口。
不去修改原有的类型,而是使用外挂或者是新类型。
如下:
interface Writer {
int write(data byte[]);
}
class W implements Writer {
int write(data byte[]) {
// ... write data
}
}
Writer w = new W();
现在需要对 write 进行增强,比如流控、批量等特性。可以保留 W 类不变,提供新的类型。如下:
class W2 implements Writer {
private Writer w;
W2(Writer w) {
this.w = w;
}
int write(data byte[]) {
System.out.println("cache ...");
w.write(data);
System.out.println("flush ...");
}
}
Writer w = new W2(new W());
上面的代码用 W2 对 W 做了一些增强,然后将 W2 的实例暴露给变量 w。
对于 w 来说,自己收到的还是符合 Writer 接口的 object。
在支持装饰器的语言中,可以很容易的实现。比如 JavaScript:
class Cache {
// clone 在其他地方实现
// 将 cache 取出的数据 copy 一份
@clone
get(key: string) {
if (xxx) {
return xxx;
} else if (xxx) {
return yyy;
}
return zzz;
}
}
看上面的代码,需求是将 cache 取出的数据深 copy 一份。
如果不用装饰器,我们需要修改 get 方法内部的逻辑。如果逻辑复杂,比如分支众多,很容易漏掉某个 return。
而用装饰器这种外挂,就不需要去关心 get 方法的实现,更简单也更干净。
责任链模式
责任链类似流水线,在一条流水线上可以随意增加/删除处理环节,常用于对网络请求的处理。
比如 Java 的 netty,JavaScript 的 express 都是这种模式。
Pipeline pipeline = new Pipeline();
pipeline.add(handler1);
pipeline.add(handler2);
pipeline.add(handler3);
pipeline.handle(request);
使用责任链模式,我们需要仔细思考的是:
- handler 抽象接口的格式,入参和出参的类型,以及异常的处理。比如:
// handler 需要实现的接口
interface Handler {
handle(Request request, Response response) throws Exception;
}
上面的接口类型表示 handler 的异常由外层的 Pipeline 处理。
- handler 的流转与中断谁来控制。这个过程可以由外层的 Pipeline 来控制,也可以由 handler 自己来。比如:
interface Handler {
// 调用 next 可以执行下一个 handler
void handle(Request request, Response response, Next next);
}
在支持函数的语言,比如 Go 和 JavaScript 中,通常 handler 是一个函数。
// express
const app = express();
function handler1(request, response, next) {}
function handler2(request, response, next) {}
app.use(handler1);
app.use(handler2);
命令模式
命令模式用于将一组动作封装在一起,提供更简洁的接口。常用于事件相关的处理。
比如我们要依次执行 A B C 操作。那么可以把 A B C 封装成一个操作,暴露的接口如下:
interface Command {
void execute();
}
class SomeCommand implements Command {
// 执行命令依赖的 object,或者上下文
private A a;
private B b;
private C c;
SomeCommand(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
execute() {
a.doSomething();
b.doSomething();
c.doSomething();
}
}
class Main {
public static void main(String[] args) {
// 封装了 A B C 的操作
SomeCommand someCommand = new SomeCommand(a, b, c);
someCommand.execute();
}
}
上面的代码中本来应该由 Main.main 调用 a b c 来执行某种操作。但是现在它只需要创建 SomeCommand 命令,再执行一个方法即可。
通过 SomeCommand 的封装,Main.main 的逻辑变得更为清晰。在 Main.main 中增加更多的 command 也会更容易。
常见的应用:编辑器里复制按钮点击,需要执行复制操作。
// 复制按钮被点击了
CopyCommand command = new CopyCommand(editor);
// 具体的复制实现可以交给其他同学来完成
// 调用方不需要关心细节
command.execute();
中介者模式
当系统中多个模块相互耦合时,可以引入一个中间抽象,然后这些模块都依赖这个中间抽象,将网状拓扑简化成星型拓扑,依赖复杂度由 M*N 变成 N+1。
中介者典型的应用有 MVC 架构、EventBus、MessageQueue 或者 NodeJS 的 EventEmitter。
状态模式
假设一个 object 有 N 种状态和 M 种行为,状态和行为之间互相影响。比如:
enum State {
// 电梯暂停
Pause,
// 电梯上下运行中
Running,
// 电梯门开
Open;
}
// 电梯
class Lift {
State state;
void open() {
if (state == State.Running) {
throw new Error("Can't open when running");
}
System.out.println("open...");
state = State.Open;
}
void close() {
if (state == State.Running) {
throw new Error("Can't close when running");
}
System.out.println("close...");
state = State.Pause;
}
void run() {
if (state == State.Open) {
throw new Error("Close first");
}
System.out.println("run...");
state = State.Running;
}
void pause() {
if (state == State.Open) {
throw new Error("Close first");
}
state = State.Pause;
}
}
从上面的代码可以看到,电梯的状态和行为相互影响,都放在电梯类里。
目前电梯的状态和行为很少这么写问题不大,但是随着状态和行为的扩展,整个 Lift 类的代码交织在一起,会陷入失控。
引入状态模式可以解决扩展的问题:
- 将每个状态拆成独立的 class,它们实现共同的接口,包括几乎所有的 Lift 行为。
- Lift 只保存一个状态 object,而状态的流转和行为的控制则由这个 object 处理,因为每个状态知道自己的下一个状态是什么。
代码如下:
// 所有 state 类需要继承
abstract class State {
// State 需要持有 Lift
// 以便实现 lift 的状态流转
protected Lift lift;
public State(Lift lift) {
this.lift = lift;
}
public abstract void open();
public abstract void close();
public abstract void run();
public abstract void pause();
}
// OpenState 只关注 open 状态下的逻辑
// 这样单个 State 的逻辑就很清晰
class OpenState extends State {
public void open() {
System.out.println("Alread open");
}
public void close() {
System.out.println("Close");
// 流转到下一个状态
lift.setState(new PauseState(lift));
}
public void run() {
throw new Error("Close first");
}
public void pause() {
throw new Error("Close first");
}
}
class RunningState extends State {}
class PauseState extends State {}
class Lift {
// 初始状态
private State state = new RunningState(this);
public void setState(State state) {
this.state = state;
}
// 所有行为委托给 state 处理
void open() {
state.open();
}
void close() {
state.close();
}
void run() {
state.run();
}
void pause() {
state.pause();
}
}
可以看到,每个状态专注自身的逻辑。每增加一个状态,只需要增加一个对应的 State 类即可。
也有小小的缺点:每增加一个行为,则要修改全部的 State 类。
比较:你是愿意在一个大箱子里面找东西?还是愿意在分类更清晰的三个小箱子里面找东西?
策略模式
设想这种场景:一份数据可能会做不同的处理。
这种情况下,数据一般是相对不易改变的部分,这里指的是数据的结构,而非具体的数值。对数据的处理逻辑可能会经常改变。
还是那句话:在不常变和常变之间做出区分,引入中间抽象把它们隔离开。
把数据的处理提取成接口,剥离处理方法的具体实现,这种方式被成为策略模式。
最简单常见的策略模式如下:
const nums = [4, 3, 0, 8, 2];
// 从小到大
nums.sort(function (a, b) {
return a - b;
// 从大到小
return b - a;
});
// 从大到小
nums.sort(function (a, b) {
return b - a;
});
nums 数组提供了一个 sort 方法,具体怎么排序则由调用方自己决定,只要排序函数满足接口即可。
这样 nums 就不用提供 sortMinToMax/sortMaxToMin 等排序方法,调用方爱怎么排序就怎么排序,自己来。
策略模式还有一个很经典的应用场景:消息通信中,对不同的消息做不同的处理。
class Message {
public int code;
public byte[] data;
}
对消息的处理可以抽象成共同的接口:
abstract class MsgHandler {
// 可以处理的消息 code
public int[] codes;
public abstract void handle(Message msg);
}
// 消息处理的实现
class MsgHandler404 extends MsgHandler {
MsgHandler404() {
super();
this.codes = {
404,
};
}
public void handle(Message msg) {
// ... doSomething
}
}
class Handlers {
private Map<Integer, List<MsgHandler>> handlers = new Map<>();
// register 提供消息处理器的插拔机制
public void register(MsgHandler handler) {
for (int i = 0; i < handler.codes.length; i++) {
int code = handler.codes[i];
List<MsgHandler> list = handlers.get(code);
if (list == null) {
list = ArrayList<MsgHandler>();
handlers.put(code, list);
}
list.add(handler);
}
}
// 不再使用 switch (msg.code) 的方式处理消息
public void handle(Message msg) {
List<MsgHandler> list = handlers.get(msg.code);
for (int i = 0; i < list.size(); i++) {
MsgHandler handler = list.get(i);
handler.handle(msg);
}
}
}
访问者模式
访问者表示对数据的访问和处理。
访问者模式和策略模式有些像,都是强调数据本身与数据的处理逻辑相分离。
但是访问者模式通常适用于对数据做出比较复杂的处理。
class Data {
}
class Visitor {
private Data data;
Visitor(Data data) {
this.data = data;
}
SomeInfo report() {
// ... 处理数据
}
}
可以把访问者看作对数据的视图,或者 PhotoShop 的蒙板。
圆形的蒙板展示的数据是圆形,方形的蒙板展示的数据则是方形。
比如对于同一份字节数据,可以解析成不同的结果:
//
class ByteBuffer {
private byte[] data;
}
// 将 bytes 解析成 uint8 类型
class Uint8View {
private ByteBuffer buffer;
}
// 将 bytes 解析成 int16 类型
class Int16View {
private ByteBuffer buffer;
}
以上就是本文的全部内容了,欢迎评论点赞分享,或者关注公众号【写好代码】,谢谢。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。