设计模式-结构型
本文主要介绍下结构型设计模式,包括
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式和享元模式,提供前端场景和 ES6 代码的实现过程。 供自己以后查漏补缺,也欢迎同道朋友交流学习。
引言
本文主要介绍下结构型设计模式,包括适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式和享元模式,提供前端场景和 ES6 代码的实现过程。
什么是结构型
结构型模式(Structural Patterns)主要关注类和对象的组合。这些模式描述了如何将类或对象结合在一起形成更大的结构,同时保持结构的灵活和高效。结构型模式通过继承或组合的方式来简化系统的设计,解决对象之间的耦合问题,使得系统更加容易扩展和维护。
适配器模式(Adapter)
适配器模式(Adapter Pattern)将一个类的接口转换成客户希望的另一个接口,使原本因接口不兼容而不能一起工作的类可以一起工作。
适配器模式通常用于包装现有的类,以便与新的接口或系统进行交互。
前端中的适配器模式场景
- 接口数据适配:后端返回的数据结构可能与前端组件需要的数据结构不一致,可以使用适配器模式进行转换。
- 旧接口兼容:在系统重构或升级时,保持对旧接口的兼容性,使用适配器模式将新接口映射到旧接口。
- Vue计算属性 :Vue 中的
computed属性可以看作是一种适配器,将原始数据转换为视图需要的数据格式。
适配器模式-JS实现
javascript
// 旧接口
class OldCalculator {
constructor() {
this.operations = function(term1, term2, operation) {
switch (operation) {
case 'add':
return term1 + term2;
case 'sub':
return term1 - term2;
default:
return NaN;
}
};
}
}
// 新接口
class NewCalculator {
add(term1, term2) {
return term1 + term2;
}
sub(term1, term2) {
return term1 - term2;
}
}
// 适配器类
class CalculatorAdapter {
constructor() {
this.calculator = new NewCalculator();
}
operations(term1, term2, operation) {
switch (operation) {
case 'add':
return this.calculator.add(term1, term2);
case 'sub':
return this.calculator.sub(term1, term2);
default:
return NaN;
}
}
}
// 客户端调用
const oldCalc = new OldCalculator();
console.log(oldCalc.operations(10, 5, 'add')); // 15
const newCalc = new NewCalculator();
console.log(newCalc.add(10, 5)); // 15
const adapter = new CalculatorAdapter();
console.log(adapter.operations(10, 5, 'add')); // 15
装饰器模式(Decorator)
装饰器模式(Decorator Pattern)动态地给一个对象添加一些额外的职责,而不影响该对象所属类的其他实例。
装饰器模式提供了一种灵活的替代继承方案,用于扩展对象的功能。
前端中的装饰器模式场景
- 高阶组件(HOC) :在
React中,高阶组件本质上就是装饰器模式的应用,用于复用组件逻辑。 - 类装饰器 :在
ES7装饰器语法或TypeScript中,可以使用装饰器来增强类或类的方法,例如用于日志记录、性能监控、权限控制等。
装饰器模式-JS实现
javascript
// 原始对象
class Circle {
draw() {
console.log("画一个圆形");
}
}
// 装饰器基类
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw();
}
}
// 具体装饰器:添加红色边框
class RedBorderDecorator extends Decorator {
draw() {
this.circle.draw();
this.setRedBorder();
}
setRedBorder() {
console.log("添加红色边框");
}
}
// 客户端调用
const circle = new Circle();
circle.draw();
// 输出:
// 画一个圆形
const redCircle = new RedBorderDecorator(new Circle());
redCircle.draw();
// 输出:
// 画一个圆形
// 添加红色边框
代理模式(Proxy)
代理模式(Proxy Pattern)为其他对象提供一种代理以控制对这个对象的访问。
代理模式可以在访问对象之前或之后执行额外的操作,如权限验证、延迟加载、缓存等。
前端中的代理模式场景
- 数据响应式 :
Vue 3使用Proxy对象来实现数据的响应式系统,拦截对象的读取和修改操作。 - 网络请求代理 :在开发环境中,配置代理服务器(如
webpack-dev-server的 proxy)解决跨域问题。 - 虚拟代理:例如图片懒加载,先显示占位图,等图片加载完成后再替换为真实图片。
- 缓存代理:对于开销较大的计算结果或网络请求结果进行缓存,下次请求时直接返回缓存结果。
代理模式-JS实现
javascript
// 真实图片加载类
class RealImage {
constructor(fileName) {
this.fileName = fileName;
this.loadFromDisk(fileName);
}
loadFromDisk(fileName) {
console.log("正在从磁盘加载 " + fileName);
}
display() {
console.log("显示 " + this.fileName);
}
}
// 代理图片类
class ProxyImage {
constructor(fileName) {
this.fileName = fileName;
}
display() {
if (!this.realImage) {
this.realImage = new RealImage(this.fileName);
}
this.realImage.display();
}
}
// 客户端调用
const image = new ProxyImage("test.jpg");
// 第一次调用,加载图片
image.display();
// 输出:
// 正在从磁盘加载 test.jpg
// 显示 test.jpg
// 第二次调用,直接显示
image.display();
// 输出:
// 显示 test.jpg
外观模式(Facade)
外观模式(Facade Pattern)提供一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。
前端中的外观模式场景
- 浏览器兼容性封装 :封装不同浏览器的
API差异,提供统一的接口。例如,封装事件监听函数,统一处理addEventListener和attachEvent。 - 简化复杂库的使用 :例如
jQuery或Axios,它们为复杂的原生 DOM 操作或 XMLHttpRequest 提供了简单易用的接口。
外观模式-JS实现
javascript
// 子系统1:灯光
class Light {
on() {
console.log("开灯");
}
off() {
console.log("关灯");
}
}
// 子系统2:电视
class TV {
on() {
console.log("打开电视");
}
off() {
console.log("关闭电视");
}
}
// 子系统3:音响
class SoundSystem {
on() {
console.log("打开音响");
}
off() {
console.log("关闭音响");
}
}
// 外观类:家庭影院
class HomeTheaterFacade {
constructor(light, tv, sound) {
this.light = light;
this.tv = tv;
this.sound = sound;
}
watchMovie() {
console.log("--- 准备看电影 ---");
this.light.off();
this.tv.on();
this.sound.on();
}
endMovie() {
console.log("--- 结束看电影 ---");
this.light.on();
this.tv.off();
this.sound.off();
}
}
// 客户端调用
const light = new Light();
const tv = new TV();
const sound = new SoundSystem();
const homeTheater = new HomeTheaterFacade(light, tv, sound);
homeTheater.watchMovie();
// 输出:
// --- 准备看电影 ---
// 关灯
// 打开电视
// 打开音响
homeTheater.endMovie();
// 输出:
// --- 结束看电影 ---
// 开灯
// 关闭电视
// 关闭音响
桥接模式(Bridge)
桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们可以独立地变化。
前端中的桥接模式场景
- UI组件与渲染引擎分离 :例如,一个通用的图表库,可以将图表的
逻辑(抽象部分)与具体的渲染方式(实现部分,如 Canvas、SVG、WebGL)分离。 - 事件监听 :在绑定事件时,将
回调函数(实现部分)与事件绑定(抽象部分)分离,使得回调函数可以复用。
桥接模式-JS实现
javascript
// 实现部分接口:颜色
class Color {
fill() {
throw new Error("抽象方法不能调用");
}
}
class Red extends Color {
fill() {
return "红色";
}
}
class Green extends Color {
fill() {
return "绿色";
}
}
// 抽象部分:形状
class Shape {
constructor(color) {
this.color = color;
}
draw() {
throw new Error("抽象方法不能调用");
}
}
class Circle extends Shape {
draw() {
console.log(`画一个${this.color.fill()}的圆形`);
}
}
class Square extends Shape {
draw() {
console.log(`画一个${this.color.fill()}的正方形`);
}
}
// 客户端调用
const redCircle = new Circle(new Red());
redCircle.draw(); // 画一个红色的圆形
const greenSquare = new Square(new Green());
greenSquare.draw(); // 画一个绿色的正方形
组合模式(Composite)
组合模式(Composite Pattern)将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
前端中的组合模式场景
- DOM树 :DOM 树本身就是一个典型的组合模式结构,既包含具体的节点(如
div、span),也包含包含其他节点的容器。 - 虚拟DOM :
Virtual DOM也是树形结构,组件可以包含其他组件或原生元素。 - 文件目录系统:文件夹可以包含文件或子文件夹。
- 级联菜单:多级菜单的展示和操作。
组合模式-JS实现
javascript
// 组件基类
class Component {
constructor(name) {
this.name = name;
}
add(component) {
throw new Error("不支持该操作");
}
remove(component) {
throw new Error("不支持该操作");
}
print(indent = "") {
throw new Error("不支持该操作");
}
}
// 叶子节点:文件
class File extends Component {
print(indent = "") {
console.log(`${indent}- ${this.name}`);
}
}
// 组合节点:文件夹
class Folder extends Component {
constructor(name) {
super(name);
this.children = [];
}
add(component) {
this.children.push(component);
}
remove(component) {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
print(indent = "") {
console.log(`${indent}+ ${this.name}`);
this.children.forEach(child => {
child.print(indent + " ");
});
}
}
// 客户端调用
const root = new Folder("根目录");
const folder1 = new Folder("文档");
const folder2 = new Folder("图片");
const file1 = new File("简历.doc");
const file2 = new File("照片.jpg");
const file3 = new File("logo.png");
root.add(folder1);
root.add(folder2);
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
root.print();
// 输出:
// + 根目录
// + 文档
// - 简历.doc
// + 图片
// - 照片.jpg
// - logo.png
享元模式(Flyweight)
享元模式(Flyweight Pattern)通过共享来高效地支持大量细粒度的对象。
前端中的享元模式场景
- 事件委托:在父元素上绑定事件监听器,通过事件冒泡处理子元素的事件,避免为每个子元素绑定监听器,节省内存。
- 对象池:在游戏开发或复杂动画中,预先创建一组对象放入池中,使用时取出,用完归还,避免频繁创建和销毁对象。
- DOM复用:在长列表滚动(虚拟滚动)中,只渲染可视区域的 DOM 节点,回收并复用移出可视区域的节点。
享元模式-JS实现
javascript
// 享元工厂
class ShapeFactory {
constructor() {
this.circleMap = {};
}
getCircle(color) {
if (!this.circleMap[color]) {
this.circleMap[color] = new Circle(color);
console.log(`创建新的 ${color} 圆形`);
}
return this.circleMap[color];
}
}
// 具体享元类
class Circle {
constructor(color) {
this.color = color;
}
draw(x, y) {
console.log(`在 (${x}, ${y}) 画一个 ${this.color} 的圆形`);
}
}
// 客户端调用
const factory = new ShapeFactory();
const redCircle1 = factory.getCircle("红色");
redCircle1.draw(10, 10);
const redCircle2 = factory.getCircle("红色");
redCircle2.draw(20, 20);
const blueCircle = factory.getCircle("蓝色");
blueCircle.draw(30, 30);
console.log(redCircle1 === redCircle2); // true
// 输出:
// 创建新的 红色 圆形
// 在 (10, 10) 画一个 红色 的圆形
// 在 (20, 20) 画一个 红色 的圆形
// 创建新的 蓝色 圆形
// 在 (30, 30) 画一个 蓝色 的圆形
// true