共计 34082 个字符,预计需要花费 86 分钟才能阅读完成。
超全 60000 多字详解 14 种设计模式 (多图 + 代码 + 总结 +Demo)
之前读耗子叔文章时,看到过有句话 没有实际,再多的实践都是扯淡
,集体很同意。你感觉本人学会了,但实际与学会之间有着很大差异。
单例模式 (Singleton Pattern)
定义或概念
- 单例模式:
保障一个类仅有一个实例,并提供一个拜访的全局拜访点。
-
实现的关键步骤:
实现一个规范的单例模式其实就是用一个变量来示意是否曾经为以后类创立过实例化对象,若创立过,在下次获取或创立实例时间接返回之前创立的实例化对象即可,若没有创立过,则间接创立
。UML 类图
Demo
// 透明版 单例模式 var CreateStr = (function () { var instance = null; return function (str) {if (instance) {return instance;} this.str = str; return instance = this; } })(); CreateStr.prototype.getStr = function () {console.log(this.str); } let a = new CreateStr('s1'); let b = new CreateStr('s2'); console.log('a ------>', a); // {str: 's1'} console.log('b ------>', b); // {str: 's1'} a.getStr(); // s1 b.getStr(); // s1 console.log(a === b); // true // 代理版 单例模式 function CreateStr(str){ this.str = str; this.getStr();} CreateStr.prototype.getStr = function (){console.log(this.str); } var ProxyObj = (function () { var instance = null; return function (str) {if (!instance) {instance = new CreateStr(str); } return instance; } })(); var a = new ProxyObj('s1'); var b = new ProxyObj('s2'); console.log('a ------>', a); // CreateStr {str: 's1'} console.log('b ------>', b); // CreateStr {str: 's1'} a.getStr(); // s1 b.getStr(); // s1 console.log('b ------>', a === b); // true
最佳实际
- jQuery, lodash, moment ….
- 电商中的购物车(因为一个用户只有一个购物车)
- Vue 或 React 中全局状态治理(Vuex、Redux、Pinia)
-
全局组件
实用场景
- 全局缓存管理器
- 音讯总线
- 购物车
- 全局状态治理
-
全局事件管理器
优缺点
-
长处:
- 全局拜访和繁多实例:因为全局仅有一个实例对象,所以对单例的多个实例化都会失去的同一个实例,这就能够确保所有的对象都可拜访一个实例。
- 节俭资源:因为全局仅有一个实例对象,所以可节约系统资源,防止频繁创立和销毁对象,造成零碎性能的节约
-
毛病:
- 违反繁多职责准则:单例模式往往负责创立和治理实例,可能会导致职责过重
- 严密耦合:引入了全局拜访,使代码适度依赖,难以保护和测试
策略模式 (Strategy Pattern)
定义或概念
- 策略模式:
定义一系列的算法,将他们一个个封装,并使他们可互相替换。
UML 类图
Demo
// 示例 1: 薪资计算
var strategies = {S: function (salary) {return salary * 4;},
A: function (salary) {return salary * 3;},
B: function (salary) {return salary * 2;},
};
var calcBonus = function (level, salary) {return strategies[level](salary);
}
calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
// 示例 2:表单验证
let infoForm = {
username: "阿斯顿产生的",
password: "ss1sdf",
tel: 15829485647,
};
var strategies = {isEmpty: function (val, msg) {if (!val) return msg;
},
minLength: function (val, length, msg) {if (val.length < length) return msg;
},
isTel: function (val, msg) {if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
},
};
var validFn = function () {var validator = new Validator();
let {username, password, tel} = infoForm;
validator.add(username, [
{
strategy: "isEmpty",
msg: "用户名不能为空",
},
{
strategy: "minLength:6",
msg: "明码不能少于 6 位",
},
]);
validator.add(password, [
{
strategy: "minLength:6",
msg: "明码不能少于 6 位",
},
]);
validator.add(tel, [
{
strategy: "isTel",
msg: "手机号码格局不正确",
},
]);
var msg = validator.start();
return msg;
};
class Validator {constructor() {this.cache = [];
}
add(attr, rules) {for (let i = 0; i < rules.length; i++) {var rule = rules[i];
var ruleArr = rule.strategy.split(":");
var msg = rule.msg;
var cacheItem = this.createCacheItem(ruleArr, attr, msg);
this.cache.push(cacheItem);
}
}
start() {for (let i = 0; i < this.cache.length; i++) {var msg = this.cache[i]();
if (msg) return msg;
}
}
createCacheItem(ruleArr, attr, msg) {return function () {var strategy = ruleArr.shift();
ruleArr.unshift(attr);
ruleArr.push(msg);
return strategies[strategy].apply(attr, ruleArr);
};
}
}
function submit() {let msg = validFn();
if (msg) {Toast(msg);
return false;
}
console.log("verify success");
// .....
}
submit();
Demo 合成图
最佳实际
- 游戏角色行为:在游戏开发中,角色通常会存在攻打,进攻,逃跑等行为,可应用策略模式来实现每一个具体的行为,而游戏角色依据具体情况抉择具体的某一个行为即可。
- 图像处理滤镜:在图像处理利用中,会存在如黑白滤镜、含糊滤镜、锐化滤镜等。每个滤镜成果都能够通过一个具体的滤镜策略类来实现,用户能够在运行时抉择实用的滤镜策略。
- 领取形式:在电商零碎中,能够应用策略模式来解决不同的领取形式,如支付宝、微信领取、银联领取等。每种领取形式都能够通过一个具体的领取策略类来实现,客户端代码依据用户抉择的领取形式来动静抉择相应的策略。
- 邮件发送形式:在邮件发送零碎中,能够应用策略模式来抉择不同的邮件发送形式,如 SMTP、API 接口、队列等。每种发送形式都能够通过一个具体的发送策略类来实现,依据系统配置或用户抉择来动静抉择适合的策略。
实用场景
- 想应用对象中各种不同算法变体来在运行时切换算法时
-
领有很多在执行某些行为时有着不同的规定时
优缺点
-
长处:
- 利用组合,委托,多态等技术无效防止了多重条件语句
- 提供了对开封——关闭准则的完满反对
- 复用性较强,防止许多反复的 C,V 工作
-
毛病:
- 客户端要理解所有的策略类,才可抉择适合的策略类去应用
- 会在程序中增加较多的策略类和策略对象
留神点
- 其实,策略模式的实现并不简单,关键在于从策略模式的实现背地找到封装的变动,委托和多态性这些思维的价值。
代理模式 (Proxy Pattern)
定义或概念
-
代理模式的要害是有个两头者来协调你与对方之间的事件,只能通过两头者将事件转达给另一方
不应用代理模式与应用代理模式的差异
UML 类图
Demo
// 示例 1:图片占位 var createImg = (function () {var imgNode = document.createElement("img"); document.appendChild(imgNode); return {setSrc: function (src) {imgNode.src = src;}, }; })(); var proxyImg = (function () {var img = new Image(); img.onload = function () {createImg.setSrc(this.src); }; return {setSrc: function (src) {createImg.setSrc("loading.gif"); img.src = src; }, }; })(); proxyImg.setSrc("bg.jpg"); // 示例 2:代理合并申请数据 // 实在对象类 - 用于存储数据 class Storage {constructor() {this.data = []; } storeData(data) { // 模仿存储数据的操作 console.log(`Storing data: ${data}`); this.data.push(data); } displayData() {console.log("Stored data:", this.data); } } // 代理对象类 - 提早存储操作和定时暂停 class ProxyStorage {constructor() {this.storage = new Storage(); this.pendingData = []; this.timer = null; this.delay = 5000; // 定时存储的延迟时间设定为 5 秒 this.paused = false; // 初始状态为未暂停 } storeData(data) {this.pendingData.push(data); this.scheduleStorage();} scheduleStorage() {if (!this.paused && !this.timer) {this.timer = setTimeout(() => {this.flushPendingData(); this.timer = null; }, this.delay); } } flushPendingData() {this.pendingData.forEach((data) => {this.storage.storeData(data); }); this.pendingData = [];} pause() { this.paused = true; clearTimeout(this.timer); this.timer = null; } restart() { this.paused = false; this.scheduleStorage();} stop() {this.pause(); this.pendingData = [];} displayData() {this.storage.displayData(); } } // 应用代理对象进行数据存储 const proxyStorage = new ProxyStorage(); // 模仿数据产生 function generateData() {const data = Math.random(); // 这里应用随机数作为数据示例 proxyStorage.storeData(data); } // 调用 generateData() 来模仿产生数据 // 在某一个时间段内间断产生数据,但理论触发存储的工夫是提早了的 const intervalId = setInterval(generateData, 1000); // 模仿数据存储进行一段时间后,进行定时器并清空待存储的数据 setTimeout(() => {// proxyStorage.stop(); proxyStorage.pause(); console.log("Timer stopped and pending data cleared"); }, 8000); // 这里设定 8 秒后进行定时器和清空待存储的数据 // 模仿数据存储完结后,手动调用 displayData() 显示已存储的数据 setTimeout(() => {proxyStorage.displayData(); }, 15000); // 这里设定 15 秒后完结数据存储并展现存储后果 // 模仿数据存储复原定时器 setTimeout(() => {proxyStorage.restart(); console.log("Timer restarted"); clearInterval(intervalId); }, 20000); // 这里设定 20 秒后复原定时器 // 示例 3: 缓存代理 // input:const movieServiceProxy = new CachedMovieServiceProxy(); console.log(movieServiceProxy.getMovie(1)); // 输入电影信息并缓存 console.log(movieServiceProxy.getMovie(2)); // 输入电影信息并缓存 console.log(movieServiceProxy.getMovie(1)); // 从缓存中输入电影信息 // output:// Fetching movie with id 1 from the database... // Caching movie with id 1... {id: 1, title: "Movie A", director: "Director A"} // Fetching movie with id 2 from the database... // Caching movie with id 2... {id: 2, title: "Movie B", director: "Director B"} // Retrieving movie with id 1 from cache... {id: 1, title: "Movie A", director: "Director A"} // 实现:// 实在对象类 - 电影服务 class MovieService {constructor() { // 模仿电影数据 this.movies = [{ id: 1, title: "Movie A", director: "Director A"}, {id: 2, title: "Movie B", director: "Director B"}, {id: 3, title: "Movie C", director: "Director C"}, ]; } // 获取电影信息 getMovie(id) {console.log(`Fetching movie with id ${id} from the database...`); // 模仿从数据库获取电影信息的操作 const movie = this.movies.find((movie) => movie.id === id); return movie; } } // 代理对象类 - 缓存代理 class CachedMovieServiceProxy {constructor() {this.movieService = new MovieService(); this.cache = {};} // 获取电影信息(代理办法)getMovie(id) {if (this.cache[id]) { // 如果缓存中有对应的电影信息,则间接返回缓存数据 console.log(`Retrieving movie with id ${id} from cache...`); return this.cache[id]; } else { // 否则,调用实在对象的办法获取电影信息,并将后果存入缓存 const movie = this.movieService.getMovie(id); console.log(`Caching movie with id ${id}...`); this.cache[id] = movie; return movie; } } } // 应用代理对象获取电影信息 const movieServiceProxy = new CachedMovieServiceProxy(); console.log(movieServiceProxy.getMovie(1)); // 第一次申请,从实在对象获取并缓存 console.log(movieServiceProxy.getMovie(2)); // 第二次申请,从实在对象获取并缓存 console.log(movieServiceProxy.getMovie(1)); // 第三次申请,从缓存中获取
最佳实际
- 图片加载器:能够在图片加载前显示占位符,并在图片加载实现后替换为实在的图片。这样能够提供更好的用户体验,并在图片加载过程中进行性能优化。
- 数据库连接池:在数据库拜访中,代理对象能够治理和复用数据库连贯,防止频繁地创立和敞开连贯,从而进步零碎的性能和资源利用率。
- 平安代理:在应用程序中,代理对象能够管制对敏感资源的拜访,并进行身份验证和权限验证,确保只有通过受权的用户能够拜访受爱护的资源。
- 近程服务代理:在分布式系统中,代理对象能够封装网络通信细节,暗藏近程服务的具体实现细节,使得客户端能够通明地拜访近程服务。
- 日志记录代理:在应用程序中,代理对象能够在办法调用前后记录办法的输出参数、返回值和执行工夫等信息,用于调试、性能监控和谬误追踪。
-
虚构代理:在图形界面 (GUI) 应用程序中,代理对象能够提早加载简单的图形组件,只有在须要显示时才理论创立和加载组件,以此进步零碎的响应速度。
实用场景
- 访问控制:可用于限度对对象的拜访,例如来管制用户对一些敏感数据的拜访。
- 虚构代理:在须要从网络上加载大量的数据时,可应用虚构代理来优化,在须要时再加载数据。
- 爱护代理:因为代理模式能够管制对实在对象的拜访,因而能够爱护代理。
- 缓存代理:可用于实现一个高度重用,并且这个操作很好使的状况。
- 智能援用代理:当须要在拜访对象时须要执行一些额定的操作时,可应用智能援用代理。
- 日志记录:可用于在调用实在对象的办法前后进行日志记录,包含参数,返回后果等信息,便于调试和排查问题。
优缺点
-
长处:
- 管制拜访 / 减少安全性:可通过代理对象对实在对象的拜访进行管制,减少了对实在对象的爱护
- 提早初始化:将高开销的操作提早到真正须要的时候,可优化一些性能
- 封装性:可暗藏对象的复杂性,只须要与代理对象打交道即可
-
毛病:
- 减少复杂性:尽管代理模式可拆散关注点,但同时也减少了代码的复杂性,因为须要创立和治理代理对象
- 透明性问题:尽管透明性是一个长处,但如果适度应用,可能导致代码难以了解和调试。
- 性能开销:代理对象须要拦挡所有对原始对象的拜访,这会导致一些性能开销。
迭代器模式 (Iterator Pattern)
定义或概念
- 迭代器模式指的是外部提供了一个办法可让对象中的每个元素都拜访一次,而又不裸露其外部办法。
UML 类图
Demo
// 示例 1:繁难迭代器
var each = function (arr, callback) {for (let i = 0; i < arr.length; i++) {callback.call(arr[i], arr[i], i);
}
};
let arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9];
each(arrs, function (item, index) {console.log(item, index);
});
/**
1 0
2 1
3 2
4 3
5 4
6 5
7 6
8 7
9 8
*/
// 示例 2: 倒序迭代器
var reverseEach = function (arr, callback) {for (let i = arr.length; i >= 0; i--) {callback(arr[i], i);
}
};
let arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9];
reverseEach(arrs, function (item, index) {console.log(item, index);
});
/**
undefined 9
9 8
8 7
7 6
6 5
5 4
4 3
3 2
2 1
1 0
*/
// 示例 3: 停止迭代器
var each = function (arr, callback) {for (let i = 0; i < arr.length; i++) {if (callback(arr[i], arr[i], i) === false) break;
}
};
let arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9];
each(arrs, function (item, index) {
// item > 3 时终止循环
if (item > 3) return false;
console.log(item, index);
});
/**
1 1
2 2
3 3
*/
最佳实际
- 提早计算:迭代器模式可实现提早计算,在须要的时候再去计算元素,这种形式可进步代码的性能和效率,尤其是解决大量数据时。
- 函数式编程:迭代器模式在函数式编程中失去了广泛应用,例如:映射,过滤等等
-
ES6 迭代器:例子 ES6 内置的迭代器有:forEach()、map()、reduce()、filter()、for()
实用场景
- 须要解决简单数据结构:例如数组,对象,树等
- 按需迭代数据:在须要的时候才迭代数据,而不是一次性将所有数据都加载到内存中,可应用迭代器模式来实现提早计算。
- 实现函数式编程
- 自定义迭代器来迭代本人所需的简单数据结构
优缺点
-
长处:
- 封装数据构造:应用迭代器模式可将数据结构的实现细节封装起来,使代码更模块化,可保护和可重用
- 简化迭代操作
- 可提早计算
- 可组合性:可将不同的迭代器组合起来,实现更简单的迭代器操作,
- 函数式编程
-
毛病:
- 额定的开销:可能会减少肯定的额定开销,例如迭代器对象的创立和保护
- 复杂度减少:可能会减少代码的复杂度
公布——订阅模式 (Publish-Subscribe Pattern)
定义或概念
- 公布订阅模式又叫观察者模式,定义了对象之间的一对多的依赖关系,当一个对象的状态产生了变动,所有的依赖它的对象都将失去告诉。
UML 类图
Demo
// 示例 1:繁难实现
// 1. 指定好谁充当发布者
// 2. 给发布者增加一个缓存列表,用于寄存回调函数以此来告诉订阅者
// 3. 在公布音讯时,遍历缓存列表,以此触发回调函数外面的回调函数。class Office {constructor() {this.list = [];
}
listen(key, fn) {if (!this.list[key]) {this.list[key] = [];}
this.list[key].push(fn);
}
trigger() {let key = Array.prototype.shift.call(arguments);
let fns = this.list[key];
if (!fns || fns.length === 0) return false;
for (let i = 0; i < fns.length; i++) {let fn = fns[i];
fn.apply(this, arguments);
}
}
remove(key, fn) {let fns = this.list[key];
if (!fns) return false;
if (!fn) {fns && (fns.length = 0);
} else {for (let i = fns.length - 1; i >= 0; i--) {let _fn = fns[i];
if (_fn === fn) {fns.splice(i, 1);
}
}
}
}
}
let office = new Office();
office.listen("客户 1 =>", log);
office.listen("客户 2 =>", log);
function log(name, price, meter) {console.log(`${name}预约:价格:${price}, 平方数: ${meter}`);
}
office.remove('客户 1 =>', log);
// office.trigger("客户 1 =>", "小红", 3000000, 110); // 小红预约:价格:3000000, 平方数: 110
office.trigger("客户 2 =>", "小明", 2000000, 88); // 小明预约:价格:2000000, 平方数: 88
最佳实际
- 事件总线:在前端开发中,事件总线充当地方调度器,组件能够通过订阅感兴趣的事件来接管音讯,而其余组件能够通过公布事件来触发相应的动作和交互。
- 音讯队列零碎:在分布式系统和大规模利用中,发布者将音讯公布到特定的主题或队列中,而订阅者能够订阅感兴趣的主题或队列以接管音讯。这种模式能够不便地实现解耦、异步通信和音讯解决。
- 音讯推送服务:在实时通信和挪动利用中,服务器作为发布者将音讯推送到特定的主题或频道,而客户端作为订阅者能够订阅感兴趣的主题或频道以接管实时的音讯推送。
-
日志零碎:在日志记录和日志解决中,日志记录器充当发布者,将日志音讯公布到特定的主题,而日志处理器充当订阅者,订阅感兴趣的主题以接管日志音讯并进行相应的解决和存储。
实用场景
- 事件驱动框架:当零碎中存在多个组件或模块之间须要进行松耦合的音讯通信时,可应用公布订阅模式
-
实时通信:用以实现实时通信和音讯推送的利用中。
优缺点
-
长处:
- 解耦性:发布者和订阅者之间没有间接的依赖关系,它们只须要通过中介对象进行通信。这会使得零碎更加灵便和可扩大,能够不便地增加或移除发布者和订阅者。
- 涣散耦合:公布订阅模式使得发布者和订阅者之间的耦合度升高,它们能够独立进行开发和演变,而不须要关注彼此的具体实现细节。
- 异步通信:公布订阅模式通常反对异步通信,发布者能够在任何工夫公布事件,而订阅者能够在本人的工夫解决这些事件。这种异步性有助于进步零碎的性能和响应能力。
- 在理论利用中,公布订阅模式被宽泛用于事件驱动的零碎,例如用户界面(UI)的事件处理、音讯队列零碎、日志零碎等。它提供了一种灵便的机制,用于将不同组件或模块之间的通信解耦,并反对异步消息传递。
-
毛病:
- 零碎性能:公布订阅模式引入了中间件或事件总线来解决音讯的散发和调度,这可能会减少肯定的零碎开销。
- 程序性和可靠性:公布订阅模式通常不保障音讯的程序性和可靠性。音讯的传递是异步的,并且无奈保障订阅者依照特定的程序接管音讯。
命令模式
定义或概念
-
命令模式指的是一个执行某些特定的指令。
UML 类图
Demo
// 示例 1:保留文件 // 命令接口 class Command {execute() {} undo() {} redo() {} } // 具体命令:打开文档 class OpenDocumentCommand extends Command {constructor(document) {super(); this.document = document; } execute() {this.document.open(); } undo() {this.document.close(); } redo() {this.execute(); } } // 具体命令:保存文档 class SaveDocumentCommand extends Command {constructor(document) {super(); this.document = document; } execute() {this.document.save(); } undo() { // 撤销保留操作,复原文档到上一个保留点或初始状态 this.document.restore();} redo() {this.execute(); } } // 接收者:文档 class Document {constructor(name) { this.name = name; this.content = ""; this.savedContent = ""; } open() {console.log(` 打开文档:${this.name}`); } close() {console.log(` 敞开文档:${this.name}`); } save() { this.savedContent = this.content; console.log(` 保存文档:${this.name}`); } restore() { this.content = this.savedContent; console.log(` 复原文档:${this.name}`); } setContent(content) { this.content = content; console.log(` 设置文档内容:${this.name}`); } getContent() {return this.content;} } // 调用者:按钮 class Button {constructor() {this.commandQueue = []; this.undoStack = []; this.redoStack = [];} // 将命令退出队列 addToQueue(command) {this.commandQueue.push(command); } // 执行队列中的命令 executeQueue() {console.log("执行命令队列:"); while (this.commandQueue.length > 0) {const command = this.commandQueue.shift(); command.execute(); this.undoStack.push(command); } } // 撤销上一次执行的命令 undo() {if (this.undoStack.length === 0) {console.log("没有可撤销的命令"); return; } const command = this.undoStack.pop(); command.undo(); this.redoStack.push(command); console.log("撤销上一次命令"); } // 重做上一次撤销的命令 redo() {if (this.redoStack.length === 0) {console.log("没有可重做的命令"); return; } const command = this.redoStack.pop(); command.redo(); this.undoStack.push(command); console.log("重做上一次撤销的命令"); } } // 应用示例 const document = new Document("example.txt"); // 创立按钮 const button = new Button(); // 创立打开文档命令并关联文档对象 const openCommand = new OpenDocumentCommand(document); // 创立保存文档命令并关联文档对象 const saveCommand = new SaveDocumentCommand(document); // 将命令退出队列 button.addToQueue(openCommand); button.addToQueue(saveCommand); // 执行命令队列 button.executeQueue(); // 撤销命令 button.undo(); // 重做命令 button.redo();
最佳实际
- 菜单和工具栏:在图形用户界面 GUI 中,每个菜单栏或工具栏按钮可关联一个命令对象,当点击菜单和按钮时,会执行相应的命令操作。
- 遥控器和智能家居:遥控器按钮和智能设施管制界面都可关联一个命令对象,以此来管制设施来执行相应命令的操作。
实用场景
- 撤销和重做
- 异步工作解决:若在后盾解决数据或执行长时间运行的操作。
- 日志记录和零碎操作记录
- 队列和调度工作:可将命令对象增加到队列中,而后依照队列中的程序顺次执行。
优缺点
-
长处:
- 解耦发送者和接收者:命令模式通过将申请封装为命令对象,将发送者和接收者解耦。发送者只须要晓得如何触摸命令,而不须要关怀具体的接收者和执行操作。
- 易扩大:因为命令模式将申请封装成了独立的命令对象,因而增加一个命令只须要实现一个新的命令的类,不须要批改原有的代码构造
- 反对队列化和提早执行:命令模式将多个命令对象组合成一个命令队列(宏命令),实现批量执行和撤销操作。也能够实现提早执行,将命令对象存储起来,在须要的时候在执行。
- 反对撤销和重做:通过保留命令的执行历史,可实现撤销和重做操作。
- 反对日志和记录:可记录命令的执行日志,用于零碎的跟踪和调试。
-
毛病:
- 减少了类的数量:随着引入命令模式的减少,会导致类的数量减少,减少代码的复杂性。
- 命令执行效率升高:因为将命令模式须要封装成对象,因而会减少肯定的执行开销,对于性能要求较高的场景可能会有影响。
组合模式 (Combination Pattern)
定义或概念
- 组合模式就是用小的子对象来构建更大的对象,而这些小的子对象自身兴许是由更小的子对象形成的。
-
组合模式将对象组合成树形构造,以示意“局部 - 整体”的层次结构。除了用来示意树形构造之外,组合模式的另一个益处是通过对象的多态性体现,使得用户对单个对象和组合对象的应用具备一致性。
宏命令与组合对象
Demo
var closeDoorCommand = {execute: function(){console.log( '关门'); } }; var openPcCommand = {execute: function(){console.log( '开电脑'); } }; var openQQCommand = {execute: function(){console.log( '登录 QQ'); } }; var MacroCommand = function(){ return {commandsList: [], add: function(command){this.commandsList.push( command); }, execute: function(){for ( var i = 0, command; command = this.commandsList[ i++]; ){command.execute(); } } } }; var macroCommand = MacroCommand(); macroCommand.add(closeDoorCommand); macroCommand.add(openPcCommand); macroCommand.add(openQQCommand); macroCommand.execute();
最佳实际
- 遥控器和智能家居设施
-
工具函数或性能
实用场景
- 示意对象的局部 - 整体层次结构:当你的对象具备层次结构,并且你心愿以统一的形式解决单个对象和组合对象时,能够应用组合模式。例如,树形构造、文件系统、菜单导航等。
- 客户心愿对立看待树中的所有对象:组合模式能够让客户端以对立的形式解决树中的所有对象,无需关怀对象的具体类型。这样能够简化客户端的代码逻辑。
- 须要递归遍历和操作简单的对象构造:组合模式能够不便地递归遍历和操作简单的对象构造,无论是增加、删除、批改还是查问操作。
- 须要对对象进行对立的操作和治理:组合模式能够将一组对象组织成一个整体,并对整体进行对立的操作和治理。这样能够进步代码的可维护性和可扩展性。
- 示意树形构造:通过上图可看到通过遍历 marcoCommand 组合对象,顺次执行每个子命令中的 execute 办法。这样就能够保障咱们只须要调用组合对象的 execute 办法,就能够顺次执行组合对象中的所有子命令。
-
对立看待组合对象和单个对象:也就是说咱们确定一个对象是不是命令,只须要判断这个对象是否有 execute 办法即可。
优缺点
-
长处:
- 简化客户端代码:组合模式使客户端可能以对立的形式看待单个对象和组合对象,从而简化了客户端的代码。客户端无需关怀对象是单个对象还是组合对象,只须要调用对立的接口进行操作。
- 灵活性和可扩展性:组合模式使得零碎可能不便地增加新的组件,无论是单个对象还是组合对象。它通过递归遍历整个对象树来进行操作,因而在不批改现有代码的状况下,能够很容易地增加新的对象。
- 层次结构的操作:组合模式十分实用于具备层次结构的对象汇合,并且须要对整体和局部对象进行递归操作的状况。它提供了一种简洁的形式来解决嵌套构造的操作。
- 代码重用:组合模式通过将操作利用于整体和局部对象,实现了代码的重用。能够在组合对象和单个对象上定义共享的办法和属性,从而防止了反复编写类似的代码。
-
毛病:
- 设计复杂性减少:应用组合模式会引入肯定的复杂性,特地是在解决递归操作时。对象的层次结构须要被正确地组织和治理,以确保正确的操作。
- 不适宜所有状况:组合模式并不适用于所有状况。如果对象的层次结构较简略,操作逻辑差别很大,或者不须要递归操作,应用组合模式可能会减少不必要的复杂性。
- 可能影响性能:因为组合模式波及递归操作和对象的档次遍历,可能会对性能产生肯定的影响。在解决大型对象树时,须要留神性能问题。
模板办法模式 (Template Method Pattern)
定义或概念
- 模板办法模式是一种只需应用继承就可实现的简略模式。
-
模板办法模式由两局部组成,第一局部是形象父类,第二局部是具体的实现子类。通常状况下在父类中封装了子类的算法框架,包含了实现一些公共办法以及子类中的办法执行程序。在子类中可继承父类,也可抉择重写父类的办法。
Demo
// 示例 1:泡茶和冲咖啡 class Beverage {init() {this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantCondiments()) {this.addCondiments(); } } boilWater() {console.log("1. 把水煮沸"); } // 充当冲泡和浸泡 brew() {throw Error("子类必须重写 brew 办法"); } pourInCup() {throw Error("子类必须重写 pourInCup 办法"); } addCondiments() {throw Error("子类必须重写 addCondiments 办法"); } customerWantCondiments() {return true;} } // 冲咖啡 class Coffee extends Beverage {constructor() { // 继承抽象类 super();} brew() {console.log("2. 用沸水冲泡咖啡"); } pourInCup() {console.log("3. 把咖啡倒进杯子"); } addCondiments() {console.log("4. 加糖或牛奶"); } customerWantCondiments() {return false;} } const coffee = new Coffee(); coffee.init(); /** 1. 把水煮沸 2. 用沸水冲泡咖啡 3. 把咖啡倒进杯子 */ // 泡茶 class Tea extends Beverage {constructor() {super(); } brew() {console.log("2. 用沸水浸泡茶叶"); } pourInCup() {console.log("3. 把茶水倒进杯子"); } addCondiments() {console.log("4. 加柠檬"); } } const tea = new Tea(); tea.init(); /** 1. 把水煮沸 2. 用沸水浸泡茶叶 3. 把茶水倒进杯子 4. 加柠檬 */
最佳实际
- 生命周期钩子:在对象的创立,初始化,销毁等不同阶段,通过定义模板办法和钩子办法,可管制对象在不同阶段的行为和逻辑
-
流程管制:可用于定义一个流程的模板办法,并在其中一次调用一系列的步骤办法,每个办法可有不同的子类提供具体的实现。这样可确保流程的一致性,且能够在须要时灵便的扩大和定制步骤。
实用场景
- 算法的整体框架曾经确定,但其中某些步骤的具体实现可能会有所变动。模板办法模式容许子类依据须要重写特定的步骤办法,从而定制算法的行为。
- 有多个类具备类似的行为模式,但其中某些步骤的实现可能有所不同。通过将这些独特的行为提取到父类的模板办法中,能够防止代码反复,并通过子类的具体实现来实现个性化的行为。
- 须要控制算法的执行流程,例如在初始化、操作和销毁等不同阶段执行特定的办法。模板办法模式提供了一个框架来定义这些步骤,并将管制流程放在模板办法中。
- 须要在不毁坏封装性的状况下,容许子类批改算法的局部步骤。模板办法模式通过将可变的局部放在可重写的办法中,放弃了封装性,并容许子类依据须要进行定制。
-
须要对算法进行扩大而不影响其整体构造。通过增加新的具体步骤办法或重写已有的办法,能够在不批改模板办法的状况下扩大算法的性能。
优缺点
-
长处:
- 提供了一种良好的代码复用机制:模板办法模式将算法的骨架定义在模板办法中,而将具体步骤的实现留给子类。这样能够防止代码的反复,并促成代码的复用。
- 提供了一种扩大算法的形式:通过子类化和重写具体步骤办法,能够在不批改模板办法的状况下扩大和定制算法的行为。这使得模板办法模式非常适合于在放弃算法整体构造不变的同时进行性能扩大。
- 封装算法的执行流程:模板办法模式将算法的执行流程封装在模板办法中,提供了一种对立的执行形式。这样能够确保算法的一致性,并简化了算法的应用。
- 促成了代码的钩子化:模板办法模式通过钩子办法(Hook Method)提供了一种在模板办法中提供默认实现但容许子类选择性笼罩的机制。这样能够在不毁坏封装性的状况下,容许子类对算法进行个性化定制。
-
毛病:
- 可能会导致类的个数减少:每个具体步骤都能够作为一个办法,从而导致子类的个数减少。这可能会减少类的复杂性和保护老本。
- 子类对模板办法的依赖性较高:子类须要继承和实现模板办法,这意味着子类与父类之间存在较高的耦合性。这可能限度了子类的灵活性和可替换性。
- 难以保护大型的模板办法:如果模板办法变得宏大而简单,可能会导致难以保护和了解。在这种状况下,可能须要从新评估设计,思考其余模式或重构算法的构造
享元模式 (Flyweight Pattern)
定义或概念
- 享元模式(Flyweight Pattern)次要用于缩小创建对象的数量,以缩小内存占用和进步性能。
- 享元模式是一种用工夫换空间的优化模式,要求将对象的属性分为外部状态和内部状态。
Demo
// 创立享元工厂
class FlyweightFactory {constructor() {this.flyweights = {};
}
getFlyweight(key) {if (!this.flyweights[key]) {this.flyweights[key] = new ConcreteFlyweight(key);
}
return this.flyweights[key];
}
getFlyweightsCount() {return Object.keys(this.flyweights).length;
}
}
// 创立享元对象
class ConcreteFlyweight {constructor(key) {this.key = key;}
operation() {console.log(`ConcreteFlyweight with key ${this.key} is being operated.`);
}
}
// 客户端代码
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight("key1");
flyweight1.operation();
const flyweight2 = factory.getFlyweight("key2");
flyweight2.operation();
const flyweight3 = factory.getFlyweight("key1"); // 复用已存在的享元对象
flyweight3.operation();
console.log(`Total flyweights created: ${factory.getFlyweightsCount()}`);
最佳实际
- 对象池:享元模式常被用于实现对象池。在须要频繁创立和销毁大量对象的场景下,通过共享对象实例能够升高内存耗费和进步性能。
- 游戏开发:在游戏开发中,有许多须要频繁创立的细粒度对象,如粒子、纹理、场景中的物体等。通过应用享元模式,能够共享这些对象的公共局部,缩小内存占用和进步渲染性能。
- 图形用户界面(GUI):在 GUI 应用程序中,有许多可视控件(如按钮、文本框、标签等)须要大量创立和治理
实用场景
- 一个零碎有大量雷同或者类似的对象,因为这类对象的大量应用,造成内存的大量消耗。
- 对象的大多数状态都能够内部化,能够将这些状态内部化,能够将这些对象内部化,也能够将这些对象设置为单例对象。
- 一个对象的行为齐全取决于其内部状态,而其外部状态不变。
优缺点
-
长处:
- 资源共享:享元模式通过共享对象实例,能够缩小零碎中的对象数量,节俭内存和其余资源的应用。它可能无效地解决大量类似对象的状况,进步零碎的性能和效率。
- 缩小反复对象:享元模式能够防止创立大量雷同或类似的对象,从而缩小反复的对象创立和初始化的开销。这对于须要频繁创立和销毁对象的场景特地有用。
- 状态内部化:享元模式将对象的状态内部化,使得对象在多个上下文中共享,而不须要在对象外部保护所有状态。这样能够简化对象的内部结构,进步对象的可复用性。
-
毛病:
- 共享状态的限度:享元模式要求享元对象之间必须共享一些状态,这种共享可能会限度对象的自由性。如果某个对象的状态须要扭转,可能须要批改共享的状态,从而影响到其余共享该状态的对象。
- 对象辨认简单:在应用享元模式时,须要对对象进行辨认和辨别,以便正确地共享对象。这可能须要引入额定的标识属性或者简单的逻辑,减少了零碎的复杂性。
- 减少了零碎复杂性:享元模式引入了共享对象和内部状态的概念,减少了零碎的复杂性。在设计和实现时须要认真思考对象的共享形式、共享状态的治理等问题,减少了开发和保护的难度。
职责链模式 (Responsibility Chain Pattern)
定义或概念
- 使多个对象都有机会解决申请,从而防止申请的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该申请,直到有一个对象解决它为止
-
职责链模式: 多个对象都有机会解决申请,他们把所有的对象都链成一条链,这个申请在这些对象之间顺次传递,直到遇到一个可解决它的对象,咱们可把这些对象称为职责链上的节点。
处理过程
Demo
// 实例 1:优惠打折流动 Function.prototype.after = function (fn) { const self = this; return function () {const ret = self.apply(this, arguments); if (ret === "next") {return fn.apply(this, arguments); } return ret; }; }; function order500(orderType, pay, stock) {if (orderType === 1 && pay) {console.log("500 元定金,失去 100 优惠券"); } else {return "next";} } function order200(orderType, pay, stock) {if (orderType === 2 && pay) {console.log("200 元定金,失去 50 优惠券"); } else {return "next";} } function orderNormal(orderType, pay, stock) {if (stock) {console.log("一般购买,无优惠券"); } else {console.log("商品库存有余"); } } const order = order500.after(order200).after(orderNormal); order(1, true, 500); // 500 元定金,失去 100 优惠券 order(2, true, 500); // 200 元定金,失去 50 优惠券 order(1, false, 500); // 一般购买,无优惠券
最佳实际
- 申请过滤器:职责链模式在 Web 开发中的申请过滤器也有广泛应用。在解决 Web 申请时,可能须要对申请进行一系列的解决和过滤,如身份验证、日志记录、输出验证等。每个过滤器能够负责其中一部分逻辑,并将申请传递给下一个过滤器,直到所有的解决逻辑都实现。
-
错误处理:职责链模式能够用于处理错误和异样。在一个零碎中,可能有多个谬误处理器,每个处理器负责解决特定类型的谬误。当零碎产生谬误时,谬误申请会被传递给第一个谬误处理器,如果它无奈解决,则传递给下一个处理器,直到找到可能解决该谬误的处理器。
实用场景
- 链式操作
- 身份验证和受权零碎:申请须要通过多个验证和步骤,每个步骤由不同的解决者来负责。
- 消息传递零碎:须要依照肯定的规定进行传递和解决
- 数据校验和过滤:例如表单数据的验证和过滤解决
优缺点
-
长处:
- 解耦责任:可将申请的发送者和接收者进行解耦,发送者无需晓得申请由那个具体的处理器解决,进步了零碎的灵活性和可维护性
- 灵活性:应用职责链模式后,链中的节点对象可灵便的拆分重组
- 可扩展性:可手动指定起始节点,并非非得从链中第一个节点开始。
-
毛病:
- 申请未被解决的危险:如果职责链中的所有节点都无奈解决该申请,就须要设置一个兜底处理器来进行解决。
- 不能保障某个申请肯定会被链中节点解决
- 性能影响:如果处理器的逻辑太过简单,可能会影响零碎的性能
中介者模式 (Mediator Pattern)
定义或概念
-
解耦对象与对象之间的紧耦合关系。通过减少一个中介者来让所有相干的对象都通过中介者来通信,而不是各个对象之前通信,当其中一个对象产生了扭转,只须要告诉中介者即可。
利用关系
- 传统对象与对象间的简单关系
-
中介者模式下的对象与对象间的关系
Demo
class Olympiad {constructor() {this.players = []; } join(player) {this.players.push(player); } exit(player) { this.players.splice(this.players.findIndex((item) => item.name === player.name), 1 ); } getResult() {console.log("参赛所有方", this.players); this.players.forEach((item) => {console.log(item.name, item.state); }); } } class player {constructor(name) { this.name = name; this.state = "ready"; } lose() {this.state = "lose";} win() {this.state = "win";} } const rabbit = new player("兔子"); const bear = new player("北极熊"); const chicken = new player("高卢鸡"); const eagle = new player("白头鹰"); const johnBull = new player("约翰牛"); const olympiad = new Olympiad(); olympiad.join(rabbit); olympiad.join(bear); olympiad.join(chicken); olympiad.join(eagle); olympiad.join(johnBull); olympiad.exit(chicken); rabbit.win(); bear.win(); eagle.lose(); johnBull.lose(); olympiad.getResult();
最佳实际
- 聊天利用:中介者模式在聊天利用中有广泛应用。在一个聊天室中,多个用户之间须要进行音讯的发送和接管。通过引入中介者作为聊天室的核心组件,用户之间的音讯能够通过中介者进行交互和播送,从而升高了用户之间的间接耦合。
- 航空控制系统:在航空控制系统中,飞机、雷达、航空公司等各个组件之间须要进行信息的交互和协调。通过引入中介者作为核心协调者,各个组件之间的通信能够通过中介者进行,从而简化了零碎的复杂性和耦合度。
- MVC 框架:中介者模式在 MVC(Model-View-Controller)框架中的控制器(Controller)中有利用。控制器作为中介者,接管用户的申请并协调模型(Model)和视图(View)之间的交互。通过中介者模式,实现了模型和视图的解耦,进步了零碎的可维护性和扩展性。
-
电子商务系统:在电子商务系统中,购物车和库存之间的交互通常波及多个对象的合作。通过引入中介者,购物车和库存之间的交互能够由中介者进行协调,从而缩小了对象之间的间接依赖关系。
实用场景
- 对象间的简单交互
- 耦合度高
- 多个对象共享通用行为
- 限度条件对象间的通信
优缺点
-
长处:
- 解耦对象间的交互
- 代码的复用性
- 简化每个对象之间的保护
-
毛病:
- 中介者对象的复杂性:随着零碎中的对象一直减少,中介者对象的复杂性也会减少。
- 违反了繁多职责准则
装璜者模式 (Decorator Pattern)
定义或概念
-
装璜器模式可在不扭转现有对象解构的根底上,动静地为对象增加性能
Demo
// 示例 1:传统装璜器 var plane = {fire: function () {console.log("一般子弹"); }, }; var missleDecorator = function () {console.log("发射导弹"); }; var atomDecorator = function () {console.log("发射原子弹"); }; var fire1 = plane.fire; plane.fire = function () {fire1(); missleDecorator();}; var fire2 = plane.fire; plane.fire = function () {fire2(); atomDecorator();}; plane.fire(); /** 一般子弹 发射导弹 发射原子弹 */ // 示例 2:Function.prototype.before = function (beforeFn) { var _self = this; return function () {beforeFn.apply(this, arguments); return _self.apply(this, arguments); }; }; Function.prototype.after = function (afterFn) { var _self = this; return function () {var ret = _self.apply(this, arguments); afterFn.apply(this, arguments); return ret; } } var o1 = function(){console.log('1'); } var o2 = function(){console.log('2'); } var o3 = function(){console.log('3'); } var desctor = o1.after(o2); desctor = desctor.after(o3); desctor(); /** var desctor = o1.before(o2); desctor = desctor.before(o3); desctor(); 3 2 1 var desctor = o1.after(o2); desctor = desctor.before(o3); desctor(); 3 1 2 var desctor = o1.before(o2); desctor = desctor.after(o3); desctor(); 2 1 3 var desctor = o1.after(o2); desctor = desctor.after(o3); desctor(); 1 2 3 */
<!-- 示例 3:日志上报 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP 日志上报 </title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button class="btn" @click="handler">Button</button>
<p id="tt">{{message}}</p>
</div>
</body>
</html>
<script type="text/javascript">
// log report
const {reactive, ref, createApp} = Vue;
const app = createApp({setup() {const message = ref("未点击");
const count = ref(0);
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) {
var _self = this;
return function () {var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};
function handler() {message.value = ` 已点击 ${++count.value}`;
}
handler = handler.after(log);
function log() {
message.value = message.value + "-----> log reported";
console.log("log report");
}
return {
message,
handler,
};
},
});
app.mount("#app");
</script>
<!-- 实例 4:动静参数 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP 动静参数 </title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{message}}</div>
</body>
</html>
<script type="text/javascript">
const {reactive, ref, createApp} = Vue;
const app = createApp({setup() {const message = ref("empty params");
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};
Function.prototype.after = function (afterFn) {
var _self = this;
return function () {var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};
// demo1:// function func(params) {
// message.value = params;
// }
// func = func.before(function (params) {
// params.b = "b";
// });
// func({a: "a"});
// demo2:
function ajax(type, url, params){message.value = `${type} ----> ${url} -----> ${JSON.stringify(params)}`;
}
function getToken(){return 'token';}
ajax = ajax.before(function(type, url, params){params.token = getToken();
})
ajax('get', 'https://www.baidu.com/userinfo', {name: 'se', password: 'xsdsd'});
return {message,};
},
});
app.mount("#app");
</script>
<!-- 示例 5:表单校验 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP 表单验证 </title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<label>
姓名:<input
type="text"
v-model="data.name"
placeholder="请输出姓名"
/>
</label>
<label>
明码:<input
type="text"
v-model="data.pass"
placeholder="请输出明码"
/>
</label>
<p v-if="data.name || data.pass">{{data.name + '/' + data.pass}} ----after------> {{data.message}}</p>
<hr>
<button @click="submitBtn">submit</button>
</div>
</body>
</html>
<script type="text/javascript">
const {reactive, ref, createApp, watchEffect} = Vue;
const app = createApp({setup() {
const data = reactive({
name: "",
pass: "",
message: "",
});
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {if (beforeFn.apply(this, arguments) === false) return;
return _self.apply(this, arguments);
};
};
function valid() {if (!data.name || !data.pass) {alert("用户名或明码不能为空");
return false;
}
}
function formSubmit() {console.log("data ------>", data);
data.message = `${data.name} ------- ${data.pass}`;
}
formSubmit = formSubmit.before(valid);
function submitBtn() {formSubmit();
}
return {
data,
submitBtn,
};
},
});
app.mount("#app");
</script>
最佳实际
- 输出 / 输入流:在 Java 中的 I / O 流中,应用装璜器模式来包装输出 / 输入流,以增加额定的性能,如缓冲、加密、压缩等。通过层层嵌套装璜器,能够实现多种组合和性能的扩大。
- 权限管制:在权限控制系统中,装璜器模式能够用于增加额定的权限验证逻辑。能够定义一个根本的权限验证接口,并应用装璜器模式来包装不同的权限验证实现,从而实现不同级别的权限管制。
-
日志记录:装璜器模式在日志记录中有广泛应用。
实用场景
- 动静地扩大对象性能:当须要在运行时动静地为对象增加额定的性能或责任时,装璜器模式是一个很好的抉择
- 遵循开闭准则:如果你心愿在不批改现有代码的状况下扩大性能,而且要放弃代码的稳定性,装璜器模式是一个适合的解决方案。
- 拆散关注点:当你心愿将不同的性能拆散开来,使每个性能都有本人独立的装璜器类时,装璜器模式是有用的。每个装璜器只关注繁多的额定性能,这样能够使代码更加清晰、可读性更高,并且容易保护和测试。
- 多层次的性能组合:如果你须要实现多个性能的组合,而且每个性能都能够灵便抉择是否增加,装璜器模式能够很好地满足这个需要。通过重叠多个装璜器对象,能够依照特定的程序组合性能,实现各种组合形式。
- 继承关系的代替计划:当你面临相似于创立大量子类的状况时,装璜器模式能够作为继承关系的代替计划。通过应用装璜器模式,能够防止创立过多的子类,而是通过组合不同的装璜器来实现不同的性能组合。
优缺点
-
长处:
- 扩展性强:装璜器模式容许在不批改现有代码的状况下,动静地增加新性能或批改现有性能。通过应用装璜器,能够在运行时按需组合和重叠装璜器对象,实现各种组合形式,从而实现更多的性能扩大。
- 遵循开闭准则:装璜器模式通过增加装璜器类来扩大性能,而不是批改现有的代码。这样能够放弃原有代码的稳定性,合乎开闭准则,即对扩大凋谢,对批改敞开。
- 拆散关注点:装璜器模式将性能的扩大和外围性能拆散开来,每个装璜器类只关注繁多的额定性能。这样能够使代码更加清晰、可读性更高,并且容易保护和测试。
-
毛病:
- 减少复杂性:应用装璜器模式会减少额定的类和对象,引入了更多的复杂性和层次结构。这可能使代码变得更加简单,了解和调试起来可能更加艰难。
- 潜在的性能影响:因为装璜器模式波及多个对象的组合和重叠,可能会引入额定的运行时开销,对性能产生肯定的影响。尤其是当装璜器链较长时,可能会导致性能降落。
状态模式 (State Pattern)
定义或概念
状态模式是一种面向对象的设计模式,它容许一个对象在其外部状态扭转时扭转它的行为。
-
状态模式的关键在于辨别事物外部的状态,事物外部状态的扭转往往会带来事物的行为的扭转。
UML 类图
Demo
// 示例 1: 订单解决零碎 // 状态接口 class OrderState {constructor(order) {this.order = order;} // 定义状态办法 confirm() {throw new Error("confirm() method must be implemented."); } cancel() {throw new Error("cancel() method must be implemented."); } ship() {throw new Error("ship() method must be implemented."); } } // 具体状态类:待处理状态 class PendingState extends OrderState {confirm() {console.log("订单已确认"); this.order.setState(new ConfirmedState(this.order)); } cancel() {console.log("订单已勾销"); this.order.setState(new CancelledState(this.order)); } ship() {console.log("无奈发货,订单未确认"); } } // 具体状态类:已确认状态 class ConfirmedState extends OrderState {confirm() {console.log("订单已确认"); } cancel() {console.log("订单已勾销"); this.order.setState(new CancelledState(this.order)); } ship() {console.log("订单已发货"); this.order.setState(new ShippedState(this.order)); } } // 具体状态类:已发货状态 class ShippedState extends OrderState {confirm() {console.log("无奈确认,订单已发货"); } cancel() {console.log("无奈勾销,订单已发货"); } ship() {console.log("订单已发货"); } } // 具体状态类:已实现状态 class CompletedState extends OrderState {confirm() {console.log("无奈确认,订单已实现"); } cancel() {console.log("无奈勾销,订单已实现"); } ship() {console.log("无奈发货,订单已实现"); } } // 具体状态类:已勾销状态 class CancelledState extends OrderState {confirm() {console.log("无奈确认,订单已勾销"); } cancel() {console.log("无奈勾销,订单已勾销"); } ship() {console.log("无奈发货,订单已勾销"); } } // 上下文类:订单 class Order {constructor() { // 初始化状态 this.currentState = new PendingState(this); } // 设置以后状态 setState(state) {this.currentState = state;} // 执行确认操作 confirm() {this.currentState.confirm(); } // 执行勾销操作 cancel() {this.currentState.cancel(); } // 执行发货操作 ship() {this.currentState.ship(); } } // 示例用法 const order = new Order(); order.confirm(); // 输入: 订单已确认 order.ship(); // 输入: 无奈发货,订单未确认 order.cancel(); // 输入: 订单已勾销 order.confirm(); // 输入: 订单已确认 order.ship(); // 输入: 订单已发货 order.confirm(); // 输入: 无奈确认,订单已发货 order.cancel(); // 输入: 无奈勾销,订单已发货 order.ship(); // 输入: 订单已发货 order.confirm(); // 输入: 无奈确认,订单已实现 /** 好了,咱们能够来看下订单状态的流转过程:1. 初始状态(pending):当订单被创立后,订单处于待处理状态。此时可进行两个操作:确认(confirm)、勾销(cancel)。确认操作后可使状态转变为已确认状态,勾销操作后可使状态转变为已勾销状态。2. 已确认状态(confirm): 订单被确认后,此时可进行两种操作:勾销(cancel)、发货(ship)。勾销操作可使状态转变为已勾销状态,发货操作可使状态转变为已发货状态。3. 已发货状态 (ship): 订单发货后,无奈在进行确认(confirm) 操作,因为订单曾经在路上了。此时可进行两个操作:勾销 (cancel)、发货(ship)。勾销(cancel) 操作可使状态转变为已勾销状态,发货操作可使订单转变为已实现状态。4. 已实现状态(complete): 订单胜利领取后,进入已实现状态。此时无奈进行以下操作:确认(confirm)、勾销(cancel)、发货(ship),因为订单曾经实现 5. 已勾销状态(cancel): 订单被勾销后,进入已勾销状态,此时无奈进行以下操作:确认(confirm)、勾销(cancel)、发货(ship),因为订单曾经勾销 */ // 示例 2:通行信号灯 // 信号灯状态基类 class TrafficLightState {constructor(light) {this.light = light;} // 状态行为办法,子类须要实现具体逻辑 display() {} stopBlinking() {} } // 红灯状态 class RedLightState extends TrafficLightState {display() {console.log("红灯亮起"); this.light.setState(new GreenLightState(this.light)); } } // 绿灯状态 class GreenLightState extends TrafficLightState {display() {console.log("绿灯亮起"); this.light.setState(new YellowLightState(this.light)); } } // 黄灯状态 class YellowLightState extends TrafficLightState {display() {console.log("黄灯亮起"); this.light.setState(new RedLightState(this.light)); } } // 闪动状态 class BlinkingLightState extends TrafficLightState {constructor(light) {super(light); this.intervalId = null; } display() {console.log("闪动灯亮起"); this.intervalId = setInterval(() => {this.light.toggle(); }, 500); } stopBlinking() {console.log("闪动灯进行"); clearInterval(this.intervalId); this.light.setState(new RedLightState(this.light)); } } // 信号灯类 class TrafficLight {constructor() {this.state = new RedLightState(this); this.isLightOn = false; } setState(state) {this.state = state;} display() {this.state.display(); } toggle() { this.isLightOn = !this.isLightOn; console.log(` 灯光 ${this.isLightOn ? "亮起" : "燃烧"}`); } stopBlinking() {this.state.stopBlinking(); } } // 应用示例 const trafficLight = new TrafficLight(); trafficLight.display(); // 红灯亮起 trafficLight.display(); // 绿灯亮起 trafficLight.display(); // 黄灯亮起 trafficLight.setState(new BlinkingLightState(trafficLight)); trafficLight.display(); /** 灯光亮起 灯光燃烧 灯光亮起 灯光燃烧 灯光亮起 */ setTimeout(() => {trafficLight.stopBlinking(); // 闪动灯进行,变为红灯 }, 3000); /** 这段代码的状态转移过程如下:1. 初始状态为红灯状态(RedLightState)。运行 trafficLight.display(); 会输入 "红灯亮起",并将状态设置为绿灯状态。2. 绿灯状态(GreenLightState)是红灯状态的下一个状态。运行 trafficLight.display(); 会输入 "绿灯亮起",并将状态设置为黄灯状态。3. 黄灯状态(YellowLightState)是绿灯状态的下一个状态。运行 trafficLight.display(); 会输入 "黄灯亮起",并将状态设置为闪动状态。4. 闪动状态(BlinkingLightState)是黄灯状态的下一个状态。运行 trafficLight.display(); 会输入 "闪动灯亮起",并开始每隔 500 毫秒切换一次灯光状态,输入灯光状态信息。5. 在通过肯定工夫后,通过调用 trafficLight.stopBlinking(); 办法,闪动状态会进行。输入 "闪动灯进行",并将状态设置为红灯状态。*/
最佳实际
- 订单解决:状态模式在订单解决中有广泛应用。订单能够具备多个状态,如待领取、待发货、已发货、已实现等。每个状态能够定义对应的行为,如领取订单、发货订单、确认订单等。
- 线程调度:在多线程编程中,有不同的状态,如就绪、运行、阻塞等。每个状态能够定义对应的行为,如线程调度、资源分配、期待告诉等。状态模式能够通过定义不同的线程状态类,并将线程对象的行为委托给以后状态类,实现依据线程状态扭转行为的灵活性。
- 游戏角色:在游戏开发中,有不同的状态,如失常、受伤、死亡等。每个状态能够定义对应的行为,如挪动、攻打、受伤解决等。
-
文档编辑器:在文档编辑器中,有不同的状态,如只读、编辑、保留等。每个状态能够定义对应的行为,如编辑文本、保存文档等。
实用场景
- 对象的行为取决于其外部状态,并且在运行时须要动静扭转行为。
- 对象具备大量的状态和相应的行为,并且状态之间的转换较为简单。
- 须要在不批改现有代码的状况下,减少新的状态和相应的行为。
- 防止应用过多的条件语句来判断对象的状态,以进步代码的可读性和可维护性。
-
须要将对象的状态转换逻辑与具体的行为解耦,以便更好地治理和组织代码。
优缺点
-
长处:
- 封装状态的变动:将每个状态封装成一个独立的类,使得状态专壹的逻辑被封装在状态类中。这使得状态变动的逻辑与主体类拆散,进步了代码的可维护性和可扩展性
- 简化条件语句:通过将状态判断和状态行为拆散,防止了大量的条件语句。
- 合乎凋谢——关闭准则:当增加新的状态时,不须要扭转原有代码。
- 进步了代码的可扩大
-
毛病:
- 减少了类的数量:引入状态模式会减少零碎中的类的数量,每个状态都须要一个独立的类来示意,这会导致类的数量过多,减少了零碎的复杂性。
- 状态转移逻辑简单
- 不适宜状态过多的状况
适配器模式 (Adapter Pattern)
定义或概念
-
适配器模式的作用是解决两个软件实体间的接口不兼容问题。
Demo
// 示例 1:温度转换器 // 指标接口(Target Interface)定义了将摄氏度转换为华氏度的办法 class Temperature {convertToFahrenheit() {throw new Error("This method should be overridden."); } } // 须要适配的类(Adaptee)示意以摄氏度为单位的温度 class CelsiusTemperature {constructor(celsius) {this.celsius = celsius;} getCelsius() {return this.celsius;} } // 适配器类(Adapter)继承了指标接口并持有一个 celsiusTemperature 对象 class TemperatureAdapter extends Temperature {constructor(celsiusTemperature) {super(); this.celsiusTemperature = celsiusTemperature; } // 实现目标接口的办法,将摄氏度转换为华氏度 convertToFahrenheit() {const celsius = this.celsiusTemperature.getCelsius(); const fahrenheit = (celsius * 9) / 5 + 32; return fahrenheit; } } // 客户端代码 function clientCode(target) {const fahrenheit = target.convertToFahrenheit(); console.log(`Temperature in Fahrenheit: ${fahrenheit}°F`); } // 创立须要适配的对象 const celsiusTemperature = new CelsiusTemperature(25); // 创立适配器对象,将须要适配的对象传递给适配器 const adapter = new TemperatureAdapter(celsiusTemperature); // 客户端代码通过适配器来调用指标接口的办法 clientCode(adapter); // 77°F
最佳实际
- 第三方库集成:适配器模式在第三方库集成中十分常见。当须要应用一个第三方库提供的接口,并且该接口与现有代码的接口不兼容时,能够创立一个适配器类来将第三方库的接口转换成现有代码所冀望的接口。这样能够防止批改现有代码,实现与第三方库的无缝集成。
- 数据格式转换:适配器模式在数据格式转换中也有利用。例如,当须要将一个对象的数据转换成另一种格局时,能够应用适配器模式。适配器类能够将原始对象的数据适配成指标格局,并提供新的接口供客户端应用。
-
跨平台开发:在跨平台开发中,适配器模式能够用于适配不同平台之间的差别。例如,当须要将一个应用程序从一个操作系统迁徙到另一个操作系统时,能够应用适配器模式来适配不同操作系统的特定性能和接口。
实用场景
- 接口转换:当一个已有接口适配另一个接口时,可应用适配器模式
- 系统集成:当须要多个独立零碎或组件整合在一起时,可应用适配器模式来对立他们的接口
- 旧零碎复用:当有一个旧零碎或组件时,但又心愿在旧零碎中复用时
- 数据格式的转换:将一种数据格式转换成另一种数据格式。
优缺点
-
长处:
- 兼容性:适配器模式可能解决不同接口之间的兼容性问题,使不兼容的类可一起工作。
- 重用性:适配器模式可重用现有的类,而无需对他们进行批改。
- 灵活性:可在不毁坏现有代码构造的状况下引入新的性能。
- 解耦性:可将客户端代码与具体的类解耦,使得客户端代码不须要理解适配器对象细节。
-
毛病:
- 减少了复杂性:引入适配器模式会减少代码的复杂性,因为须要额定的类和接口来实现适配器,且须要了解适配器的转换逻辑。
- 运行时靠小,适配器模式须要在运行时进行适配,在转换逻辑时可能须要进行额定的计算和解决,会导致性能的损耗
- 过多适配器:应防止在一个零碎中过多的应用适配器模式,如果应用过多,可能须要重新考虑零碎的设计与架构
Tip: 文章局部内容参考于 曾探
大佬的《JavaScript 设计模式与开发实际》。文章仅做集体学习总结和常识汇总