本文最后更新于 2025-03-07,文章超过7天没更新,应该是已完结了~

五、观察者模式 & 发布订阅模式

5.1 观察者模式

观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。

在上图中,Subject(主题)就是发布者,而观察者就是小秦和小王。由于观察者模式支持简单的广播通信,当消息更新时,会自动通知所有的观察者。

下面我们来看一下如何使用 TypeScript 来实现观察者模式。

5.1.1 实现代码
interface Observer {
    notify: Function;
}

class ConcreteObserver implements Observer{
    constructor(private name: string) {}

    notify() {
        console.log(`${this.name} has been notified.`);
    }
}

class Subject { 
    private observers: Observer[] = [];

    public addObserver(observer: Observer): void {
        console.log(observer, "is pushed!");
        this.observers.push(observer);
    }

    public deleteObserver(observer: Observer): void {
        console.log("remove", observer);
        const n: number = this.observers.indexOf(observer);
        n != -1 && this.observers.splice(n, 1);
    }

    public notifyObservers(): void {
        console.log("notify all the observers", this.observers);
        this.observers.forEach(observer => observer.notify());
    }
}
5.1.2 使用示例
const subject: Subject = new Subject();
const xiaoQin = new ConcreteObserver("小秦");
const xiaoWang = new ConcreteObserver("小王");
subject.addObserver(xiaoQin);
subject.addObserver(xiaoWang);
subject.notifyObservers();

subject.deleteObserver(xiaoQin);
subject.notifyObservers();
5.1.3 应用场景及案例
  • 一个对象的行为依赖于另一个对象的状态。或者换一种说法,当被观察对象(目标对象)的状态发生改变时 ,会直接影响到观察对象的行为。

5.2 发布订阅模式

在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,然后分别发送给不同的订阅者。 同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。

在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。

在上图中,Publisher(发布者),Channels(通道)中 Topic A 和 Topic B 分别对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王和小池。

下面我们来看一下如何使用 TypeScript 来实现发布订阅模式。

5.2.1 实现代码
type EventHandler = (...args: any[]) => any;

class EventEmitter {
    private c = new Map<string, EventHandler[]>();

// 订阅指定的主题
subscribe(topic: string, ...handlers: EventHandler[]) {
    let topics = this.c.get(topic);
    if (!topics) {
        this.c.set(topic, topics = []);
    }
    topics.push(...handlers);
}

// 取消订阅指定的主题
unsubscribe(topic: string, handler?: EventHandler): boolean {
    if (!handler) {
        return this.c.delete(topic);
    }

    const topics = this.c.get(topic);
    if (!topics) {
        return false;
    }

    const index = topics.indexOf(handler);

    if (index < 0) {
        return false;
    }
    topics.splice(index, 1);
    if (topics.length === 0) {
        this.c.delete(topic);
    }
    return true;
}

// 为指定的主题发布消息
publish(topic: string, ...args: any[]): any[] | null {
    const topics = this.c.get(topic);
    if (!topics) {
        return null;
    }
    return topics.map(handler => {
        try {
            return handler(...args);
        } catch (e) {
            console.error(e);
            return null;
        }
    });
}
}
5.2.2 使用示例
const eventEmitter = new EventEmitter();
eventEmitter.subscribe("ts", (msg) => console.log(`收到订阅的消息:${msg}`) );

eventEmitter.publish("ts", "TypeScript发布订阅模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts", "TypeScript发布订阅模式");
5.2.3 应用场景
  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。

  • 作为事件总线,来实现不同组件间或模块间的通信。

六、策略模式

策略模式(Strategy Pattern)定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。

目前在一些主流的 Web 站点中,都提供了多种不同的登录方式。比如账号密码登录、手机验证码登录和第三方登录。为了方便维护不同的登录方式,我们可以把不同的登录方式封装成不同的登录策略。

下面我们来看一下如何使用策略模式来封装不同的登录方式。

6.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

interface Strategy {
  authenticate(...args: any): any;
}

class Authenticator {
  strategy: any;
  constructor() {
    this.strategy = null;
  }

  setStrategy(strategy: any) {
    this.strategy = strategy;
  }

  authenticate(...args: any) {
    if (!this.strategy) {
      console.log('尚未设置认证策略');
      return;
    }
    return this.strategy.authenticate(...args);
  }
}

class WechatStrategy implements Strategy {
  authenticate(wechatToken: string) {
    if (wechatToken !== '123') {
      console.log('无效的微信用户');
      return;
    }
    console.log('微信认证成功');
  }
}

class LocalStrategy implements Strategy {
  authenticate(username: string, password: string) {
    if (username !== 'abao' && password !== '123') {
      console.log('账号或密码错误');
      return;
    }
    console.log('账号和密码认证成功');
  }
}
6.2 使用示例
const auth = new Authenticator();

auth.setStrategy(new WechatStrategy());
auth.authenticate('123456');

auth.setStrategy(new LocalStrategy());
auth.authenticate('abao', '123');
6.3 应用场景及案例
  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。

  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。

七、职责链模式

职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

在公司中不同的岗位拥有不同的职责与权限。以上述的请假流程为例,当请 1 天假时,只要组长审批就可以了,不需要流转到主管和总监。如果职责链上的某个环节无法处理当前的请求,若含有下个环节,则会把请求转交给下个环节来处理。

在日常的软件开发过程中,对于职责链来说,一种常见的应用场景是中间件,下面我们来看一下如何利用职责链来处理请求。

7.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

import java.util.HashMap;
import java.util.Map;

// Define the Handler interface
interface IHandler {
    IHandler addMiddleware(IHandler handler);
    void get(String url, Callback callback);
}

// Define the Callback functional interface
@FunctionalInterface
interface Callback {
    void handle(Object data);
}

// AbstractHandler class implementing IHandler
abstract class AbstractHandler implements IHandler {
    protected IHandler next;

    @Override
    public IHandler addMiddleware(IHandler handler) {
        this.next = handler;
        return this.next;
    }

    @Override
    public void get(String url, Callback callback) {
        if (next != null) {
            next.get(url, callback);
        }
    }
}

// Auth middleware class
class Auth extends AbstractHandler {
    private boolean isAuthenticated;

    public Auth(String username, String password) {
        if ("abao".equals(username) && "123".equals(password)) {
            this.isAuthenticated = true;
        } else {
            this.isAuthenticated = false;
        }
    }

    @Override
    public void get(String url, Callback callback) {
        if (isAuthenticated) {
            super.get(url, callback);
        } else {
            throw new RuntimeException("Not Authorized");
        }
    }
}

// Logger middleware class
class Logger extends AbstractHandler {
    @Override
    public void get(String url, Callback callback) {
        System.out.println("/GET Request to: " + url);
        super.get(url, callback);
    }
}

// Route class
class Route extends AbstractHandler {
    private Map<String, Object> URLMaps;

    public Route() {
        URLMaps = new HashMap<>();
        URLMaps.put("/api/todos", new Object[]{new Todo("learn ts"), new Todo("learn react")});
        URLMaps.put("/api/random", Math.random());
    }

    @Override
    public void get(String url, Callback callback) {
        super.get(url, callback);
        if (URLMaps.containsKey(url)) {
            callback.handle(URLMaps.get(url));
        }
    }

    // Define Todo class for demonstration
    static class Todo {
        private String title;

        public Todo(String title) {
            this.title = title;
        }

        public String getTitle() {
            return title;
        }
    }
}
7.2 使用示例
const route = new Route();
route.addMiddleware(new Auth('abao', '123')).addMiddleware(new Logger());

route.get('/api/todos', data => {
  console.log(JSON.stringify({ data }, null, 2));
});

route.get('/api/random', data => {
  console.log(data);
});
7.3 应用场景
  • 可处理一个请求的对象集合应被动态指定。

  • 想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。

  • 有多个对象可以处理一个请求,哪个对象处理该请求运行时自动确定,客户端只需要把请求提交到链上即可。

八、模板方法模式

模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

在上图中,通过使用不同的解析器来分别解析 CSV 和 Markup 文件。虽然解析的是不同的类型的文件,但文件的处理流程是一样的。这里主要包含读取文件、解析文件和打印数据三个步骤。针对这个场景,我们就可以引入模板方法来封装以上三个步骤的处理顺序。

下面我们来看一下如何使用模板方法来实现上述的解析流程。

8.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

import fs from 'fs';

abstract class DataParser {
  data: string = '';
  out: any = null;

  // 这就是所谓的模板方法
  parse(pathUrl: string) {
    this.readFile(pathUrl);
    this.doParsing();
    this.printData();
  }

  readFile(pathUrl: string) {
    this.data = fs.readFileSync(pathUrl, 'utf8');
  }

  abstract doParsing(): void;

  printData() {
    console.log(this.out);
  }
}

class CSVParser extends DataParser {
  doParsing() {
    this.out = this.data.split(',');
  }
}

class MarkupParser extends DataParser {
  doParsing() {
    this.out = this.data.match(/<\w+>.*<\/\w+>/gim);
  }
}
8.2 使用示例
const csvPath = './data.csv';
const mdPath = './design-pattern.md';

new CSVParser().parse(csvPath);
new MarkupParser().parse(mdPath);
8.3 应用场景
  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。

  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。