共计 7134 个字符,预计需要花费 18 分钟才能阅读完成。
设计模式
咱们常提到的设计模式源自 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;}
以上就是本文的全部内容了,欢送评论点赞分享,或者关注公众号【写好代码】,谢谢。