常见的设计模式--下
本文最后更新于 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 应用场景
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
- 感谢你赐予我前进的力量