从前端视角看设计模式之结构型模式篇
上篇我们介绍了 设计模式之创建型模式篇,接下来介绍设计模式之结构型模式篇
适配器模式
适配器模式旨在解决接口不兼容的问题,它通过创建一个适配器类,将源对象的接口转换成目标接口,从而使得不兼容的接口能够协同工作。简单来说,适配器模式的目标就是将一个接口转化成客户端所期望的接口,通过这种方式,现有的类(或者第三方类)可以在不修改的情况下被重用
它的使用场景主要有以下几个:
1)需要使用现有类,但其接口不符合系统需求
2)希望创建一个可复用的类,与多个不相关的类一起工作,这些类可能没有统一的接口
3)通过接口转换,将一个类集成到另一个类系中
适配器模式包含以下几个主要角色:
1)目标接口
定义客户需要的接口
2)适配者类
定义一个已经存在的接口,这个接口需要适配
3)适配器类
实现目标接口,并通过组合或继承的方式调用适配者类中的方法,从而实现目标接口
通过以下这个例子来理解适配器模式,假设有一个支付系统,系统中既有旧的支付接口OldPaymentSystem
,也有新的支付接口NewPaymentSystem
,我们希望使用一个统一的接口来调用这两个不同的支付系统
1)定义目标接口
目标接口是客户端期望使用的接口,所有支付系统都应当提供一个pay()
方法
// 目标接口(Target)
class PaymentGateway {
pay(amount) {
throw new Error("该方法需要被重写")
}
}
2)定义源接口
源接口是现有的不兼容接口,假设现有的支付系统使用的是OldPaymentSystem
,它提供了一个makePayment()
方法,而不是pay()
// 源接口:旧支付系统(Adaptee)
class OldPaymentSystem {
makePayment(amount) {
console.log(`使用旧支付系统支付:${amount} 元`)
}
}
3)创建适配器
适配器的作用是将OldPaymentSystem
的接口适配到PaymentGateway
接口,使得它们能够通过相同的接口来使用
// 适配器:将旧支付系统适配为目标接口
class OldPaymentAdapter extends PaymentGateway {
constructor(oldPaymentSystem) {
super()
this.oldPaymentSystem = oldPaymentSystem
}
// 实现目标接口的 pay 方法
pay(amount) {
this.oldPaymentSystem.makePayment(amount) // 调用旧支付系统的方法
}
}
4)创建新支付系统
新支付系统与目标接口兼容,直接实现pay()
方法
// 新支付系统(NewPaymentSystem)
class NewPaymentSystem extends PaymentGateway {
pay(amount) {
console.log(`使用新支付系统支付:${amount} 元`)
}
}
5)使用适配器模式
客户端只依赖于PaymentGateway
接口来进行支付,而不关心支付系统的实现,无论使用的是旧的支付系统还是新的支付系统,客户端代码都会通过统一的接口pay()
来进行支付
// 客户端代码:使用支付网关接口
function processPayment(paymentSystem, amount) {
paymentSystem.pay(amount) // 使用支付系统进行支付
}
// 客户端代码使用旧支付系统
const oldPaymentSystem = new OldPaymentSystem()
const adaptedOldPaymentSystem = new OldPaymentAdapter(oldPaymentSystem) // 使用适配器
processPayment(adaptedOldPaymentSystem, 100)
// 客户端代码使用新支付系统
const newPaymentSystem = new NewPaymentSystem()
processPayment(newPaymentSystem, 200)
执行代码,运行结果如下:
桥接模式
桥接模式通过将一个对象的抽象部分与它的实现部分分离,使它们可以独立变化,通过组合的方式,而不是继承的方式,将抽象和实现的部分连接起来
它的使用场景主要有以下:
1)当需要抽象和实现分离,且两者独立变化时
2)当系统不希望固定抽象类和实现类的绑定时
3)当多个类的不同实现有类似的功能时
桥接模式包含以下几个主要角色:
1)抽象类
定义抽象接口,通常包含对实现接口的引用
2)扩展抽象类
对抽象的扩展,可以是抽象的子类或具体实现类
3)实现类
定义具体实现类必须遵循的接口,通常包含一些基本操作
4)具体实现
实现实现接口的具体类
通过以下绘制图形的例子来展示桥接模式,图形有不同的形状(如圆形和矩形),而每个图形可以有不同的绘制方式
1)定义实现接口
定义一个接口DrawingAPI
,它包含绘制图形的基本方法
// 实现类接口:绘制API
class DrawingAPI {
drawCircle(radius, x, y) {
throw new Error("该方法需要被重写")
}
drawRectangle(width, height, x, y) {
throw new Error("该方法需要被重写")
}
}
2)创建具体实现类
为DrawingAPI
接口创建几个具体实现类,例如,一个实现类使用Canvas
来绘制图形,另一个实现类使用SVG
来绘制
// 具体实现类:使用 Canvas 绘制图形
class CanvasDrawingAPI extends DrawingAPI {
drawCircle(radius, x, y) {
console.log(`在 Canvas 上绘制圆形,半径:${radius}, 位置:(${x}, ${y})`)
}
drawRectangle(width, height, x, y) {
console.log(`在 Canvas 上绘制矩形,宽度:${width}, 高度:${height}, 位置:(${x}, ${y})`)
}
}
// 具体实现类:使用 SVG 绘制图形
class SVGDrawingAPI extends DrawingAPI {
drawCircle(radius, x, y) {
console.log(`在 SVG 上绘制圆形,半径:${radius}, 位置:(${x}, ${y})`)
}
drawRectangle(width, height, x, y) {
console.log(`在 SVG 上绘制矩形,宽度:${width}, 高度:${height}, 位置:(${x}, ${y})`)
}
}
3)定义抽象类
定义一个抽象类Shape
,它包含一个DrawingAPI
的引用,并通过该引用调用具体实现类的方法
// 抽象类:形状
class Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI
}
draw() {
throw new Error("该方法需要被重写")
}
resize() {
throw new Error("该方法需要被重写")
}
}
4)扩展抽象类
创建两个扩展抽象类Circle
和Rectangle
,它们具体化了Shape
类的draw
和resize
方法
// 扩展抽象类:圆形
class Circle extends Shape {
constructor(drawingAPI, radius, x, y) {
super(drawingAPI)
this.radius = radius
this.x = x
this.y = y
}
draw() {
this.drawingAPI.drawCircle(this.radius, this.x, this.y) // 调用实现类的方法
}
resize(newRadius) {
this.radius = newRadius
}
}
// 扩展抽象类:矩形
class Rectangle extends Shape {
constructor(drawingAPI, width, height, x, y) {
super(drawingAPI)
this.width = width
this.height = height
this.x = x
this.y = y
}
draw() {
this.drawingAPI.drawRectangle(this.width, this.height, this.x, this.y) // 调用实现类的方法
}
resize(newWidth, newHeight) {
this.width = newWidth
this.height = newHeight
}
}
5)客户端
客户端只关心如何使用Shape
类和不同的实现类来绘制图形,而无需关心具体的绘制方法
// 客户端代码:创建形状对象并绘制
const canvasAPI = new CanvasDrawingAPI()
const svgAPI = new SVGDrawingAPI()
const circle = new Circle(canvasAPI, 5, 10, 20)
circle.draw()
const rectangle = new Rectangle(svgAPI, 4, 6, 30, 40)
rectangle.draw()
// 还可以通过切换 API 来动态改变绘制方式
circle.drawingAPI = svgAPI
circle.draw()
执行代码,运行结果如下:
过滤器模式
过滤器模式允许我们使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式将它们连接起来。换句话说,该模式提供了一个可以复用的过滤框架,用于过滤掉不符合条件的对象
它的使用场景主要有以下:
1)当对象集合需要根据不同的标准进行筛选时
2)需要进行动态组合条件的筛选时
过滤器模式包括以下几个主要角色:
1)过滤器接口
定义一个标准的过滤方法,通常是一个filter
方法
2)具体过滤器
实现filter
接口,提供实际的过滤逻辑
3)标准
用于组合多个过滤器,通过链式调用的方式来组合过滤条件
4)对象类
需要被过滤的对象类
5)过滤器管理器
负责管理多个过滤器,并协调它们的工作
通过以下例子来理解过滤器模式,我们将构建一个基于人员的过滤系统,可以根据不同的条件来筛选人员,例如:年龄、性别
1)定义Person
类
// 定义 Person 类
class Person {
constructor(name, gender, age) {
this.name = name
this.gender = gender
this.age = age
}
}
2)定义Filter
接口
定义一个通用的Filter
接口,要求所有过滤器都必须实现filter
方法
// 过滤器接口
class Filter {
filter(persons) {
throw new Error("该方法需要被重写")
}
}
3)创建具体的过滤器
创建几个具体的过滤器:按性别和按年龄进行过滤
// 过滤器:按性别过滤
class GenderFilter extends Filter {
constructor(gender) {
super()
this.gender = gender
}
filter(persons) {
return persons.filter(person => person.gender === this.gender)
}
}
// 过滤器:按年龄过滤
class AgeFilter extends Filter {
constructor(age) {
super()
this.age = age
}
filter(persons) {
return persons.filter(person => person.age === this.age)
}
}
4)创建CriteriaManager
类
CriteriaManager
类负责组合多个过滤器,并使用这些过滤器来筛选人员
// 过滤器管理器
class CriteriaManager {
constructor() {
this.filters = []
}
addFilter(filter) {
this.filters.push(filter)
}
applyFilters(persons) {
let result = persons
this.filters.forEach(filter => {
result = filter.filter(result)
})
return result
}
}
5)使用过滤器
假设有一组人员数据,我们可以使用GenderFilter
和AgeFilter
来筛选符合条件的人员
// 创建人员数据
const persons = [
new Person("Alice", "Female", 25),
new Person("Bob", "Male", 30),
new Person("Charlie", "Male", 35),
new Person("Diana", "Female", 25),
new Person("Eve", "Female", 30)
]
// 创建过滤器
const genderFilter = new GenderFilter("Female")
const ageFilter = new AgeFilter(25)
// 创建过滤器管理器并添加过滤器
const criteriaManager = new CriteriaManager()
criteriaManager.addFilter(genderFilter)
criteriaManager.addFilter(ageFilter)
// 应用过滤器
const result = criteriaManager.applyFilters(persons)
// 输出过滤结果
console.log("过滤后的人员:")
result.forEach(person => {
console.log(`姓名:${person.name}, 性别:${person.gender}, 年龄:${person.age}`)
})
执行代码,筛选出年龄25的女性人员,运行结果如下:
组合模式
组合模式允许我们将对象组合成树形结构来表示"部分-整体"的层次结构,使得客户端对单个对象和组合对象的使用具有一致性,即使是复杂的对象集合,客户端也能够统一处理
它的使用场景主要有以下:
1)表示树形结构的场景:如文件系统中的文件和文件夹、组织结构、菜单结构等
2)需要在单个对象和组合对象之间使用相同操作时
3)树形结构中的元素具有相似的操作
它的优缺点:
1)优点
- 客户端可以统一处理单个对象和组合对象
- 可以动态增加、删除节点,灵活性较高
2)缺点
- 可能会导致设计中的过度泛化,树形结构的组合可能会引入不必要的复杂性
组合模式包含以下几个主要角色:
1)组件
定义一个接口,所有的叶子节点和复合节点(组合对象)都实现这个接口
2)叶子节点
表示树结构的叶子节点,通常用于表示"最基本"的对象
3)复合节点
它包含一个集合,用于存储子组件,并实现了组件接口,可以对其子组件进行操作
通过以下这个简单的文件系统来示范组合模式,其中File
代表文件(叶子节点),Folder
代表文件夹(复合节点,包含子文件和子文件夹)
1)定义组件接口
定义一个统一的接口,它包含一些对所有节点共有的操作,比如display()
// 组件类
class Component {
constructor(name) {
this.name = name
}
display() {
throw new Error("此方法必须被重写")
}
}
2)创建File
类(叶子节点)
File
代表文件,文件没有子节点,因此它只需要实现display
方法
// 叶子节点:文件
class File extends Component {
constructor(name) {
super(name)
}
display() {
console.log(`文件:${this.name}`)
}
}
3)创建Folder
类(复合节点)
Folder
代表文件夹,文件夹可以包含多个文件和其他文件夹,因此它实现了对其子组件的管理功能,如 add()
, remove()
和display()
// 复合节点:文件夹
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)
}
}
display() {
console.log(`文件夹:${this.name}`)
// 递归显示文件夹中的子节点
this.children.forEach(child => {
child.display()
})
}
}
4)创建文件系统并测试
创建一个文件夹,并向其中添加文件和其他文件夹,测试组合模式的效果
// 测试组合模式
const file1 = new File("文件1.txt")
const file2 = new File("文件2.txt")
const file3 = new File("文件3.txt")
const folder1 = new Folder("文件夹1")
const folder2 = new Folder("文件夹2")
// 向文件夹1 中添加文件
folder1.add(file1)
folder1.add(file2)
// 向文件夹2 中添加文件夹
folder2.add(file3)
folder2.add(folder1)
// 最终目录结构
folder2.display()
执行代码,运行结果如下:
装饰器模式
装饰器模式允许在不改变对象的基础上动态地给对象添加新的行为和功能,它提供了比继承更灵活的功能扩展方式
它的使用场景主要有以下:
1)动态添加或移除对象的功能,而不影响其他对象
2)功能扩展时避免使用类继承
3)当不能修改类源代码或希望通过组合方式实现灵活扩展时
装饰器模式包含以下几个主要角色:
1)组件
定义一个基本接口或抽象类,可以动态添加行为或功能
2)具体组件
实现了组件接口的类,可以被装饰
3)装饰器
实现了组件接口,并包含一个指向组件的引用,它可以在调用实际组件的行为之前或之后,加入额外的功能
4)具体装饰器
具体的装饰器类,用于向组件添加具体的功能或行为
通过以下这个咖啡订单系统来理解装饰器模式,基础咖啡类有不同类型的咖啡,而装饰器用于动态地给咖啡添加配料,如牛奶、糖、巧克力等,用户可以根据需求动态组合出多种咖啡订单
1)定义组件接口
创建一个Coffee
基础接口,定义获取描述和价格的方法
// 基础接口:Coffee
class Coffee {
getDescription() {
return "未知的咖啡"
}
getCost() {
return 0
}
}
2)创建具体组件
创建具体组件SimpleCoffee
,实现Coffee
接口,表示一种基础的咖啡
// 具体组件:基础咖啡
class SimpleCoffee extends Coffee {
getDescription() {
return "普通咖啡"
}
getCost() {
return 10 // 基础价格
}
}
3)创建装饰器基类
装饰器基类实现Coffee
接口,同时持有一个Coffee
对象的引用
// 装饰器基类
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super()
this.decoratedCoffee = coffee
}
getDescription() {
return this.decoratedCoffee.getDescription()
}
getCost() {
return this.decoratedCoffee.getCost()
}
}
4)创建具体装饰器
具体装饰器继承自CoffeeDecorator
,用于动态地给咖啡添加配料,如牛奶、糖等
// 具体装饰器:牛奶
class MilkDecorator extends CoffeeDecorator {
getDescription() {
return `${this.decoratedCoffee.getDescription()},加牛奶`
}
getCost() {
return this.decoratedCoffee.getCost() + 2 // 牛奶的额外价格
}
}
// 具体装饰器:糖
class SugarDecorator extends CoffeeDecorator {
getDescription() {
return `${this.decoratedCoffee.getDescription()},加糖`
}
getCost() {
return this.decoratedCoffee.getCost() + 1 // 糖的额外价格
}
}
// 具体装饰器:巧克力
class ChocolateDecorator extends CoffeeDecorator {
getDescription() {
return `${this.decoratedCoffee.getDescription()},加巧克力`
}
getCost() {
return this.decoratedCoffee.getCost() + 3 // 巧克力的额外价格
}
}
5)测试组合装饰器
创建基础咖啡,并使用不同的装饰器动态添加功能
// 测试装饰器模式
let myCoffee = new SimpleCoffee()
console.log(`描述: ${myCoffee.getDescription()},价格: ${myCoffee.getCost()} 元`)
milkCoffee = new MilkDecorator(myCoffee)
console.log(`描述: ${milkCoffee.getDescription()},价格: ${milkCoffee.getCost()} 元`)
sugarCoffee = new SugarDecorator(myCoffee)
console.log(`描述: ${sugarCoffee.getDescription()},价格: ${sugarCoffee.getCost()} 元`)
chocolateCoffee = new ChocolateDecorator(myCoffee)
console.log(`描述: ${chocolateCoffee.getDescription()},价格: ${chocolateCoffee.getCost()} 元`)
执行代码,运行结果如下:
外观模式
外观模式定义了一个高层接口,用来访问子系统中的一群接口,让子系统更容易使用,从而降低系统的复杂性
它的使用场景主要有以下:
1)当客户端不需要了解系统内部的复杂逻辑和组件交互时
2)当需要为整个系统定义一个清晰的入口点时
外观模式涉及以下核心角色:
1)外观
提供一个统一的接口,调用子系统的功能
2)子系统
一组实现具体功能的类,这些类可以被单独调用
3)客户端
调用外观类,避免直接与复杂子系统交互
通过以下这个智能家具控制系统来理解外观模式,这个系统包括电视、音响和灯光等子系统,用户只需通过统一的SmartHomeController
接口来管理这些设备
1)创建子系统类
定义电视、音响和灯光等子系统,提供各自的功能
// 子系统1:电视
class TV {
turnOn() {
console.log("电视已打开")
}
turnOff() {
console.log("电视已关闭")
}
}
// 子系统2:音响
class SoundSystem {
turnOn() {
console.log("音响已打开")
}
turnOff() {
console.log("音响已关闭")
}
setVolume(level) {
console.log(`音量已设置为 ${level}`)
}
}
// 子系统3:灯光
class Lights {
turnOn() {
console.log("灯光已打开")
}
turnOff() {
console.log("灯光已关闭")
}
dim(level) {
console.log(`灯光已调暗至 ${level}%`)
}
}
2)创建外观类
外观类SmartHomeController
提供统一的接口,让客户端使用起来更简单
// 外观类:SmartHomeController
class SmartHomeController {
constructor() {
this.tv = new TV()
this.soundSystem = new SoundSystem()
this.lights = new Lights()
}
startMovieMode() {
console.log("启动电影模式...")
this.tv.turnOn()
this.soundSystem.turnOn()
this.soundSystem.setVolume(15)
this.lights.dim(20)
}
endMovieMode() {
console.log("结束电影模式...")
this.tv.turnOff()
this.soundSystem.turnOff()
this.lights.turnOff()
}
}
3)客户端调用
客户端通过SmartHomeController
的简单接口管理设备,而无需关心子系统的复杂实现
// 测试外观模式
const controller = new SmartHomeController()
// 启动电影模式
controller.startMovieMode()
// 结束电影模式
controller.endMovieMode()
执行代码,输出结果如下:
享元模式
享元模式通过将共享部分提取并重用,避免重复创建和消耗内存
它的使用场景主要有以下:
1)当系统中存在大量相似或相同的对象
2)对象的创建和销毁成本较高
3)对象的状态可以外部化,即对象的部分状态可以独立于对象本身存在
它的优缺点:
1)优点
- 减少系统内存使用
- 提高对象创建效率
2)缺点
- 增加实现复杂度,需管理共享和非共享对象
- 需要区分内部状态与外部状态
享元模式包含以下几个核心角色:
1)享元工厂
负责创建和管理享元对象
2)抽象享元
定义了具体享元和非共享享元的接口,通常包含了设置外部状态的方法
3)具体享元
实现了抽象享元接口,包含了内部状态和外部状态,内部状态是可以被共享的,而外部状态则由客户端传递
4)客户端
使用享元工厂获取享元对象,并通过设置外部状态来操作享元对象
通过以下这个文本编辑器来理解享元模式,文字需要不同的字体、大小和颜色,但很多文字共享相同的字体设置,通过享元模式可以避免为每个字符创建重复对象
1)创建享元类
文字包含字体、大小、颜色
class TextStyle {
constructor(font, size, color) {
this.font = font
this.size = size
this.color = color
}
getDetails() {
return `字体: ${this.font}, 大小: ${this.size}, 颜色: ${this.color}`
}
}
2)创建享元工厂
class TextStyleFactory {
constructor() {
this.styles = {} // 存储共享的字体样式
}
getStyle(font, size, color) {
const key = `${font}-${size}-${color}`
if (!this.styles[key]) {
console.log(`创建新样式: ${key}`)
this.styles[key] = new TextStyle(font, size, color)
} else {
console.log(`复用已有样式: ${key}`)
}
return this.styles[key]
}
}
3)客户端使用
客户端不需要关心享元对象的具体实现
class TextCharacter {
constructor(char, textStyle) {
this.char = char
this.textStyle = textStyle // 字体样式(共享部分)
}
display() {
console.log(`字符: ${this.char}, 样式: ${this.textStyle.getDetails()}`)
}
}
4)测试
// 初始化享元工厂
const styleFactory = new TextStyleFactory()
// 获取共享样式
const style1 = styleFactory.getStyle("Arial", 12, "red")
const style2 = styleFactory.getStyle("Arial", 12, "red")
const style3 = styleFactory.getStyle("Times New Roman", 14, "blue")
// 创建字符对象
const charA = new TextCharacter("A", style1)
const charB = new TextCharacter("B", style2)
const charC = new TextCharacter("C", style3)
// 显示字符信息
charA.display()
charB.display()
charC.display()
运行代码,执行结果如下:
代理模式
代理模式通过引入一个代理对象来控制对原对象的访问,代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理
它的使用场景主要有以下:
1)远程代理
代理对象在本地,实际对象在远程服务器上
2)虚拟代理
延迟创建代价昂贵的目标对象
3)保护代理
控制目标对象的访问权限
4)智能引用
记录目标对象的使用情况或进行性能统计
PS:
- 与适配器模式的区别:适配器模式改变接口,而代理模式不改变接口
- 与装饰器模式的区别:装饰器模式用于增强功能,而代理模式用于控制访问
代理模式主要涉及以下几个核心角色:
1)抽象主题
定义了真实主题和代理主题的共同接口,这样在任何使用真实主题的地方都可以使用代理主题
2)真实主题
实现了抽象主题接口,是代理对象所代表的真实对象,客户端直接访问真实主题,但在某些情况下,可以通过代理主题来间接访问
3)代理
实现了抽象主题接口,并持有对真实主题的引用,代理主题通常在真实主题的基础上提供一些额外的功能
4)客户端
使用抽象主题接口来操作真实主题或代理主题,不需要知道具体是哪一个实现类
通过以下 API 请求代理来理解代理模式,代理对象负责添加日志、权限控制和缓存,而目标对象专注于发送实际的网络请求
1)定义目标对象
定义核心逻辑,处理实际 API 请求
class ApiService {
fetchData(endpoint) {
console.log(`Fetching data from API: ${endpoint}`)
return { data: `Response from ${endpoint}` }
}
}
2)创建代理对象
包装目标对象,添加日志、缓存等功能
class ApiProxy {
constructor() {
this.apiService = new ApiService()
this.cache = {}
}
fetchData(endpoint) {
// 缓存检查
if (this.cache[endpoint]) {
console.log(`从缓存获取数据: ${endpoint}`)
return this.cache[endpoint]
}
// 权限检查
if (!this.hasAccess(endpoint)) {
console.log(`权限不足,无法访问: ${endpoint}`)
return { error: "Access denied" }
}
// 实际调用目标对象
console.log(`代理转发请求: ${endpoint}`)
const response = this.apiService.fetchData(endpoint)
// 缓存结果
this.cache[endpoint] = response
return response
}
hasAccess(endpoint) {
// 简单的权限检查逻辑
return endpoint.startsWith("/public")
}
}
3)客户端使用
通过代理对象访问目标对象
// 客户端代码
const proxy = new ApiProxy()
// 第一次请求,直接调用 API
console.log(proxy.fetchData("/public/data")) // 调用目标对象
console.log(proxy.fetchData("/private/data")) // 权限不足
// 第二次请求,相同 endpoint,从缓存获取
console.log(proxy.fetchData("/public/data")) // 从缓存返回
执行代码,运行结果如下:
原文地址:https://blog.csdn.net/triggerV/article/details/145263478
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!