设计模式
咱们常提到的设计模式源自 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 是一个函数。
// expressconst 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;}
以上就是本文的全部内容了,欢送评论点赞分享,或者关注公众号【写好代码】,谢谢。