docs: update structual design pattern

This commit is contained in:
mrsingsing
2023-12-21 00:42:10 +08:00
parent 5d9cc2a639
commit 27f329291d
7 changed files with 481 additions and 340 deletions
+114 -24
View File
@@ -6,42 +6,132 @@ group:
title: 结构型
order: 3
title: 适配器模式
order: 2
order: 1
---
# 适配器模式
**适配器模式(Adapter Pattern**将一个接口转换成客户望的另一个接口适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式
适配器模式(Adapter Pattern是一种结构型设计模式,它允许将一个类的接口转换成客户端期望的另一个接口适配器模式主要用于使原本由于接口不匹配而无法一起工作的两个类能够协同工作
### 模式结构
适配器模式涉及以下几个关键角色:
适配器模式包含如下角色:
1. **目标接口(Target Interface**: 客户端所期望的接口,适配器通过实现这个接口,使得客户端可以调用适配器的方法。
2. **适配器(Adapter**: 实现目标接口并包装一个或多个被适配者对象,以便将调用转发给被适配者。
3. **被适配者(Adaptee**: 需要被适配的类或接口,它定义了客户端不知道的方法,适配器通过调用被适配者的方法来完成适配。
4. **客户端(Client**: 使用目标接口的对象,与适配器交互。
- Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承 Target 并关联一个 Adaptee 对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
- Client(客户类)
适配器模式主要有两种形式:类适配器和对象适配器
## 代码实现
## 类适配器
```js
class Adapter {
test() {
return '旧接口';
}
}
在类适配器中,适配器类继承了目标接口,并且同时持有一个被适配者对象。通过继承目标接口,适配器可以将被适配者的方法暴露给客户端。
```typescript
// 目标接口
class Target {
constructor() {
this.adapter = new Adapter();
}
test() {
let info = this.adapter.test();
return `适配${info}`;
request() {}
}
// 被适配者
class Adaptee {
specificRequest() {
console.log("Adaptee's specific request");
}
}
const target = new Target();
// '适配旧借口'
console.log(target.test());
// 类适配器
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
// 客户端
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request(); // 输出:Adaptee's specific request
```
## 对象适配器
在对象适配器中,适配器类持有一个被适配者对象的实例,并实现了目标接口。通过委托,适配器将客户端的请求传递给被适配者。
```typescript
// 对象适配器
class Adapter {
constructor(adaptee) {
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
// 客户端
const adaptee = {
specificRequest: function() {
console.log("Adaptee's specific request");
}
};
const adapter = new Adapter(adaptee);
adapter.request(); // 输出:Adaptee's specific request
```
适配器模式的使用场景包括:
- 当需要使用一个已经存在的类,而它的接口与你所需要的不匹配时。
- 当你想创建一个可复用的类,该类可以与其他不相关的类或不可预见的类(即接口不一致的类)协同工作时。
适配器模式使得不同接口的类能够协同工作,使系统更加灵活,同时能够保持类的独立性和可复用性。
## 代码示例
假设我们有两个不同的日志库,一个是新日志库 NewLogger,一个是旧日志库 OldLogger,它们的接口不同。我们想要在应用程序中统一使用新日志库,但是现有的代码仍然使用旧日志库。这时候就可以使用适配器模式。
```typescript
// 新日志库接口
class NewLogger {
log(message) {
console.log(`New Logger: ${message}`);
}
}
// 旧日志库接口
class OldLogger {
logMessage(msg) {
console.log(`Old Logger: ${msg}`);
}
}
// 适配器 - 将旧日志库适配成新日志库接口
class OldLoggerAdapter extends NewLogger {
constructor(oldLogger) {
super();
this.oldLogger = oldLogger;
}
log(message) {
// 调用旧日志库的方法
this.oldLogger.logMessage(message);
}
}
// 客户端代码,使用新日志库接口
const newLogger = new NewLogger();
newLogger.log("This is a message from the new logger.");
// 适配器让旧日志库适应新日志库接口
const oldLogger = new OldLogger();
const adaptedLogger = new OldLoggerAdapter(oldLogger);
adaptedLogger.log("This is a message from the old logger, adapted to the new logger interface.");
```
在这个例子中,`OldLoggerAdapter` 充当适配器的角色,它继承了新日志库的接口,并持有一个旧日志库的实例。在适配器中,它的 `log` 方法调用了旧日志库的 `logMessage` 方法,从而实现了旧日志库适配到新日志库的接口。客户端代码使用新日志库接口,而适配器在内部负责将旧日志库的调用适配到新日志库的接口。
+92 -22
View File
@@ -6,40 +6,110 @@ group:
title: 结构型
order: 3
title: 桥接模式
order: 5
order: 2
---
# 桥接模式
桥接模式(Bridge Pattern将抽象部分与它的实现部分分离,使它们可以独立变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式
桥接模式(Bridge Pattern是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立变化,而不影响彼此。桥接模式主要用于处理具有多层次继承结构的情况,通过将抽象部分和实现部分分开,使得系统更加灵活
### 模式结构
在桥接模式中,有两个关键的角色:
桥接模式包含如下角色:
1. **抽象类(Abstraction**: 定义了抽象部分的接口,并维护一个指向实现部分的引用。
2. **实现类(Implementor**: 定义了实现部分的接口,被抽象类引用。
3. **具体抽象类(Concrete Abstraction**: 继承自抽象类,实现抽象类定义的接口。
4. **具体实现类(Concrete Implementor**: 继承自实现类,实现实现类定义的接口。
- Abstraction(抽象类):定义抽象接口,拥有一个 Implementor 类型的对象引用
- RefinedAbstraction(扩充抽象类):扩展 Abstraction 中的接口定义
- Implementor(实现类接口):是具体实现的接口,Implementor 和 RefinedAbstraction 接口并不一定完全一致,实际上这两个接口可以完全不一样 Implementor 提供具体操作方法,而 Abstraction 提供更高层次的调用
- ConcreteImplementor(具体实现类):实现 Implementor 接口,给出具体实现
以下是一个通俗易懂的 JavaScript 示例,假设我们要设计一个绘制不同形状的图形的系统:
### 模式分析
```typescript
// 实现类接口
class DrawingAPI {
drawCircle(x, y, radius) {}
drawSquare(x, y, side) {}
}
理解桥接模式,重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。
// 具体实现类 - SVG 绘制
class SVGDrawingAPI extends DrawingAPI {
drawCircle(x, y, radius) {
console.log(`Drawing a circle at (${x},${y}) with radius ${radius} using SVG`);
}
- 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。
- 实现化:针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
- 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。
drawSquare(x, y, side) {
console.log(`Drawing a square at (${x},${y}) with side ${side} using SVG`);
}
}
### 优点和缺点
// 具体实现类 - Canvas 绘制
class CanvasDrawingAPI extends DrawingAPI {
drawCircle(x, y, radius) {
console.log(`Drawing a circle at (${x},${y}) with radius ${radius} using Canvas`);
}
桥接模式的优点:
drawSquare(x, y, side) {
console.log(`Drawing a square at (${x},${y}) with side ${side} using Canvas`);
}
}
- 分离抽象接口及其实现部分。
- 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
// 抽象类
class Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI;
}
桥接模式的缺点:
draw() {}
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
resize() {}
}
// 具体抽象类 - 圆
class Circle extends Shape {
constructor(x, y, radius, drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
draw() {
this.drawingAPI.drawCircle(this.x, this.y, this.radius);
}
resize(radius) {
this.radius = radius;
}
}
// 具体抽象类 - 正方形
class Square extends Shape {
constructor(x, y, side, drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.side = side;
}
draw() {
this.drawingAPI.drawSquare(this.x, this.y, this.side);
}
resize(side) {
this.side = side;
}
}
// 客户端代码
const svgDrawingAPI = new SVGDrawingAPI();
const canvasDrawingAPI = new CanvasDrawingAPI();
const circle = new Circle(1, 2, 3, svgDrawingAPI);
const square = new Square(4, 5, 6, canvasDrawingAPI);
circle.draw();
square.draw();
circle.resize(4);
circle.draw();
```
在这个例子中,`DrawingAPI` 是实现类的接口,`SVGDrawingAPI``CanvasDrawingAPI` 是具体的实现类。`Shape` 是抽象类,`Circle``Square` 是具体的抽象类。通过这种设计,我们可以方便地切换不同的实现类,例如从 `SVG` 切换到 `Canvas` 绘制,而不需要修改 `Circle``Square` 类的代码。这样的设计使得抽象部分和实现部分可以独立演化,提高了系统的灵活性和可维护性。
+76 -8
View File
@@ -6,20 +6,88 @@ group:
title: 结构型
order: 3
title: 组合模式
order: 6
order: 3
---
# 组合模式
**组合模式**Composite)通常用于将对象组合成树形结构以表示部分-整体的层次结构组合模式使得用户对单个对象和组合对象的使用具有一致性。
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构以表示"部分-整体"的层次结构组合模式使得客户端对单个对象和组合对象的使用具有一致性。
**基本结构**
在组合模式中,有两种关键角色:
组合模式主要有三种角色:
1. **组件(Component**: 定义了树形结构中所有具体对象和组合对象的共同接口。
2. **叶子(Leaf**: 实现了组件接口的叶子对象,它是树中的叶子节点,没有子节点。
3.** 合成(Composite)**: 实现了组件接口的组合对象,它具有叶子和其他组合对象作为子节点,可以递归地组合成更复杂的树形结构。
- 抽象组件(Component):抽象类,主要定义了参与组合的对象的公共接口
- 子对象(Leaf):组成组合对象的最基本对象
- 组合对象(Composite):由子对象组合起来的复杂对象
假设我们要构建一个文件系统的树形结构:
理解组合模式的关键是要理解组合模式对单个对象和组合对象使用的一致性。
```typescript
// 组件接口
class FileSystemComponent {
constructor(name) {
this.name = name;
}
// 公共接口方法
display() {}
}
// 叶子对象
class File extends FileSystemComponent {
display() {
console.log(`File: ${this.name}`);
}
}
// 组合对象
class Directory extends FileSystemComponent {
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(`Directory: ${this.name}`);
this.children.forEach(child => {
child.display();
});
}
}
// 客户端代码
const file1 = new File("document.txt");
const file2 = new File("image.jpg");
const directory1 = new Directory("Documents");
directory1.add(file1);
directory1.add(file2);
const file3 = new File("video.mp4");
const directory2 = new Directory("Media");
directory2.add(file3);
const rootDirectory = new Directory("Root");
rootDirectory.add(directory1);
rootDirectory.add(directory2);
// 显示整个文件系统结构
rootDirectory.display();
```
在这个例子中,`FileSystemComponent` 是组件接口,`File` 是叶子对象,`Directory` 是组合对象。`Directory` 可以包含叶子对象(File)和其他组合对象(Directory),从而构建了一个树形结构。客户端代码可以通过调用 `display` 方法遍历整个文件系统结构,而不需要关心是文件还是目录,实现了对整体和部分的一致性访问。
+54 -170
View File
@@ -11,188 +11,72 @@ order: 4
# 装饰者模式
**装饰模式(Decorator Pattern**:动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为 **油漆工模式**,它是一种对象结构型模式。ES7 的装饰器语法以及 React 中的高阶组件(HoC)都是这一模式的实现,`react-redux``connect()` 也运用了装饰器模式
装饰模式(Decorator Pattern是一种结构型设计模式,它允许你通过将对象放入包含行为的特殊包装类中来给对象动态地添加新的行为。装饰模式提供了一种灵活的方式,使得客户端可以单独扩展对象的功能,而不需要修改其结构
## 模式结构
在装饰者模式中,有几个关键角色:
装饰模式包含如下角色:
1. **组件接口(Component**: 定义了具体组件和装饰者共用的接口,确保它们是可互换的。
2. **具体组件(Concrete Component**: 实现了组件接口,是被装饰的原始对象。
3. **装饰者(Decorator**: 实现了组件接口并持有一个具体组件的引用,在其上可以添加额外的行为。
4. **具体装饰者(Concrete Decorator**: 扩展了装饰者接口,实现了具体的装饰行为。
- Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
- ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
- Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
- ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
假设我们有一杯咖啡,我们可以动态地给它添加调料(如牛奶、糖等):
## 模式分析
- 与继承关系相比,关联关系的主要优势在于不会破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,因此使得系统更加容易维护。当然,关联关系的缺点是比继承关系要创建更多的对象。
- 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
## 优点和缺点
装饰模式的优点:
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”
装饰模式的缺点:
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
## 代码实现
AOP 是一种可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
AOP 实际是 GoF 设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP 可以说也是这种目标的一种实现。
AOP (面向切面编程)装饰函数:
```js
Function.prototype.before = function (fn) {
const self = this;
return function () {
fn.apply(new self(), arguments);
return self.apply(new self(), arguments);
};
};
Function.prototype.after = function (fn) {
const self = this;
return function () {
self.apply(new self(), arguments);
return fn.apply(new self(), arguments);
};
};
```
⚠️ **注意**:装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。
## 实践应用
### 表单提交前验证
插件式表单验证,在提交前校验参数:
```js
Function.prototype.before = function (fn) {
const self = this;
return function () {
// before 返回 false 的情况直接 return 不再执行后续的原函数
if (fn.apply(this, arguments) === false) {
return;
}
return self.apply(this, arguments);
};
};
const params = {
username: 'mrsingsing',
password: '',
};
const validate = function () {
if (params.username === '') {
console.log('用户名无效');
return false;
```typescript
// 组件接口
class Coffee {
cost() {
return 5; // 基础咖啡价格
}
if (params.password === '') {
console.log('密码无效');
return false;
}
};
}
let submit = function () {
const params = {
username: 'mrsingsing',
password: '',
};
};
// 具体组件
class SimpleCoffee extends Coffee {}
submit = submit.before(validate);
// 密码无效
```
### 单次访问装饰器
```js
const onceDecorator = function (fn) {
if (typeof fn !== 'function') {
throw new TypeError('`fn` must be a function');
// 装饰者
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this._coffee = coffee;
}
// 函数执行上下文
const context = this;
// Promise 是否被 fulfilled
let isPromiseFulfilled = false;
// 函数是否已被调用过
let isFuncInvoked = false;
cost() {
return this._coffee.cost();
}
}
const invokeFunc = (funcArgs, resolve, reject) => {
fn.call(context, funcArgs).then(
() => {
isPromiseFulfilled = true;
resolve();
},
() => {
isPromiseFulfilled = false;
isFuncInvoked = false;
reject();
}
);
};
// 具体装饰者 - 牛奶
class MilkDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
return function (...args) {
return new Promise((resolve, reject) => {
if (!isPromiseFulfilled && !isFuncInvoked) {
invokeFunc(args, resolve, reject);
isFuncInvoked = true;
}
});
};
};
cost() {
return super.cost() + 2; // 加牛奶的价格
}
}
export default onceDecorator;
// 具体装饰者 - 糖
class SugarDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
cost() {
return super.cost() + 1; // 加糖的价格
}
}
// 客户端代码
const simpleCoffee = new SimpleCoffee();
console.log(`Cost of Simple Coffee: $${simpleCoffee.cost()}`);
const milkCoffee = new MilkDecorator(simpleCoffee);
console.log(`Cost of Coffee with Milk: $${milkCoffee.cost()}`);
const sugarMilkCoffee = new SugarDecorator(milkCoffee);
console.log(`Cost of Coffee with Milk and Sugar: $${sugarMilkCoffee.cost()}`);
```
因为装饰的可能是函数,也可能是对象的方法,所以提供了两个工具函数 `decoratorFunction``decoratorMethod`,具体实现如下:
在这个例子中,`Coffee` 是组件接口,`SimpleCoffee` 是具体组件。`CoffeeDecorator` 是装饰者,`MilkDecorator``SugarDecorator` 是具体装饰者。通过装饰者模式,我们可以动态地将调料添加到咖啡中,而不需要修改咖啡类本身。客户端可以根据需要组合不同的装饰者,实现了灵活的行为扩展。
```js
/**
* 装饰函数
* @param {*} func 被装饰的函数
* @param {*} decorator 装饰器
*/
const decorateFunction = (func, decorator) => {
return decorator(func);
};
/**
* 装饰方法
* @param {*} func 被装饰的方法
* @param {*} decorator 装饰器
* @param {*} context 上下文
*/
const decorateMethod = (func, decorator, context) => {
return decorator.bind(context)(func);
};
```
实际应用场景:
- Redux 中间件
- Vuex 中间件
- Express 和 Koa 洋葱模型(中间件)
---
**参考资料:**
- [我是如何在项目中使用装饰器模式的](https://www.jianshu.com/p/d9085f84a1c0)
+57 -2
View File
@@ -6,9 +6,64 @@ group:
title: 结构型
order: 3
title: 外观模式
order: 1
order: 5
---
# 外观模式
**外观模式**Facade)通常用于隐藏底层复杂系统,降低子系统的复杂度,为调用者提供更高级的统一接口,提高应用易用性。
外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个统一的接口,用于访问子系统中的一群接口。外观模式定义了一个高层接口,使得子系统更容易使用。
外观模式的主要目的是简化客户端与复杂系统之间的交互,通过引入一个外观类,隐藏了系统的复杂性,使得客户端只需与外观类交互,而无需直接与子系统中的各个组件打交道。这有助于减少客户端与子系统之间的依赖关系,提高了系统的灵活性和可维护性。
外观模式涉及到以下几个角色:
1. **外观(Facade**: 提供了一个高层次的接口,该接口使用了子系统中的一群接口,使得子系统更容易使用。
2. **子系统(Subsystems**: 由多个类组成,实现了系统的各个功能。外观将客户端请求代理给适当的子系统对象。
3. **客户端(Client**: 通过外观接口与子系统交互,而无需直接与子系统的具体实现打交道。
假设有一个音响系统,包含了开启音响、播放音乐和关闭音响等多个步骤,我们可以使用外观模式来简化这个过程:
```typescript
// 子系统 - 音响
class Stereo {
turnOn() {
console.log("Stereo is ON");
}
playMusic() {
console.log("Music is playing");
}
turnOff() {
console.log("Stereo is OFF");
}
}
// 外观
class AudioFacade {
constructor(stereo) {
this.stereo = stereo;
}
playMusic() {
this.stereo.turnOn();
this.stereo.playMusic();
}
turnOffMusic() {
this.stereo.turnOff();
}
}
// 客户端
const stereo = new Stereo();
const audioFacade = new AudioFacade(stereo);
// 使用外观接口
audioFacade.playMusic();
// 关闭音响
audioFacade.turnOffMusic();
```
在这个示例中,`Stereo` 类是子系统的一部分,包含了音响的各种操作。`AudioFacade` 类是外观类,它提供了一个简化的接口,包含了开启音响和播放音乐的步骤。客户端通过与 `AudioFacade` 类交互,无需直接调用 `Stereo` 的具体方法,从而实现了对子系统的简化访问。
+42 -21
View File
@@ -6,41 +6,62 @@ group:
title: 结构型
order: 3
title: 享元模式
order: 7
order: 6
---
# 享元模式
**享元模式(Flyweight Pattern**:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。享元模式结构较为复杂,一般结合工厂模式一起使用
享元模式(Flyweight Pattern是一种结构型设计模式,旨在通过共享对象来减少内存和计算开销。该模式适用于系统中存在大量相似对象,它通过共享这些对象的相同部分,减少了实例的数量,从而降低了内存占用和提高了性能
### 模式结构
在享元模式中,存在两种类型的状态:
享元模式包含如下角色:
1. **内部状态(Intrinsic State**: 内部状态是对象可共享的部分,它存储在享元对象内部并且不会随环境的改变而改变。对于所有共享对象,内部状态是一致的。
2. **外部状态(Extrinsic State**: 外部状态是对象依赖的、随环境改变而改变的部分。它不可共享,需要在使用享元对象时由客户端传递给享元对象。
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂一共一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
享元模式的关键是将对象的状态分为内部状态和外部状态,并尽可能共享内部状态。
### 模式分析
假设有一个图书馆,包含了大量的书籍,我们可以使用享元模式来共享相同的书籍标题:
享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
```typescript
// 享元工厂
class BookFactory {
constructor() {
this.books = {};
}
享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
getBook(title) {
if (!this.books[title]) {
this.books[title] = new Book(title);
}
return this.books[title];
}
享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
getTotalBooks() {
return Object.keys(this.books).length;
}
}
- 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
- 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。
// 享元对象
class Book {
constructor(title) {
this.title = title;
}
### 优点和缺点
display(author) {
console.log(`Book: ${this.title}, Author: ${author}`);
}
}
享元模式的优点
// 客户端
const factory = new BookFactory();
- 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
const book1 = factory.getBook("Design Patterns");
book1.display("Erich Gamma");
享元模式的缺点
const book2 = factory.getBook("Design Patterns");
book2.display("Richard Helm");
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
console.log(`Total Books in Library: ${factory.getTotalBooks()}`);
```
在这个示例中,`BookFactory` 是享元工厂,负责创建和管理共享的 `Book` 对象。`Book` 类是享元对象,表示图书的内部状态。客户端通过 `BookFactory` 获取书籍对象,如果书籍对象已存在,则直接返回现有对象,否则创建新的对象。通过这种方式,相同标题的书籍被共享,减少了对象的数量,从而提高了系统的性能和效率。
+46 -93
View File
@@ -6,114 +6,67 @@ group:
title: 结构型
order: 3
title: 代理模式
order: 3
order: 7
---
# 代理模式
**代理模式(Proxy Pattern** 是指为一个原对象找一个代理对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,通过代理做授权和控制。代理模式的英文叫做 Proxy 或 Surrogate,它是一种对象结构型模式
代理模式(Proxy Pattern是一种结构型设计模式,其主要目的是通过引入一个代理对象来控制对另一个对象的访问。代理对象充当了客户端和目标对象之间的中介,可以用于实现各种用途,如延迟加载、权限控制、日志记录等
最常见的例子就是经纪人代理明星业务,假设你作为投资人,想联系明星打广告,那么你就需要先经过代理经纪人,经纪人对你的资质进行考察,并为你进行排期,替明星过滤不必要的信息。
代理模式涉及到以下几个角色:
事件委托/代理、jQuery 的 `$.proxy`、ES6 的 `proxy` 都是这一模式的实现
1. `抽象主题(Subject`: 定义了目标对象和代理对象的共同接口,客户端通过该接口访问目标对象
2. `具体主题(Real Subject`: 实现了抽象主题接口,是真正的目标对象,代理对象直接控制对它的访问。
3. `代理(Proxy`: 实现了抽象主题接口,保存了对真正目标对象的引用,客户端通过代理访问目标对象。代理对象可以在客户端访问目标对象之前或之后执行一些额外的操作。
代理模式又分为 **静态代理****动态代理**
代理模式可以分为多种类型,包括
- **静态代理** 是由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的 `.class` 文件就已经存在了
- **动态代理** 是在程序运行时,通过运用反射机制动态的创建而成。
静态代理(Static Proxy): 代理对象在编译时就已经确定,代理类和目标类的关系在编译阶段就确定了。静态代理需要为每一个目标类创建一个代理类,导致类的数量增加
## 模式结构
动态代理(Dynamic Proxy): 代理对象在运行时动态生成,代理类不需要预先定义,而是在运行时根据需要创建。Java 中的 java.lang.reflect.Proxy 和 InvocationHandler 接口就是动态代理的经典实现。
代理模式包含如下角色
下面是一个简单的静态代理的示例,假设有一个简单的接口 Subject,以及一个实现了该接口的类 RealSubject,我们可以使用代理模式创建一个代理类 Proxy
- Subject(抽象主题角色):声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
- Proxy(代理主题角色):也称为委托角色或者被代理角色。定义了代理对象所代表的目标对象。
- RealSubject(真实主题角色):也叫委托类、代理类。代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。
```typescript
// 抽象主题
interface Subject {
void request();
}
## 优点和缺点
// 具体主题
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
代理模式的优点
// 代理
class Proxy implements Subject {
private RealSubject realSubject;
- 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
- 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
- 保护代理可以控制对真实对象的使用权限。
代理模式的缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
## 实践应用
### 图片预加载
虚拟代理:作为创建开销大的对象的代表;虚拟代理经常直到我们真正需要一个对象的时候才创建它;当对象在创建或创建中时,由虚拟代理来扮演对象的替身;对象创建后,代理就会将请求直接委托给对象。
```js
const image = (function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
},
};
})();
// 代理容器
const proxyImage = (function () {
let img = new Image();
// 加载完之后将设置为添加的图片
img.onload = function () {
image.setSrc(this.src);
};
return {
setSrc: function (src) {
image.setSrc('loading.gif');
img.src = src;
},
};
})();
proxyImage.setSrc('file.jpg');
```
代理容器控制了客户对 Image 的访问,并且在过程中加了一些额外的操作。
### 计算乘积
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前端存储的结果。
```js
// 求乘积函数(专注于自身职责,计算成绩,缓存由代理实现)
const mult = function () {
let result = 1;
for (let i = 0, l = arguments.length; i < l; i++) {
result = result * arguments[i];
}
return result;
};
// proxyMult
const proxyMult = (function () {
let cache = {};
return function () {
let args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
return (cache[arg] = mult.apply(this, arguments));
};
})();
@Override
public void request() {
System.out.println("Proxy: Logging before request.");
realSubject.request();
System.out.println("Proxy: Logging after request.");
}
}
proxyMult(1, 2, 3); // 6
proxyMult(1, 2, 3); // 6
// 客户端
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Proxy proxy = new Proxy(realSubject);
// 通过代理访问目标对象
proxy.request();
}
}
```
在这个示例中,`Subject` 是抽象主题,`RealSubject` 是具体主题。`Proxy` 是代理类,它持有一个 `RealSubject` 的引用,并在调用 `request` 方法前后添加了额外的日志记录。客户端通过代理对象访问目标对象,实现了对目标对象的访问控制。