超全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
最佳实践
- 第三方库集成:适配器模式在第三方库集成中非常常见。当需要使用一个第三方库提供的接口,并且该接口与现有代码的接口不兼容时,可以创建一个适配器类来将第三方库的接口转换成现有代码所期望的接口。这样可以避免修改现有代码,实现与第三方库的无缝集成。
- 数据格式转换:适配器模式在数据格式转换中也有应用。例如,当需要将一个对象的数据转换成另一种格式时,可以使用适配器模式。适配器类可以将原始对象的数据适配成目标格式,并提供新的接口供客户端使用。
- 跨平台开发:在跨平台开发中,适配器模式可以用于适配不同平台之间的差异。例如,当需要将一个应用程序从一个操作系统迁移到另一个操作系统时,可以使用适配器模式来适配不同操作系统的特定功能和接口。
适用场景
- 接口转换:当一个已有接口适配另一个接口时,可使用适配器模式
- 系统集成:当需要多个独立系统或组件整合在一起时,可使用适配器模式来统一他们的接口
- 旧系统复用:当有一个旧系统或组件时,但又希望在旧系统中复用时
- 数据格式的转换:将一种数据格式转换成另一种数据格式。
优缺点
- 优点:
- 兼容性:适配器模式能够解决不同接口之间的兼容性问题,使不兼容的类可一起工作。
- 重用性:适配器模式可重用现有的类,而无需对他们进行修改。
- 灵活性:可在不破坏现有代码结构的情况下引入新的功能。
- 解耦性:可将客户端代码与具体的类解耦,使得客户端代码不需要了解适配器对象细节。
- 缺点:
- 增加了复杂性:引入适配器模式会增加代码的复杂性,因为需要额外的类和接口来实现适配器,且需要理解适配器的转换逻辑。
- 运行时靠小,适配器模式需要在运行时进行适配,在转换逻辑时可能需要进行额外的计算和处理,会导致性能的损耗
- 过多适配器:应避免在一个系统中过多的使用适配器模式,如果使用过多,可能需要重新考虑系统的设计与架构