关于设计模式:设计模式

62次阅读

共计 7134 个字符,预计需要花费 18 分钟才能阅读完成。

设计模式

咱们常提到的设计模式源自 1994 年出版的 Design Patterns: Elements of Reusable Object-Oriented Software 一书。

书中用 C++ 形容了 23 种罕用的软件设计模式,这些模式能够分类如下:

  • 创立型,关注对象如何创立

    1. 工厂办法 Factory Method
    2. 形象工厂 Abstract Factory
    3. 建造者 Builder
    4. 原型 Prototype
    5. 单例 Singleton
  • 结构型

    1. 适配器 Adapter
    2. 桥 Bridge
    3. 组合 Composite
    4. 装璜器 Decorator
    5. 门面 Facade
    6. 享元 Flyweight
    7. 代理 Proxy
  • 行为型

    1. 责任链 Chain
    2. 命令 Command
    3. 解释器
    4. 迭代器
    5. 中介者
    6. 备忘录
    7. 观察者 Observer
    8. 状态 State
    9. 策略 Strategy
    10. 模版办法
    11. 访问者 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);

应用责任链模式,咱们须要认真思考的是:

  1. handler 形象接口的格局,入参和出参的类型,以及异样的解决。比方:
// handler 须要实现的接口
interface Handler {handle(Request request, Response response) throws Exception;
}

下面的接口类型示意 handler 的异样由外层的 Pipeline 解决。

  1. 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 类的代码交错在一起,会陷入失控。

引入状态模式能够解决扩大的问题:

  1. 将每个状态拆成独立的 class,它们实现独特的接口,包含简直所有的 Lift 行为。
  2. 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;}

以上就是本文的全部内容了,欢送评论点赞分享,或者关注公众号【写好代码】,谢谢。

正文完
 0