在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。根据模式的目的来划分的话,GoF(Gang of Four)设计模式可以分为以下 3 种类型:
1、创建型模式:用来描述 “如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。包括单例、原型、工厂方法、抽象工厂和建造者 5 种模式。
2、结构型模式:用来描述如何将类或对象按照某种布局组成更大的结构。包括代理、适配器、桥接、装饰、外观、享元和组合 7 种模式。
3、行为型模式:用来识别对象之间的常用交流模式以及如何分配职责。包括模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录和解释器 11 种模式。
接下来阿宝哥将结合一些生活中的场景并通过精美的配图,来向大家介绍 9 种常用的设计模式。
建造者模式(Builder Pattern)将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
一辆小汽车 通常由 发动机、底盘、车身和电气设备 四大部分组成。汽车电气设备的内部构造很复杂,简单起见,我们只考虑三个部分:引擎、底盘和车身。
在现实生活中,小汽车也是由不同的零部件组装而成,比如上图中我们把小汽车分成引擎、底盘和车身三大部分。下面我们来看一下如何使用建造者模式来造车子。
class Car {
constructor(
public engine: string,
public chassis: string,
public body: string
) {}
}
class CarBuilder {
engine!: string; // 引擎
chassis!: string; // 底盘
body!: string; // 车身
addChassis(chassis: string) {
this.chassis = chassis;
return this;
}
addEngine(engine: string) {
this.engine = engine;
return this;
}
addBody(body: string) {
this.body = body;
return this;
}
build() {
return new Car(this.engine, this.chassis, this.body);
}
}
在以上代码中,我们定义一个 CarBuilder
类,并提供了 addChassis
、addEngine
和 addBody
3 个方法用于组装车子的不同部位,当车子的 3 个部分都组装完成后,调用 build
方法就可以开始造车。
const car = new CarBuilder()
.addEngine('v12')
.addBody('镁合金')
.addChassis('复合材料')
.build();
在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。
在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。
在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂会先判断用户选择的车型,然后按照对应的模型进行生产并在生产完成后交付给用户。
下面我们来看一下如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。
abstract class BMW {
abstract run(): void;
}
class BMW730 extends BMW {
run(): void {
console.log("BMW730 发动咯");
}
}
class BMW840 extends BMW {
run(): void {
console.log("BMW840 发动咯");
}
}
class BMWFactory {
public static produceBMW(model: "730" | "840"): BMW {
if (model === "730") {
return new BMW730();
} else {
return new BMW840();
}
}
}
在以上代码中,我们定义一个 BMWFactory
类,该类提供了一个静态的 produceBMW()
方法,用于根据不同的模型参数来创建不同型号的车子。
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");
bmw730.run();
bmw840.run();
工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。
在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车子,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。
同样,我们来看一下如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。
abstract class BMWFactory {
abstract produceBMW(): BMW;
}
class BMW730Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW730();
}
}
class BMW840Factory extends BMWFactory {
produceBMW(): BMW {
return new BMW840();
}
}
在以上代码中,我们分别创建了 BMW730Factory
和 BMW840Factory
两个工厂类,然后使用这两个类的实例来生产不同型号的车子。
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();
const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();
bmw730.run();
bmw840.run();
抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
在上图中,阿宝哥模拟了用户购车的流程,小王向 BMW 工厂订购了 BMW730,工厂按照 730 对应的模型进行生产并在生产完成后交付给小王。而小秦向同一个 BMW 工厂订购了 BMW840,工厂按照 840 对应的模型进行生产并在生产完成后交付给小秦。
下面我们来看一下如何使用抽象工厂来描述上述的购车过程。
abstract class BMWFactory {
abstract produce730BMW(): BMW730;
abstract produce840BMW(): BMW840;
}
class ConcreteBMWFactory extends BMWFactory {
produce730BMW(): BMW730 {
return new BMW730();
}
produce840BMW(): BMW840 {
return new BMW840();
}
}
const bmwFactory = new ConcreteBMWFactory();
const bmw730 = bmwFactory.produce730BMW();
const bmw840 = bmwFactory.produce840BMW();
bmw730.run();
bmw840.run();
单例模式(Singleton Pattern)是一种常用的模式,有一些对象我们往往只需要一个,比如全局缓存、浏览器中的 window 对象等。单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在上图中,阿宝哥模拟了借车的流程,小王临时有急事找阿宝哥借车子,阿宝哥家的车子刚好没用,就借给小王了。当天,小秦也需要用车子,也找阿宝哥借车,因为阿宝哥家里只有一辆车子,所以就没有车可借了。
对于车子来说,它虽然给生活带来了很大的便利,但养车也需要一笔不小的费用(车位费、油费和保养费等),所以阿宝哥家里只有一辆车子。
在开发软件系统时,如果遇到创建对象时耗时过多或耗资源过多,但又经常用到的对象,我们就可以考虑使用单例模式。
下面我们来看一下如何使用 TypeScript 来实现单例模式。
class Singleton {
// 定义私有的静态属性,来保存对象实例
private static singleton: Singleton;
private constructor() {}
// 提供一个静态的方法来获取对象实例
public static getInstance(): Singleton {
if (!Singleton.singleton) {
Singleton.singleton = new Singleton();
}
return Singleton.singleton;
}
}
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
在实际生活中,也存在适配器的使用场景,比如:港式插头转换器、电源适配器和 USB 转接口。而在软件工程中,适配器模式的作用是解决两个软件实体间的接口不兼容的问题**。**使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体就可以一起工作。
interface Logger {
info(message: string): Promise<void>;
}
interface CloudLogger {
sendToServer(message: string, type: string): Promise<void>;
}
class AliLogger implements CloudLogger {
public async sendToServer(message: string, type: string): Promise<void> {
console.info(message);
console.info('This Message was saved with AliLogger');
}
}
class CloudLoggerAdapter implements Logger {
protected cloudLogger: CloudLogger;
constructor (cloudLogger: CloudLogger) {
this.cloudLogger = cloudLogger;
}
public async info(message: string): Promise<void> {
await this.cloudLogger.sendToServer(message, 'info');
}
}
class NotificationService {
protected logger: Logger;
constructor (logger: Logger) {
this.logger = logger;
}
public async send(message: string): Promise<void> {
await this.logger.info(`Notification sended: ${message}`);
}
}
在以上代码中,因为 Logger
和 CloudLogger
这两个接口不匹配,所以我们引入了 CloudLoggerAdapter
适配器来解决兼容性问题。
(async () => {
const aliLogger = new AliLogger();
const cloudLoggerAdapter = new CloudLoggerAdapter(aliLogger);
const notificationService = new NotificationService(cloudLoggerAdapter);
await notificationService.send('Hello semlinker, To Cloud');
})();
观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。
在上图中,Subject(主题)就是阿宝哥的 TS 专题文章,而观察者就是小秦和小王。由于观察者模式支持简单的广播通信,当消息更新时,会自动通知所有的观察者。
下面我们来看一下如何使用 TypeScript 来实现观察者模式。
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());
}
}
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();
在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,然后分别发送给不同的订阅者。 同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。
在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。
在上图中,Publisher(发布者)是阿宝哥,Channels(通道)中 Topic A 和 Topic B 分别对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王和小池。
下面我们来看一下如何使用 TypeScript 来实现发布订阅模式。
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;
}
});
}
}
const eventEmitter = new EventEmitter();
eventEmitter.subscribe("ts", (msg) => console.log(`收到订阅的消息:${msg}`) );
eventEmitter.publish("ts", "TypeScript发布订阅模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts", "TypeScript发布订阅模式");
策略模式(Strategy Pattern)定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。
目前在一些主流的 Web 站点中,都提供了多种不同的登录方式。比如账号密码登录、手机验证码登录和第三方登录。为了方便维护不同的登录方式,我们可以把不同的登录方式封装成不同的登录策略。
下面我们来看一下如何使用策略模式来封装不同的登录方式。
为了更好地理解以下代码,我们先来看一下对应的 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('账号和密码认证成功');
}
}
const auth = new Authenticator();
auth.setStrategy(new WechatStrategy());
auth.authenticate('123456');
auth.setStrategy(new LocalStrategy());
auth.authenticate('abao', '123');
职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。
在公司中不同的岗位拥有不同的职责与权限。以上述的请假流程为例,当阿宝哥请 1 天假时,只要组长审批就可以了,不需要流转到主管和总监。如果职责链上的某个环节无法处理当前的请求,若含有下个环节,则会把请求转交给下个环节来处理。
在日常的软件开发过程中,对于职责链来说,一种常见的应用场景是中间件,下面我们来看一下如何利用职责链来处理请求。
为了更好地理解以下代码,我们先来看一下对应的 UML 类图:
interface IHandler {
addMiddleware(h: IHandler): IHandler;
get(url: string, callback: (data: any) => void): void;
}
abstract class AbstractHandler implements IHandler {
next!: IHandler;
addMiddleware(h: IHandler) {
this.next = h;
return this.next;
}
get(url: string, callback: (data: any) => void) {
if (this.next) {
return this.next.get(url, callback);
}
}
}
// 定义Auth中间件
class Auth extends AbstractHandler {
isAuthenticated: boolean;
constructor(username: string, password: string) {
super();
this.isAuthenticated = false;
if (username === 'abao' && password === '123') {
this.isAuthenticated = true;
}
}
get(url: string, callback: (data: any) => void) {
if (this.isAuthenticated) {
return super.get(url, callback);
} else {
throw new Error('Not Authorized');
}
}
}
// 定义Logger中间件
class Logger extends AbstractHandler {
get(url: string, callback: (data: any) => void) {
console.log('/GET Request to: ', url);
return super.get(url, callback);
}
}
class Route extends AbstractHandler {
URLMaps: {[key: string]: any};
constructor() {
super();
this.URLMaps = {
'/api/todos': [{ title: 'learn ts' }, { title: 'learn react' }],
'/api/random': Math.random(),
};
}
get(url: string, callback: (data: any) => void) {
super.get(url, callback);
if (this.URLMaps.hasOwnProperty(url)) {
callback(this.URLMaps[url]);
}
}
}
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);
});
模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
在上图中,阿宝哥通过使用不同的解析器来分别解析 CSV 和 Markup 文件。虽然解析的是不同的类型的文件,但文件的处理流程是一样的。这里主要包含读取文件、解析文件和打印数据三个步骤。针对这个场景,我们就可以引入模板方法来封装以上三个步骤的处理顺序。
下面我们来看一下如何使用模板方法来实现上述的解析流程。
为了更好地理解以下代码,我们先来看一下对应的 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);
}
}
const csvPath = './data.csv';
const mdPath = './design-pattern.md';
new CSVParser().parse(csvPath);
new MarkupParser().parse(mdPath);
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/EQ1_bEW7ti0xd3AcJHmLyw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。