47:JS常见设计模式有哪些并说明
答:
常见的JS设计模式有以下几种:
- 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供全局访问点。
- 工厂模式(Factory Pattern):定义一个用于创建对象的接口,让子类决定实例化哪一个类。
- 观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
- 装饰者模式(Decorator Pattern):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰者模式比生成子类更为灵活。
- 适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- 策略模式(Strategy Pattern):定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
- 命令模式(Command Pattern):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
- 模板方法模式(Template Method Pattern):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,
- 发布-订阅模式(Publish-Subscribe Pattern):也称为观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当主题对象状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
- 职责链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
- 外观模式(Facade Pattern):为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 中介者模式(Mediator Pattern):用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 组合模式(Composite Pattern):将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
- 享元模式(Flyweight Pattern):运用共享技术来有效地支持大量细粒度对象的复用。通过共享相同的状态,可以在有限的内存容量下支持大量的对象。
- 解释器模式(Interpreter Pattern):给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
- 访问者模式(Visitor Pattern):将数据结构与数据操作分离,使得操作集合可相对自由地演化而不影响数据结构。访问者模式适用于数据结构相对稳定的系统,而其操作算法经常变化的系统。
- 状态模式(State Pattern):允许对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
- 备忘录模式(Memento Pattern):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
- 建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 享元模式(Flyweight Pattern):运用共享技术来有效地支持大量细粒度对象的复用。通过共享相同的状态,可以在有限的内存容量下支持大量的对象。
- 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。
- 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素。
- 外观模式(Facade Pattern):为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 中介者模式(Mediator Pattern):27. 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种创建型设计模式,允许通过复制现有对象来创建新对象,而不是通过实例化(构造函数或工厂方法)来创建。这种方法在某些情况下比传统的实例化更有效,因为对象的构造可能非常昂贵,或者因为需要与类层次结构分离的对象只有几个变体。
- 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种创建型设计模式,允许通过复制现有对象来创建新对象,而不是通过实例化(构造函数或工厂方法)来创建。这种方法在某些情况下比传统的实例化更有效,因为对象的构造可能非常昂贵,或者因为需要与类层次结构分离的对象只有几个变体。
代码示例:
1. 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供全局访问点。
答: 优点:
- 保证唯一实例,避免了重复创建对象,节省了系统资源。
- 提供了全局访问点,方便了对唯一实例的访问。
缺点:
- 单例模式没有抽象层,扩展困难。
- 单例模式对测试不利,因为单例模式在整个系统中只有一个实例,如果要对单例模式进行测试,那么测试结果可能会受到其他因素的影响。
应用场景:
-
系统中某个类只需要一个实例,如线程池、数据库连接池等。
-
当一个类的实例化过程比较耗费资源或者时间时,通过单例模式可以避免重复实例化,提高系统性能。
-
当多个实例会导致系统资源不足时,如Windows中的任务管理器,只能有一个实例运行。
// 单例模式示例
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
// 测试
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,让子类决定实例化哪一个类。
答:
优点:
- 工厂模式可以使代码更加灵活,易于维护和扩展。
- 工厂模式可以隐藏对象的创建细节,使客户端代码更加简洁。
缺点:
- 工厂模式增加了代码的复杂度,需要额外的类来实现。
- 工厂模式可能会导致系统中类的数量增加,增加了系统的复杂度。
应用场景:
- 当需要创建的对象具有共同的接口时,可以使用工厂模式。
- 当需要隐藏对象的创建细节时,可以使用工厂模式。
- 当需要根据不同的条件创建不同的对象时,可以使用工厂模式。
// 工厂模式
function Factory(name, age, career) {
let work;
switch (career) {
case 'coder':
work = ['写代码', '修Bug'];
break;
case 'product manager':
work = ['订会议室', '写PRD', '催更'];
break;
case 'boss':
work = ['喝茶', '看报', '见客户'];
break;
default:
throw new Error('未知职业');
}
return {
name,
age,
career,
work
};
}
// 使用工厂模式创建对象
const coder = Factory('张三', 25, 'coder');
const pm = Factory('李四', 30, 'product manager');
const boss = Factory('王五', 35, 'boss');
console.log(coder);
console.log(pm);
console.log(boss);
//运行结果
{ name: '张三', age: 25, career: 'coder', work: [ '写代码', '修Bug' ] }
{ name: '李四', age: 30, career: 'product manager', work: [ '订会议室', '写PRD', '催更' ] }
{ name: '王五', age: 35, career: 'boss', work: [ '喝茶', '看报', '见客户' ] }
3. 观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
答:
优点:
- 观察者模式可以实现松耦合,使得观察者和被观察者之间的依赖关系变得简单,且不会影响到系统的核心功能。
- 观察者模式支持广播通信,被观察者会向所有的观察者发送通知,简化了系统的设计和实现。
- 观察者模式符合开闭原则,可以在不修改目标代码的情况下,增加新的观察者和被观察者。
缺点:
- 如果一个被观察者对象有很多直接和间接的观察者,通知所有的观察者会花费很多时间。
- 如果在观察者和被观察者之间有循环依赖,可能会导致系统崩溃。
- 观察者模式需要考虑一些开发问题,例如异常处理和线程安全等。
应用场景:
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这两个方面封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象,而且不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象是紧密耦合的。
观察者模式的实现方法:
- 定义主题(Subject)接口,包含注册观察者、删除观察者、通知观察者等方法。
- 定义观察者(Observer)接口,包含更新状态等方法。
- 定义具体主题(ConcreteSubject)类,实现主题接口,维护观察者列表,并在状态发生改变时通知观察者。
- 定义具体观察者(ConcreteObserver)类,实现观察者接口,当接收到主题通知时更新自己的状态。
- 在客户端中创建具体主题和具体观察者对象,并将观察者注册到主题中。
// 定义主题接口
class Subject {
constructor() {
this.observers = [];
}
// 注册观察者
registerObserver(observer) {
this.observers.push(observer);
}
// 删除观察者
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知观察者
notifyObservers() {
this.observers.forEach((observer) => {
observer.update();
});
}
}
// 定义观察者接口
class Observer {
constructor() {}
// 更新状态
update() {}
}
// 定义具体主题类
class ConcreteSubject extends Subject {
constructor() {
super();
this.state = null;
}
// 获取状态
getState() {
return this.state;
}
// 设置状态
setState(state) {
this.state = state;
this.notifyObservers();
}
}
// 定义具体观察者类
class ConcreteObserver extends Observer {
constructor() {
super();
this.state = null;
}
// 更新状态
update() {
this.state = subject.getState();
}
}
// 创建具体主题和具体观察者对象
const subject = new ConcreteSubject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();
// 将观察者注册到主题中
subject.registerObserver(observer1);
subject.registerObserver(observer2);
// 改变主题状态
subject.setState('new state');
// 输出观察者状态
console.log(observer1.state); // 'new state'
console.log(observer2.state); // 'new state'
4. 装饰者模式(Decorator Pattern):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰者模式比生成子类更为灵活。
答:
优点:
- 装饰者模式可以动态地给一个对象添加一些额外的职责,而不需要生成子类,增加功能更为灵活。
- 装饰者模式符合开闭原则,可以在不修改原有代码的情况下,增加新的功能。
- 装饰者模式可以实现递归组合,可以对一个对象进行多次装饰,形成复杂的功能。
缺点:
- 装饰者模式会增加许多小类,增加系统的复杂度。
- 装饰者模式会增加许多对象,增加系统的内存消耗。
- 装饰者模式需要注意装饰的顺序,可能会影响到系统的运行。
应用场景:
-
当需要动态地给一个对象添加一些额外的职责时,可以使用装饰者模式。
-
当需要递归组合一个对象,形成复杂的功能时,可以使用装饰者模式。
-
当需要在不修改原有代码的情况下,增加新的功能时,可以使用装饰者模式。
// 定义一个基础组件
class Component {
operation() {}
}
// 定义一个装饰者
class Decorator extends Component {
constructor(component) {
super();
this._component = component;
}
operation() {
this._component.operation();
}
}
// 定义一个具体的装饰者
class ConcreteDecorator extends Decorator {
constructor(component) {
super(component);
}
operation() {
super.operation();
console.log('ConcreteDecorator operation');
}
}
// 使用装饰者模式
const component = new Component();
const decorator = new ConcreteDecorator(component);
decorator.operation();
// 运行结果:
"ConcreteDecorator operation"
5. 适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
答:
优点:
- 可以让两个没有任何关系的类一起运行,只要适配器这个角色能够搞定他们就成。
- 增加了类的透明性,客户端只需要调用目标接口,不需要调用适配者的接口。
- 提高了类的复用度,不需要修改原来的类就可以复用。
- 灵活性非常好,不仅可以适配类,还可以适配对象,因为适配器可以继承目标或适配者。
缺点:
- 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
- 增加了系统代码的阅读难度,降低了代码的可读性,过多使用适配器会使系统代码变得凌乱。
应用场景:
-
系统需要使用现有的类,而这些类的接口不符合系统的需要。
-
想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作。
-
对于一些已经投产的产品,由于一些原因,如改进等,不得不使用一些现有的类,但这些类又不符合现有的标准接口,这时使用适配器模式。
适配器模式是一种结构型设计模式,它允许将不兼容的对象包装成兼容的对象。适配器模式可以用来解决两个不兼容的接口之间的兼容性问题。下面是一个使用适配器模式的示例代码:
// 已有的类
class LegacyCalculator {
constructor() {
this.operations = [];
}
add(n1, n2) {
this.operations.push(`Added ${n1} to ${n2}`);
return n1 + n2;
}
subtract(n1, n2) {
this.operations.push(`Subtracted ${n2} from ${n1}`);
return n1 - n2;
}
multiply(n1, n2) {
this.operations.push(`Multiplied ${n1} by ${n2}`);
return n1 * n2;
}
divide(n1, n2) {
this.operations.push(`Divided ${n1} by ${n2}`);
return n1 / n2;
}
getOperations() {
return this.operations;
}
}
// 适配器
class NewCalculator {
constructor() {
this.legacyCalculator = new LegacyCalculator();
}
add(n1, n2) {
return this.legacyCalculator.add(n1, n2);
}
subtract(n1, n2) {
return this.legacyCalculator.subtract(n1, n2);
}
multiply(n1, n2) {
return this.legacyCalculator.multiply(n1, n2);
}
divide(n1, n2) {
return this.legacyCalculator.divide(n1, n2);
}
getHistory() {
return this.legacyCalculator.getOperations().join('; ');
}
}
// 使用
const calculator = new NewCalculator();
console.log(calculator.add(2, 3)); // 5
console.log(calculator.subtract(5, 2)); // 3
console.log(calculator.getHistory()); // Added 2 to 3; Subtracted 2 from 5
在上面的示例中,我们有一个名为LegacyCalculator的类,它是一个已有的类,但是它的接口与我们的需求不兼容。我们需要将它包装成一个新的类NewCalculator,以便与我们的需求兼容。
为此,我们创建了一个新的类NewCalculator,它包含一个LegacyCalculator对象。然后,我们在NewCalculator类中实现了与我们需求兼容的接口,即add()、subtract()、multiply()和divide()方法。这些方法实际上只是将调用转发给了LegacyCalculator对象。
此外,我们还实现了一个getHistory()方法,它返回LegacyCalculator对象的操作历史记录。这个方法是一个适配器方法,它将LegacyCalculator对象的getOperations()方法返回的操作历史记录转换成了一个字符串,以便与我们的需求兼容。
最后,我们使用NewCalculator对象来执行加、减、乘和除运算,并调用getHistory()方法来获取操作历史记录。由于NewCalculator对象已经适配了LegacyCalculator对象,因此我们可以像使用一个普通的计算器一样使用它。
6. 策略模式(Strategy Pattern):定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
答:
优点:
- 策略模式可以方便地扩展新的算法或者修改现有算法,因为每个算法都被封装在一个类中,易于理解和维护。
- 策略模式可以避免使用大量的条件语句,提高代码的可读性和可维护性。
- 策略模式可以使算法的变化独立于使用算法的客户端,从而降低了耦合度。
缺点:
- 策略模式会增加系统中类的数量,增加了系统的复杂度。
- 策略模式需要客户端了解不同的策略之间的区别,选择合适的策略,增加了客户端的复杂度。
应用场景:
-
当一个系统需要动态地在几种算法中选择一种时,可以使用策略模式。
-
当一个对象有多种行为,而这些行为在不同的场景下有不同的实现时,可以使用策略模式。
-
当一个系统需要动态地添加新的算法时,可以使用策略模式。
策略模式是一种行为型设计模式,它允许在运行时动态地切换算法或策略。在策略模式中,算法或策略被封装成独立的对象,它们之间可以相互替换,从而使得算法或策略可以独立于客户端而变化。
以下是一个使用策略模式的示例代码:
// 策略接口
class SortStrategy {
sort(arr) {}
}
// 冒泡排序策略
class BubbleSortStrategy extends SortStrategy {
sort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
}
// 快速排序策略
class QuickSortStrategy extends SortStrategy {
sort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return this.sort(left).concat([pivot], this.sort(right));
}
}
// 策略上下文
class SortContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(arr) {
return this.strategy.sort(arr);
}
}
// 使用
const arr = [5, 3, 1, 4, 2];
const bubbleSort = new BubbleSortStrategy();
const quickSort = new QuickSortStrategy();
const context = new SortContext(bubbleSort);
console.log(context.sort(arr)); // [1, 2, 3, 4, 5]
context.setStrategy(quickSort);
console.log(context.sort(arr)); // [1, 2, 3, 4, 5]
在上面的示例中,我们定义了一个SortStrategy接口和两个具体的排序策略类BubbleSortStrategy和QuickSortStrategy,它们都实现了SortStrategy接口中的sort()方法。我们还定义了一个SortContext类作为策略的上下文,它持有一个SortStrategy对象,并提供了setStrategy()和sort()方法用于切换策略和执行排序操作。
在使用时,我们可以创建一个SortContext对象,并将其初始化为使用冒泡排序策略。然后,我们可以使用sort()方法对一个数组进行排序。在排序时,SortContext对象会调用其持有的SortStrategy对象的sort()方法实现排序操作。如果需要切换策略,我们可以调用setStrategy()方法来设置新的策略,然后再次调用sort()方法即可。
7. 命令模式(Command Pattern):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
答:
优点:
- 降低系统的耦合度,请求发送者和接收者之间没有直接的联系,请求发送者只需要知道如何发送请求,而不需要知道请求的处理细节。
- 可以方便地设计一个命令队列或宏命令(组合命令)。
- 可以方便地实现对请求的撤销和重做。
- 可以方便地将命令记入日志。
- 可以方便地实现对请求的延时执行和预约执行。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统的复杂性会增加。
应用场景:
- GUI 中菜单的设计,一个菜单项对应一个命令,菜单项的点击相当于调用命令的 execute() 方法。
- 模拟 CMD。
- 模拟多级撤销和重做操作。
- 模拟宏命令。
// 定义命令接口
class Command {
execute() {}
undo() {}
}
// 定义具体命令类
class ConcreteCommand extends Command {
constructor(receiver) {
super();
this.receiver = receiver;
}
execute() {
this.receiver.action();
}
undo() {
this.receiver.undoAction();
}
}
// 定义接收者类
class Receiver {
action() {
console.log('执行操作');
}
undoAction() {
console.log('撤销操作');
}
}
// 定义请求者类
class Invoker {
constructor(command) {
this.command = command;
}
setCommand(command) {
this.command = command;
}
executeCommand() {
this.command.execute();
}
undoCommand() {
this.command.undo();
}
}
// 使用
const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker(command);
invoker.executeCommand(); // 执行操作
invoker.undoCommand(); // 撤销操作
8. 模板方法模式(Template Method Pattern):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
答:
优点:
- 提高代码复用性,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
- 实现了反向控制,通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制和松耦合。
- 可以在不修改一个算法的结构的情况下,通过重定义其中的某些步骤来实现对该算法的个性化定制。
缺点:
- 每个不同的实现都需要定义一个子类,会导致类的个数增加,系统更加庞大,设计更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,增加了代码阅读的难度。
应用场景:
- 多个子类有公有的方法,并且逻辑基本相同时。
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见下)约束其行为。
// 抽象类
class AbstractClass {
// 模板方法
templateMethod() {
this.primitiveOperation1();
this.primitiveOperation2();
}
// 基本方法1
primitiveOperation1() {
console.log('AbstractClass.primitiveOperation1()');
}
// 基本方法2
primitiveOperation2() {
console.log('AbstractClass.primitiveOperation2()');
}
}
// 具体子类
class ConcreteClass extends AbstractClass {
// 基本方法1的实现
primitiveOperation1() {
console.log('ConcreteClass.primitiveOperation1()');
}
// 基本方法2的实现
primitiveOperation2() {
console.log('ConcreteClass.primitiveOperation2()');
}
}
// 测试
const concreteClass = new ConcreteClass();
concreteClass.templateMethod();
//结果
ConcreteClass.primitiveOperation1()
ConcreteClass.primitiveOperation2()
在这个示例中,我们定义了一个抽象类AbstractClass,其中包含一个模板方法templateMethod()和两个基本方法primitiveOperation1()和primitiveOperation2()。templateMethod()是一个具体的算法框架,它定义了算法的步骤和顺序,但是将一些具体的实现延迟到子类中去完成。primitiveOperation1()和primitiveOperation2()是子类必须实现的基本方法,它们在模板方法中被调用。
我们还定义了一个具体子类ConcreteClass,它继承自抽象类AbstractClass,并实现了基本方法primitiveOperation1()和primitiveOperation2()。在客户端代码中,我们创建了一个具体子类的实例,并调用了它的模板方法templateMethod()。在控制台输出中,我们可以看到,具体子类的基本方法被调用,并输出了相应的信息。
因此,我们可以看到,模板方法模式的核心在于定义一个算法的框架,将一些具体的实现延迟到子类中去完成。这样可以提高代码的复用性和可维护性。
9. 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,
答:
优点:
- 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尝试过使用for循环进行遍历,但是对于那些比较复杂的数据结构,就没有办法了。而迭代器模式就提供了这样一种遍历方式,使得遍历操作更加简便。
- 封装性良好,我们对于迭代器的使用可以做到“对容器内部数据的隐藏”,即我们不需要关心遍历对象的内部构造,只需要通过迭代器的接口就可以实现对容器内部数据的访问。
- 可以提供多种遍历方式,比如说对于一个有序列表,我们可以根据需要提供正序遍历、倒序遍历、跳跃遍历等多种遍历方式,甚至可以在同一个遍历算法中,通过参数指定不同的遍历顺序和遍历起点。
缺点:
- 对于比较简单的遍历操作,使用迭代器方式可能会增加一定的复杂度。
- 迭代器模式的使用需要考虑较多因素,例如遍历的起点、遍历的顺序等,需要在设计时考虑周全。
应用场景:
- 需要对一个聚合对象进行遍历操作时,可以考虑使用迭代器模式。
- 需要提供多种遍历方式时,可以考虑使用迭代器模式。
- 需要对遍历进行封装时,可以考虑使用迭代器模式。
//定义迭代器接口
class Iterator {
constructor(container) {
this.list = container.list;
this.index = 0;
}
next() {
if (this.hasNext()) {
return this.list[this.index++];
}
return null;
}
hasNext() {
if (this.index >= this.list.length) {
return false;
}
return true;
}
}
//定义容器接口
class Container {
constructor(list) {
this.list = list;
}
//生成迭代器
getIterator() {
return new Iterator(this);
}
}
//测试代码
let arr = [1, 2, 3, 4, 5];
let container = new Container(arr);
let iterator = container.getIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
//结果
// 1,2,3,4,5
10. 发布-订阅模式(Publish-Subscribe Pattern):也称为观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当主题对象状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
答:
优点:
1.松耦合:发布者和订阅者之间没有直接的依赖关系,可以独立地改变和扩展。
2.可扩展性:可以动态地添加和删除订阅者,发布者和订阅者之间的关系可以随时改变。
3.解耦:发布者和订阅者之间没有直接的依赖关系,可以独立地改变和扩展。
4.灵活性:可以根据需要订阅特定的事件,而不需要修改发布者的代码。
缺点:
1.过度使用会导致性能问题:如果发布者发布了大量的事件,那么所有的订阅者都会收到通知,这可能会导致性能问题。
2.订阅者处理事件的顺序不确定:如果多个订阅者订阅了同一个事件,那么它们处理事件的顺序是不确定的。
应用场景:
1.事件驱动系统:发布-订阅模式是事件驱动系统的基础,可以用于实现异步编程。
2.消息队列:发布-订阅模式可以用于实现消息队列,订阅者可以订阅特定的消息类型,而发布者可以发布不同类型的消息。
3.日志记录:发布-订阅模式可以用于实现日志记录,订阅者可以订阅特定的日志类型,而发布者可以发布不同类型的日志。
// 定义一个主题对象
class Subject {
constructor() {
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 删除观察者
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知所有观察者
notifyObservers() {
this.observers.forEach(observer => observer.update());
}
}
// 定义一个观察者对象
class Observer {
constructor(name) {
this.name = name;
}
// 更新方法
update() {
console.log(`${this.name} has been notified.`);
}
}
// 创建一个主题对象
const subject = new Subject();
// 创建两个观察者对象
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
// 添加观察者
subject.addObserver(observer1);
subject.addObserver(observer2);
// 通知所有观察者
subject.notifyObservers();
// 删除一个观察者
subject.removeObserver(observer1);
// 通知所有观察者
subject.notifyObservers();
//结果
Observer 1 has been notified.
Observer 2 has been notified.
Observer 2 has been notified.
11. 职责链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
答:
优点:
- 降低耦合度,请求发送者不需要知道请求的处理细节,只需要知道链中的第一个处理者即可。
- 灵活性增强,可以动态地增加或修改处理节点,增加系统的灵活性。
- 可以对请求进行统一的管理和控制。
缺点:
- 性能问题,请求的传递过程中需要遍历整个链,特别是链比较长时,性能问题比较严重。
- 调试不方便,由于采用了链式结构,调试时需要逐个调试,比较麻烦。
应用场景:
- 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 需要动态指定一组对象处理请求。
//定义抽象处理者
class Handler {
constructor() {
this.nextHandler = null;
}
setNextHandler(handler) {
this.nextHandler = handler;
}
handleRequest(request) {
if (this.nextHandler !== null) {
this.nextHandler.handleRequest(request);
}
}
}
//具体处理者1
class ConcreteHandler1 extends Handler {
handleRequest(request) {
if (request === 'request1') {
console.log('ConcreteHandler1处理了请求');
} else {
super.handleRequest(request);
}
}
}
//具体处理者2
class ConcreteHandler2 extends Handler {
handleRequest(request) {
if (request === 'request2') {
console.log('ConcreteHandler2处理了请求');
} else {
super.handleRequest(request);
}
}
}
//具体处理者3
class ConcreteHandler3 extends Handler {
handleRequest(request) {
if (request === 'request3') {
console.log('ConcreteHandler3处理了请求');
} else {
super.handleRequest(request);
}
}
}
//客户端代码
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();
const handler3 = new ConcreteHandler3();
handler1.setNextHandler(handler2);
handler2.setNextHandler(handler3);
handler1.handleRequest('request1');
handler1.handleRequest('request2');
handler1.handleRequest('request3');
//结果
ConcreteHandler1处理了请求
ConcreteHandler2处理了请求
ConcreteHandler3处理了请求
12. 外观模式(Facade Pattern):为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
答:
优点:
- 简化了客户端与子系统之间的交互,降低了客户端的复杂度。
- 对客户端屏蔽了子系统组件,减少了客户端与子系统组件之间的耦合。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程。
缺点:
- 如果设计不当,增加新的子系统可能需要修改外观类或客户端代码。
- 不符合开闭原则,对修改关闭,对扩展开放。
应用场景:
- 当需要为一个复杂子系统提供一个简单接口时,可以使用外观模式。
- 当需要将子系统组件进行分层时,可以使用外观模式。
- 当需要构建一个层次结构的子系统时,可以使用外观模式。
// 子系统中的一组接口
class SubSystemA {
methodA() {
console.log('SubSystemA methodA');
}
}
class SubSystemB {
methodB() {
console.log('SubSystemB methodB');
}
}
class SubSystemC {
methodC() {
console.log('SubSystemC methodC');
}
}
// 外观类
class Facade {
constructor() {
this.subSystemA = new SubSystemA();
this.subSystemB = new SubSystemB();
this.subSystemC = new SubSystemC();
}
method() {
this.subSystemA.methodA();
this.subSystemB.methodB();
this.subSystemC.methodC();
}
}
// 客户端代码
const facade = new Facade();
facade.method();
//结果
SubSystemA methodA
SubSystemB methodB
SubSystemC methodC
13. 中介者模式(Mediator Pattern):用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
答: 优点:
- 中介者模式可以将系统中的各个对象解耦,使得对象之间的交互更加灵活,扩展更加容易。
- 中介者模式可以减少子类生成,降低系统的复杂度。
- 中介者模式可以集中控制交互,使得系统更加易于维护和管理。
缺点:
- 中介者模式会增加系统的复杂度,因为中介者需要维护各个对象之间的关系。
- 中介者模式会降低系统的效率,因为所有的交互都需要通过中介者进行转发。
应用场景:
- 当系统中的对象之间存在复杂的交互关系时,可以考虑使用中介者模式来简化交互。
- 当系统中的对象之间存在循环依赖关系时,可以考虑使用中介者模式来解耦对象之间的关系。
- 当系统需要对对象之间的交互进行集中控制时,可以考虑使用中介者模式来实现。
// 定义抽象中介者
class Mediator {
constructor() {
this.colleagues = [];
}
register(colleague) {
this.colleagues.push(colleague);
colleague.setMediator(this);
}
relay(sender, message) {
this.colleagues.forEach((colleague) => {
if (colleague !== sender) {
colleague.receive(message);
}
});
}
}
// 定义抽象同事类
class Colleague {
constructor() {
this.mediator = null;
}
setMediator(mediator) {
this.mediator = mediator;
}
send(message) {
this.mediator.relay(this, message);
}
receive(message) {
console.log(`Received message: ${message}`);
}
}
// 定义具体同事类
class ConcreteColleague1 extends Colleague {
constructor() {
super();
}
receive(message) {
console.log(`ConcreteColleague1 received message: ${message}`);
}
}
class ConcreteColleague2 extends Colleague {
constructor() {
super();
}
receive(message) {
console.log(`ConcreteColleague2 received message: ${message}`);
}
}
// 定义具体中介者
class ConcreteMediator extends Mediator {
constructor() {
super();
}
}
// 使用中介者模式
const mediator = new ConcreteMediator();
const colleague1 = new ConcreteColleague1();
const colleague2 = new ConcreteColleague2();
mediator.register(colleague1);
mediator.register(colleague2);
colleague1.send('Hello, colleague2!');
colleague2.send('Hi, colleague1!');
//结果
ConcreteColleague2 received message: Hello, colleague2!
ConcreteColleague1 received message: Hi, colleague1!
14. 组合模式(Composite Pattern):将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
答:
优点:
- 组合模式定义了包含基本对象和组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这些组合对象又可以被组合,这样不断递归下去,客户代码中无需对组合对象和基本对象进行区分,可以像处理单个对象一样处理组合对象,使得客户代码更加简单。
- 组合模式让客户端代码可以一致地处理单个对象和组合对象,无需关心处理的是单个对象还是组合对象,从而简化了客户端代码。
- 在组合模式中增加新的容器构件和叶子构件都很方便,无需对现有类库进行修改,符合“开闭原则”。
缺点:
- 组合模式在增加新构件时很难对构件类型进行限制,因为组合模式中定义的接口或抽象类通常都是用来描述构件行为的,而不是用来限制构件类型的。
- 在使用组合模式时,设计者必须考虑清楚哪些方法对于叶子构件而言是不必要的,如果没有这样做,可能会在叶子构件中抛出运行时异常。
应用场景:
- 组合模式常用于树形结构的实现,例如文件系统、菜单、组织机构等。
- 组合模式可以用于描述整体与部分的关系,例如整体由部分构成的部分和整体之间的关系,例如汽车由轮子、发动机、底盘等部分构成,整体与部分之间的关系可以用组合模式来描述。
- 组合模式可以用于描述一组相似的对象,例如一组图形,这些图形可以是图形可以是圆形、矩形、三角形等,这些图形可以被组合成更复杂的图形,例如一个由多个圆形、矩形、三角形组成的图形,这样可以用组合模式来描述。
实现方法:
- 定义抽象构件(Component):抽象构件是组合中对象声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component的子部件。
- 定义叶子构件(Leaf):叶子构件是组合中的对象,它没有子部件。实现在Component接口中定义的操作。
- 定义容器构件(Composite):容器构件是组合中的对象,它有一个或多个子部件。容器构件将请求委托给它的子部件执行。它提供一个接口,用于添加和删除子部件。
- 客户端通过Component接口操作组合部件,无需知道具体的叶子构件和容器构件的类别,可以对它们进行一致的处理。
class Component {
constructor(name) {
this.name = name;
}
add() {}
remove() {}
display() {}
}
class Leaf extends Component {
constructor(name) {
super(name);
}
display() {
console.log(this.name);
}
}
class Composite 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();
});
}
}
// 使用示例
const root = new Composite("root");
root.add(new Leaf("Leaf A"));
root.add(new Leaf("Leaf B"));
const composite = new Composite("Composite X");
composite.add(new Leaf("Leaf XA"));
composite.add(new Leaf("Leaf XB"));
root.add(composite);
const composite2 = new Composite("Composite XY");
composite2.add(new Leaf("Leaf XYA"));
composite2.add(new Leaf("Leaf XYB"));
composite.add(composite2);
root.add(new Leaf("Leaf C"));
const leaf =new Leaf("Leaf D")
// 客户端代码
root.display(); // 输出:root Leaf A Leaf B Composite X Leaf XA Leaf XB Composite XY Leaf XYA Leaf XYB Leaf C
root.remove(leaf);
root.display(); // 输出:root Leaf A Leaf B Composite X Leaf XA Leaf XB Composite XY Leaf XYA Leaf XYB
15. 享元模式(Flyweight Pattern):运用共享技术来有效地支持大量细粒度对象的复用。通过共享相同的状态,可以在有限的内存容量下支持大量的对象。
答:
优点:
- 减少内存使用,提高性能。
- 减少对象数量,降低系统复杂度。
- 提高系统灵活性,利用共享技术支持多种不同的对象。
缺点:
- 需要对对象进行分析,划分出内部状态和外部状态。
- 共享内部状态会导致系统的透明性降低。
- 代码复杂度增加,需要维护共享池。
应用场景:
- 系统中存在大量相似对象,需要重复创建。
- 对象的大部分状态可以外部化,可以将其作为参数传入。
- 系统需要缓存对象,以提高性能。
享元模式的实现方法:
- 创建享元工厂类,用于创建和管理享元对象。
- 创建享元接口,定义享元对象的方法。
- 创建具体享元类,实现享元接口,包含内部状态和外部状态。
- 在享元工厂类中维护一个享元池,用于存储已经创建的享元对象。
- 在客户端中通过享元工厂类获取享元对象,如果对象已经存在于享元池中,则直接返回;否则创建新的享元对象并加入享元池中。
// 创建享元接口
interface Flyweight {
operation(extrinsicState: any): void;
}
// 创建具体享元类
class ConcreteFlyweight implements Flyweight {
private intrinsicState: any;
constructor(intrinsicState: any) {
this.intrinsicState = intrinsicState;
}
public operation(extrinsicState: any): void {
console.log(`Intrinsic State: ${this.intrinsicState}`);
console.log(`Extrinsic State: ${extrinsicState}`);
}
}
// 创建享元工厂类
class FlyweightFactory {
private flyweights: { [key: string]: Flyweight } = {};
public getFlyweight(key: string): Flyweight {
if (!this.flyweights[key]) {
this.flyweights[key] = new ConcreteFlyweight(key);
}
return this.flyweights[key];
}
}
// 客户端代码
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight('key1');
const flyweight2 = factory.getFlyweight('key1');
const flyweight3 = factory.getFlyweight('key2');
flyweight1.operation('state1');
flyweight2.operation('state2');
flyweight3.operation('state3');
运行客户端代码后,控制台会输出以下结果:
Intrinsic State: key1
Extrinsic State: state1
Intrinsic State: key1
Extrinsic State: state2
Intrinsic State: key2
Extrinsic State: state3
可以看到,我们创建了一个享元工厂类FlyweightFactory,它可以返回具体享元类ConcreteFlyweight的实例。在客户端代码中,我们创建了三个享元实例,其中前两个使用的是同一个享元对象,而第三个使用的是另一个享元对象。
当我们调用每个享元对象的operation()方法时,它们都会输出自己的内部状态和传入的外部状态。由于前两个享元对象使用的是同一个对象,因此它们的内部状态是相同的。而第三个享元对象使用的是另一个对象,因此它的内部状态与前两个对象不同。
因此,我们可以看到控制台输出了三次,每次输出都包含了相应的内部状态和传入的外部状态。
16. 解释器模式(Interpreter Pattern):给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
答:
优点:
- 可扩展性好,增加新的解释表达式较为方便。
- 易于实现文法,解释器模式提供了构建抽象语法树的方法,每个节点都是一个表达式,只需要实现相应的解释操作即可。
缺点:
- 执行效率较低,解释器模式通常不是一个高效的模式,特别是在解释复杂文法时。
- 可维护性较差,解释器模式中的每个表达式类都需要实现解释操作,当文法规则较多时,类的数量将急剧增加,导致系统难以维护和扩展。
应用场景:
- 当有一个语言需要解释执行,并且你可以将该语言表示为一个抽象语法树时,可以考虑使用解释器模式。
- 当需要对语言进行修改、扩展时,可以考虑使用解释器模式,因为可以通过定义新的解释器来实现。
- 当一个语言的文法较为简单时,可以考虑使用解释器模式,因为简单的文法通常意味着解释器的类结构较为简单,易于实现。
解释器模式包含以下角色:
- 抽象表达式(Abstract Expression):定义解释器的接口,约定解释器的解释操作。
- 终结符表达式(Terminal Expression):实现抽象表达式的接口,用于解释文法中的终结符。
- 非终结符表达式(Nonterminal Expression):实现抽象表达式的接口,用于解释文法中的非终结符。
- 环境(Context):包含解释器之外的一些全局信息。
- 客户端(Client):构建抽象语法树,调用解释器进行解释。
实现方法:
- 定义抽象表达式接口,约定解释器的解释操作。
- 定义终结符表达式和非终结符表达式,实现抽象表达式接口,用于解释文法中的终结符和非终结符。
- 定义环境类,包含解释器之外的一些全局信息。
- 构建抽象语法树,调用解释器进行解释。
// 定义抽象表达式接口
class AbstractExpression {
interpret(context) {}
}
// 定义终结符表达式
class TerminalExpression extends AbstractExpression {
interpret(context) {
console.log("终端解释器");
}
}
// 定义非终结符表达式
class NonterminalExpression extends AbstractExpression {
constructor(expression) {
super();
this.expression = expression;
}
interpret(context) {
console.log("非终端解释器");
this.expression.interpret(context);
}
}
// 定义环境类
class Context {
constructor(input) {
this.input = input;
this.output = "";
}
}
// 构建抽象语法树,调用解释器进行解释
let context = new Context("Hello World!");
let expression = new NonterminalExpression(new TerminalExpression());
expression.interpret(context);
//结果
非终端解释器
终端解释器
17. 访问者模式(Visitor Pattern):将数据结构与数据操作分离,使得操作集合可相对自由地演化而不影响数据结构。访问者模式适用于数据结构相对稳定的系统,而其操作算法经常变化的系统。
答:
优点:
- 可以在不改变数据结构的前提下增加新的操作。
- 可以将相关的操作集中到一个访问者对象中,而不是分散在一个个的元素类中。
- 可以通过访问者来定义新的操作,从而实现对元素对象的扩展。
缺点:
- 增加新的元素类比较困难,需要修改访问者接口及其所有实现类。
- 具体元素对访问者公布细节,违反了迪米特原则。
应用场景:
- 对象结构相对稳定,但其操作算法经常变化的系统。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作“污染”这些对象的类。
- 对象结构包含很多具有不同接口的对象,而你希望对这些对象实施一些依赖于其具体类的操作。
实现方法
- 定义元素类,即数据结构,包含接受访问者的方法。
- 定义访问者接口,包含访问元素类的方法。
- 定义具体访问者类,实现访问者接口,完成对元素类的具体操作。
- 定义对象结构类,即包含元素类的集合,提供遍历元素的方法。
- 在对象结构类中定义对外公开的访问方法,接受访问者作为参数,在内部调用元素类的接受访问者方法。
- 在客户端代码中创建对象结构类和具体访问者类,调用对象结构类的访问方法,传入具体访问者类,完成对元素的操作。
// 1. 定义元素类,即数据结构,包含接受访问者的方法。
class Element {
accept(visitor) {
throw new Error('You have to implement the method accept!');
}
}
// 2. 定义访问者接口,包含访问元素类的方法。
class Visitor {
visit(element) {
throw new Error('You have to implement the method visit!');
}
}
// 3. 定义具体访问者类,实现访问者接口,完成对元素类的具体操作。
class ConcreteVisitor1 extends Visitor {
visit(element) {
console.log('ConcreteVisitor1 visited', element.constructor.name);
}
}
class ConcreteVisitor2 extends Visitor {
visit(element) {
console.log('ConcreteVisitor2 visited', element.constructor.name);
}
}
// 4. 定义具体元素类,继承自元素类,实现接受访问者的方法。
class ConcreteElement1 extends Element {
accept(visitor) {
visitor.visit(this);
}
}
class ConcreteElement2 extends Element {
accept(visitor) {
visitor.visit(this);
}
}
// 5. 定义对象结构类,即包含元素类的集合,提供遍历元素的方法。
class ObjectStructure {
constructor() {
this.elements = [];
}
addElement(element) {
this.elements.push(element);
}
removeElement(element) {
const index = this.elements.indexOf(element);
if (index !== -1) {
this.elements.splice(index, 1);
}
}
// 6. 在对象结构类中定义对外公开的访问方法,接受访问者作为参数,在内部调用元素类的接受访问者方法。
accept(visitor) {
for (const element of this.elements) {
element.accept(visitor);
}
}
}
// 7. 在客户端代码中创建对象结构类和具体访问者类,调用对象结构类的访问方法,传入具体访问者类,完成对元素的操作。
const objectStructure = new ObjectStructure();
const element1 = new ConcreteElement1();
const element2 = new ConcreteElement2();
objectStructure.addElement(element1);
objectStructure.addElement(element2);
const visitor1 = new ConcreteVisitor1();
const visitor2 = new ConcreteVisitor2();
objectStructure.accept(visitor1);
objectStructure.accept(visitor2);
运行以上代码,控制台会输出以下结果:
ConcreteVisitor1 visited ConcreteElement1
ConcreteVisitor1 visited ConcreteElement2
ConcreteVisitor2 visited ConcreteElement1
ConcreteVisitor2 visited ConcreteElement2
我们定义了一个元素类Element,它包含一个接受访问者的方法accept()。然后我们定义了一个访问者接口Visitor,它包含一个访问元素的方法visit()。接着,我们定义了两个具体访问者类ConcreteVisitor1和ConcreteVisitor2,它们实现了访问者接口,完成了对元素的具体操作。
我们还定义了一个对象结构类ObjectStructure,它包含一个元素类的集合,并提供了添加、移除、遍历元素的方法。在对象结构类中,我们定义了对外公开的访问方法accept(),它接受一个访问者作为参数,在内部调用元素类的接受访问者方法。
在客户端代码中,我们创建了一个对象结构类,并向其中添加了两个具体元素。然后,我们创建了两个具体访问者类,并将它们分别传入对象结构类的访问方法中,完成了对元素的操作。在控制台输出中,我们可以看到,每个具体访问者都访问了两个具体元素,并输出了相应的信息。
因此,我们可以看到,访问者模式的核心在于将元素类和具体操作分离开来,通过访问者类来完成对元素的操作。在客户端中,我们只需要创建具体元素和具体访问者,并将它们传递给对象结构类的访问方法中,即可完成对元素的操作。
18. 状态模式(State Pattern):允许对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
答:
优点:
- 状态模式将与特定状态相关的行为局部化,并且将不同状态的行为分割开来,使得代码更加清晰易懂。
- 状态模式将状态转换显示化,减少了对象间的相互依赖,从而减少了耦合度。
- 状态类职责明确,有利于程序的扩展。
缺点:
- 状态模式会增加系统中类和对象的个数,导致系统运行开销增大。
- 状态模式对开闭原则的支持并不太好,增加新的状态需要修改原有源代码,否则无法转换到新增状态。
应用场景:
- 对象的行为随着状态的改变而改变,例如游戏中角色的状态转换。
- 条件、分支语句的替代者,例如替代if-else语句。
- 多层状态嵌套,例如订单状态的多层嵌套。
状态模式的实现方法包括以下几个角色:
- 抽象状态(State):定义状态的接口,封装与状态相关的行为。
- 具体状态(Concrete State):实现抽象状态定义的接口,具体实现与状态相关的行为。
- 环境(Context):维护一个抽象状态的实例,定义当前状态,并提供一个接口以允许状态的切换。
// 抽象状态
class State {
constructor() {
if (new.target === State) {
throw new Error('抽象类不能实例化');
}
}
handle() {
throw new Error('抽象方法必须重写');
}
}
// 具体状态A
class ConcreteStateA extends State {
handle() {
console.log('当前状态是 A');
}
}
// 具体状态B
class ConcreteStateB extends State {
handle() {
console.log('当前状态是 B');
}
}
// 环境
class Context {
constructor() {
this.state = null;
}
setState(state) {
this.state = state;
}
getState() {
return this.state;
}
}
// 测试代码
const context = new Context();
const stateA = new ConcreteStateA();
const stateB = new ConcreteStateB();
context.setState(stateA);
context.getState().handle();
context.setState(stateB);
context.getState().handle();
//结果
当前状态是 A
当前状态是 B
19. 备忘录模式(Memento Pattern):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
答:
优点:
- 备忘录模式可以在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,从而实现对象状态的保存和恢复。
- 备忘录模式可以让客户端代码不直接访问原始对象,从而保证了封装性。
- 备忘录模式可以对状态进行版本管理,可以方便地实现撤销和重做操作。
缺点:
- 备忘录模式会增加系统的复杂度和开销,需要额外的存储空间来保存备忘录对象。
- 如果原始对象的状态过多或者过于复杂,备忘录模式可能会导致存储和恢复的开销过大。
应用场景:
- 需要保存和恢复对象状态的场景,例如文本编辑器、图形编辑器等。
- 需要实现撤销和重做操作的场景,例如文本编辑器、图形编辑器等。
- 需要实现事务性操作的场景,例如数据库操作、事务性内存等。
备忘录模式的实现方法包括以下几个角色:
- 发起人(Originator):负责创建一个备忘录,并记录自己的内部状态。
- 备忘录(Memento):存储发起人的内部状态,可以根据发起人来决定存储哪些内部状态。
- 管理者(Caretaker):负责保存备忘录,不能对备忘录的内容进行操作或检查。
具体实现步骤如下:
- 定义备忘录类,用于存储发起人的内部状态。
- 定义发起人类,负责创建备忘录,并记录自己的内部状态。
- 定义管理者类,负责保存备忘录。
- 在发起人类中添加创建备忘录和恢复备忘录的方法。
- 在管理者类中添加保存备忘录和获取备忘录的方法。
// 备忘录类,用于存储发起人的内部状态
class Memento {
constructor(state) {
this.state = state;
}
getState() {
return this.state;
}
}
// 发起人类,负责创建备忘录,并记录自己的内部状态
class Originator {
constructor() {
this.state = null;
}
setState(state) {
this.state = state;
}
getState() {
return this.state;
}
createMemento() {
return new Memento(this.state);
}
restoreMemento(memento) {
this.state = memento.getState();
}
}
// 管理者类,负责保存备忘录
class Caretaker {
constructor() {
this.mementoList = [];
}
add(memento) {
this.mementoList.push(memento);
}
get(index) {
return this.mementoList[index];
}
}
// 测试代码
const originator = new Originator();
const caretaker = new Caretaker();
originator.setState('State 1');
caretaker.add(originator.createMemento());
originator.setState('State 2');
caretaker.add(originator.createMemento());
originator.setState('State 3');
caretaker.add(originator.createMemento());
console.log('Current State:', originator.getState()); // Current State: State 3
originator.restoreMemento(caretaker.get(1));
console.log('Restored State:', originator.getState()); // Restored State: State 2
originator.restoreMemento(caretaker.get(0));
console.log('Restored State:', originator.getState()); // Restored State: State 1
20. 建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
答:
优点:
- 将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示,提高了代码的复用性。
- 隐藏了复杂对象的创建过程,使得客户端无需关心具体的创建细节,降低了客户端的耦合度。
- 可以对构建过程进行精细化控制,满足不同的需求。
缺点:
- 增加了代码的复杂度,需要定义多个类和接口。
- 对于简单对象的创建,使用建造者模式会显得过于繁琐。
应用场景:
- 需要创建复杂对象,且对象的创建过程比较稳定,但是对象的表示方式有多种。
- 需要对对象的创建过程进行精细化控制,以满足不同的需求。
- 需要隐藏对象的创建过程,使得客户端无需关心具体的创建细节。
建造者模式的实现方法包括以下几个步骤:
- 定义一个抽象的建造者接口,该接口包含了创建对象各个部分的抽象方法。
- 定义一个具体的建造者类,实现抽象建造者接口,完成对象各个部分的具体创建。
- 定义一个指挥者类,该类包含了一个建造者对象,通过调用建造者对象的方法来完成对象的创建。
- 定义一个产品类,该类包含了需要创建的对象的各个部分。
- 在客户端中,通过实例化具体的建造者对象和指挥者对象,调用指挥者对象的方法来创建对象。
// 定义产品类
class Product {
constructor() {
this.partA = '';
this.partB = '';
this.partC = '';
}
}
// 定义抽象建造者接口
class Builder {
constructor() {
this.product = new Product();
}
buildPartA() {}
buildPartB() {}
buildPartC() {}
getResult() {
return this.product;
}
}
// 定义具体建造者类
class ConcreteBuilder extends Builder {
buildPartA() {
this.product.partA = 'partA';
}
buildPartB() {
this.product.partB = 'partB';
}
buildPartC() {
this.product.partC = 'partC';
}
}
// 定义指挥者类
class Director {
constructor(builder) {
this.builder = builder;
}
construct() {
this.builder.buildPartA();
this.builder.buildPartB();
this.builder.buildPartC();
return this.builder.getResult();
}
}
// 客户端代码
const builder = new ConcreteBuilder();
const director = new Director(builder);
const product = director.construct();
console.log(product);
运行结果为:
Product { partA: 'partA', partB: 'partB', partC: 'partC' }
在这个示例中,我们使用建造者模式创建了一个产品对象Product,并定义了抽象建造者接口Builder、具体建造者类ConcreteBuilder和指挥者类Director。具体建造者类实现了抽象建造者接口中定义的方法,用于创建产品对象的各个部件。指挥者类负责指导具体建造者类如何创建产品对象,并返回最终的产品对象。
在客户端代码中,我们首先创建了具体建造者对象ConcreteBuilder和指挥者对象Director,并将具体建造者对象传递给指挥者对象。然后,我们调用指挥者对象的construct()方法,该方法会按照一定的顺序调用具体建造者对象的方法,从而创建产品对象Product。最后,我们输出了创建的产品对象Product,可以看到该对象的各个部件已经被正确地创建和赋值。
因此,建造者模式的主要目的是将一个复杂对象的构建过程和表示分离出来,使得同样的构建过程可以创建不同的表示。这种分离使得建造过程更加灵活,可以按照不同的顺序或方式来创建对象的部件,从而得到不同的表示。同时,它也可以避免在客户端代码中暴露产品对象的创建细节,提高了代码的封装性和可维护性。
21. 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
答:
优点:
- 分离抽象部分和实现部分,使得两部分可以独立扩展,提高了系统的灵活性和可扩展性。
- 对于不同的实现部分,可以动态的切换,方便了系统的维护和升级。
- 可以减少子类的个数,降低系统的管理和维护成本。
缺点:
- 增加了系统的理解和设计难度。
- 需要正确地识别出系统中两个独立变化的维度,否则会导致系统变得复杂。
应用场景:
- 当一个类存在两个或多个独立变化的维度时,可以使用桥接模式将它们分离,使得各自可以独立扩展。
- 当需要动态地切换实现部分时,可以使用桥接模式。
- 当一个类不希望与其它类有任何耦合关系时,可以使用桥接模式。
桥接模式的实现方法是定义一个抽象类,其中包含一个指向实现部分的引用,以及一些抽象方法。然后定义一个实现类,实现抽象类中的方法,并包含一个指向抽象部分的引用。这样,抽象部分和实现部分就可以独立地变化,而且可以动态地切换实现部分。在客户端中,只需要使用抽象类和实现类的接口即可,不需要关心具体的实现细节。
//定义抽象类
class AbstractClass {
constructor(implementation) {
this.implementation = implementation;
}
operation() {
const implementationResult = this.implementation.operationImplementation();
return `AbstractClass: Base operation with:\n${implementationResult}`;
}
}
//定义实现类
class ImplementationA {
operationImplementation() {
return 'ImplementationA: Here's the result on the platform A.';
}
}
class ImplementationB {
operationImplementation() {
return 'ImplementationB: Here's the result on the platform B.';
}
}
//使用抽象类和实现类的接口
const implementationA = new ImplementationA();
const abstraction = new AbstractClass(implementationA);
console.log(abstraction.operation());
const implementationB = new ImplementationB();
abstraction.implementation = implementationB;
console.log(abstraction.operation());
运行结果为:
AbstractClass: Base operation with:
ImplementationA: Here's the result on the platform A.
AbstractClass: Base operation with:
ImplementationB: Here's the result on the platform B.
在这个示例中,我们使用桥接模式实现了抽象类和实现类的接口分离。首先,我们定义了抽象类AbstractClass,该类包含了一个对实现类的引用,并且定义了一个基本的操作operation(),该操作会调用实现类的具体实现,并将结果作为字符串返回。然后,我们定义了两个实现类ImplementationA和ImplementationB,它们实现了抽象类中定义的操作operationImplementation(),并返回不同的结果。最后,我们创建了具体的实现类对象implementationA和implementationB,并将它们分别传递给抽象类对象abstraction的构造函数和属性中。最后,我们调用abstraction的operation()方法,该方法会调用具体实现类的operationImplementation()方法,从而得到不同的结果。
因此,桥接模式的主要目的是将抽象部分和实现部分解耦,使得它们可以独立地变化。它通过将抽象部分和实现部分分别封装在不同的类中,并通过一个桥接接口将它们连接起来,从而可以在不影响彼此的情况下分别进行修改和扩展。这样可以增加程序的灵活性和可维护性,并且可以使得代码更加简洁和易于理解。
22. 享元模式(Flyweight Pattern):运用共享技术来有效地支持大量细粒度对象的复用。通过共享相同的状态,可以在有限的内存容量下支持大量的对象。
答:
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享相同的状态来有效地支持大量细粒度对象的复用,从而在有限的内存容量下支持大量的对象。享元模式的优点包括减少内存使用、提高性能、简化代码等。
缺点
是可能会增加代码复杂性,因为需要将对象分为可共享和不可共享的两类。应用场景包括需要创建大量细粒度对象的系统,例如图形编辑器、文本编辑器、游戏等。
享元模式的实现方法包括以下步骤:
- 创建享元工厂(Flyweight Factory),用于创建和管理享元对象。
- 创建享元接口(Flyweight),定义享元对象的接口方法。
- 创建具体享元类(Concrete Flyweight),实现享元接口,并存储享元对象的内部状态。
- 创建非共享具体享元类(Unshared Concrete Flyweight),实现享元接口,但不共享状态。
- 在客户端中,通过享元工厂获取享元对象,并调用其方法。
// 创建享元接口
class Flyweight {
constructor(sharedState) {
this.sharedState = sharedState;
}
operation(uniqueState) {
const s = JSON.stringify(this.sharedState);
const u = JSON.stringify(uniqueState);
console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`);
}
}
// 创建具体享元类
class ConcreteFlyweight extends Flyweight {
constructor(sharedState) {
super(sharedState);
}
}
// 创建非共享具体享元类
class UnsharedConcreteFlyweight extends Flyweight {
constructor(sharedState) {
super(sharedState);
}
operation(uniqueState) {
const s = JSON.stringify(this.sharedState);
const u = JSON.stringify(uniqueState);
console.log(`Unshared Concrete Flyweight: Displaying shared (${s}) and unique (${u}) state.`);
}
}
// 创建享元工厂
class FlyweightFactory {
constructor(initialFlyweights) {
this.flyweights = {};
for (const state of initialFlyweights) {
this.flyweights[this.getKey(state)] = new ConcreteFlyweight(state);
}
}
getKey(state) {
return state.join('_');
}
getFlyweight(sharedState) {
const key = this.getKey(sharedState);
if (!(key in this.flyweights)) {
console.log('FlyweightFactory: Can't find a flyweight, creating new one.');
this.flyweights[key] = new ConcreteFlyweight(sharedState);
} else {
console.log('FlyweightFactory: Reusing existing flyweight.');
}
return this.flyweights[key];
}
listFlyweights() {
const count = Object.keys(this.flyweights).length;
console.log(`\nFlyweightFactory: I have ${count} flyweights:`);
for (const key in this.flyweights) {
console.log(key);
}
}
}
// 客户端代码
const factory = new FlyweightFactory([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12],
]);
factory.listFlyweights();
function addCarToPoliceDatabase(
ff, plates, owner, brand, model, color,
) {
console.log('\nClient: Adding a car to database.');
const flyweight = ff.getFlyweight([brand, model, color]);
flyweight.operation([plates, owner]);
}
addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red');
addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red');
factory.listFlyweights();
结果:
FlyweightFactory: I have 4 flyweights:
1_2_3
4_5_6
7_8_9
10_11_12
Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW","M5","red"]) and unique (["CL234IR","James Doe"])
Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW","X1","red"]) and unique (["CL234IR","James Doe"])
FlyweightFactory: I have 4 flyweights:
1_2_3
4_5_6
7_8_9
10_11_12
在第一次调用factory.listFlyweights()方法时,我们可以看到享元工厂中存在4个享元对象,它们分别对应4种不同的状态。然后,我们调用addCarToPoliceDatabase()函数两次,并传递不同的车辆信息。由于车辆品牌、型号和颜色等信息是共享的,因此它们会被封装成享元对象并重用。而车牌号和车主信息等信息是非共享的,因此每个享元对象都需要接收不同的唯一状态。最后,我们再次调用factory.listFlyweights()方法,可以看到享元工厂中的对象数量仍然是4个,因为我们没有创建新的享元对象。
23. 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。
答:
代理模式是一种结构型设计模式,它允许对象在不改变其接口的情况下间接地访问其所包含的内容。代理对象充当客户端和实际对象之间的中介,隐藏了实际对象的复杂性。代理可以用于许多不同的目的,例如远程对象访问、虚拟代理、保护代理等。
优点:
- 代理模式可以在不改变原始类代码的情况下,增强或修改原始类的某些方法。
- 代理模式可以将客户端与目标对象分离,从而保护目标对象的隐私和安全性。
- 代理模式可以实现延迟加载,即在需要时才创建目标对象,从而提高系统的性能。
缺点:
- 代理模式会增加系统的复杂度,因为需要增加代理类。
- 代理模式会降低系统的速度,因为需要通过代理访问目标对象。
应用场景:
- 远程代理:为位于不同地址空间的对象提供本地代表,隐藏了对象存在于不同地址空间的事实。
- 虚拟代理:根据需要创建开销很大的对象,例如图片等。
- 安全代理:控制对真实对象的访问权限。
- 智能指引:当调用真实对象时,代理处理另外一些事情,例如计算真实对象的引用次数,以便在没有引用时释放对象等。
实现方法:
- 定义一个接口,该接口是代理类和目标类的公共接口。
- 创建一个代理类,实现公共接口,并在代理类中创建一个目标类的实例。
- 在代理类中实现公共接口的方法,其中一些方法可以直接调用目标类的方法,而另一些方法可以在调用目标类方法之前或之后执行一些操作。
- 在客户端中创建代理类的实例,然后通过代理类访问目标类的方法。
// 定义一个接口,该接口是代理类和目标类的公共接口
interface Subject {
request(): void;
}
// 创建一个目标类,实现公共接口
class RealSubject implements Subject {
public request(): void {
console.log("RealSubject: Handling request.");
}
}
// 创建一个代理类,实现公共接口,并在代理类中创建一个目标类的实例
class Proxy implements Subject {
private realSubject: RealSubject;
constructor(realSubject: RealSubject) {
this.realSubject = realSubject;
}
// 在代理类中实现公共接口的方法,其中一些方法可以直接调用目标类的方法,而另一些方法可以在调用目标类方法之前或之后执行一些操作
public request(): void {
if (this.checkAccess()) {
this.realSubject.request();
this.logAccess();
}
}
private checkAccess(): boolean {
console.log("Proxy: Checking access prior to firing a real request.");
return true;
}
private logAccess(): void {
console.log("Proxy: Logging the time of request.");
}
}
// 在客户端中创建代理类的实例,然后通过代理类访问目标类的方法
function clientCode(subject: Subject) {
subject.request();
}
console.log("Client: Executing the client code with a real subject:");
const realSubject = new RealSubject();
clientCode(realSubject);
console.log("");
console.log("Client: Executing the same client code with a proxy:");
const proxy = new Proxy(realSubject);
clientCode(proxy);
结果:
Client: Executing the client code with a real subject:
RealSubject: Handling request.
Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.
在这个示例中,我们定义了一个公共接口Subject,并创建了一个目标类RealSubject和一个代理类Proxy,它们都实现了该接口。代理类中包含一个对目标类的实例realSubject的引用。当客户端调用代理类的request()方法时,代理类会在调用目标类方法之前执行一些操作(如检查访问权限),然后再调用目标类的request()方法。然后,代理类可能会在调用目标类方法之后执行其他操作(如记录访问时间)。
在客户端代码中,我们首先创建了一个RealSubject的实例,并通过该实例直接调用了request()方法,输出了RealSubject: Handling request.。接着,我们创建了一个Proxy的实例,并将RealSubject的实例传递给它。然后,我们再次调用request()方法,但这一次输出了更多的信息,包括Checking access prior to firing a real request.和Logging the time of request.。这是因为代理类在调用目标类方法之前和之后执行了其他的操作。
24. 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素。
答:
优点:
- 简化了遍历方式,对于对象集合的遍历,还是比较麻烦的,对于数组或者有序列表,我们尝试过使用for循环进行遍历,但是对于那些比较复杂的数据结构,就没有办法了。而引入了迭代器方法后,我们对于各种数据结构的遍历就统一了,都是用Iterator这个接口进行遍历,这样就简化了遍历方式。
- 可以提供多种遍历方式,比如说对于一个List,我们可以根据不同的遍历方式,如顺序遍历、倒序遍历、按照某种特殊的算法遍历等等。
- 封装性良好,我们对于迭代器的遍历算法封装在迭代器中,我们只需要给出迭代器的初始位置,然后使用hasNext()和next()方法就可以实现迭代器的遍历,而不需要关心迭代器遍历的底层实现。
缺点:
- 对于比较简单的遍历,使用迭代器方式可能会比直接使用for循环要繁琐一些。
- 在对集合遍历时,如果要遍历的过程中需要对集合进行修改,如增加、删除等操作,就需要使用集合本身的修改方法,如果在迭代器遍历的过程中直接对集合进行修改,会导致遍历的结果不可预期或者抛出异常。
应用场景:
- 访问一个聚合对象的内容而无需暴露它的内部表示。
- 支持对聚合对象的多种遍历。
- 为遍历不同的聚合结构提供一个统一的接口。
在 JavaScript 中,可以使用生成器函数来实现迭代器模式。下面是一个使用生成器函数实现的迭代器示例:
// 定义一个聚合类
class Aggregate {
#items = [];
add(item) {
this.#items.push(item);
}
// 返回一个迭代器对象
createIterator() {
let index = 0;
const items = this.#items;
// 生成器函数作为迭代器对象
return {
next: function() {
return index < items.length ?
{ value: items[index++], done: false } :
{ done: true };
}
}
}
}
// 使用迭代器遍历聚合对象
const aggregate = new Aggregate();
aggregate.add("element1");
aggregate.add("element2");
aggregate.add("element3");
const iterator = aggregate.createIterator();
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
//结果
element1
element2
element3
在这个示例中,我们定义了一个聚合类Aggregate,其中包含一个私有数组#items,表示要迭代的元素列表。Aggregate类中的createIterator()方法返回一个迭代器对象,该对象实际上是一个生成器函数,可以使用next()方法来遍历元素列表。
在主程序中,我们首先创建了一个Aggregate对象,并向其中添加了三个元素。然后,我们通过调用createIterator()方法创建了一个迭代器对象,并使用while循环遍历了聚合对象中的所有元素,并打印出每个元素的值。
通过使用生成器函数作为迭代器对象,我们可以更轻松地实现迭代器模式,并且可以使用更简洁的代码来完成相同的任务。
25. 外观模式(Facade Pattern):为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
答:
优点:
- 简化了客户端与子系统之间的交互,客户端无需了解子系统的具体实现,降低了客户端的复杂度。
- 提高了子系统的独立性和可移植性,由于外观类的存在,客户端可以通过外观类来访问子系统,而不需要直接访问子系统,这样子系统的变化不会影响到客户端。
- 符合迪米特法则,即最少知道原则,客户端只需要知道外观类即可,不需要知道子系统的具体实现。
缺点:
- 外观类的引入增加了系统的复杂度和理解难度,如果设计不当,可能会导致系统出现难以维护的问题。
- 外观类对于某些客户端来说可能过于简化了系统的访问,导致客户端无法实现更加细粒度的控制。
应用场景:
- 当需要为一个复杂子系统提供一个简单接口时,可以使用外观模式,外观类可以将子系统的复杂性隐藏起来,为客户端提供一个简单的接口。
- 当需要将子系统从客户端中解耦时,可以使用外观模式,外观类可以将客户端与子系统的耦合度降到最低,使得子系统的变化不会影响到客户端。
- 当需要对子系统进行分层时,可以使用外观模式,外观类可以将子系统分成多个层次,每个层次都提供一个简单的接口,使得子系统的结构更加清晰。
外观模式的实现方法如下:
- 定义一个外观类,该类包含子系统中所有需要被客户端访问的方法。
- 在外观类中定义一个高层接口,该接口将子系统中的一组接口进行封装,使得客户端可以通过该接口来访问子系统。
- 在外观类中实现高层接口,该接口将客户端的请求转发给子系统中相应的接口。
- 客户端通过外观类来访问子系统,而不需要直接访问子系统中的接口,从而降低了客户端的复杂度。
// 子系统中的一组接口
class SubSystemA {
methodA() {
console.log('SubSystemA methodA');
}
}
class SubSystemB {
methodB() {
console.log('SubSystemB methodB');
}
}
class SubSystemC {
methodC() {
console.log('SubSystemC methodC');
}
}
// 外观类
class Facade {
constructor() {
this.subSystemA = new SubSystemA();
this.subSystemB = new SubSystemB();
this.subSystemC = new SubSystemC();
}
// 高层接口
method() {
this.subSystemA.methodA();
this.subSystemB.methodB();
this.subSystemC.methodC();
}
}
// 客户端
const facade = new Facade();
facade.method();
//运行结果
SubSystemA methodA
SubSystemB methodB
SubSystemC methodC
26. 中介者模式(Mediator Pattern):
答:
中介者模式是一种行为型设计模式,它允许将复杂的系统划分为一组相互关联的对象,这些对象通过中介者对象进行通信,而不是直接相互引用。中介者模式可以减少对象之间的耦合度,使得系统更易于维护和扩展。在中介者模式中,中介者对象负责协调对象之间的交互,它通常包含一个或多个接口,用于定义对象之间的通信协议。当一个对象需要与其他对象进行通信时,它将消息发送给中介者对象,中介者对象将消息转发给目标对象,从而实现对象之间的通信。中介者模式适用于需要协调多个对象之间的复杂交互的场景,例如图形用户界面(GUI)应用程序、多人在线游戏等。
优点:
- 减少类之间的耦合,将原本互相依赖的类通过中介者进行通信,使得类之间的关系更加简单明了。
- 可以将控制集中管理,降低系统的复杂度。
- 可以减少子类的生成,因为中介者将原本分散在多个类中的行为集中在一起,减少了子类的生成。
缺点:
- 中介者本身可能会变得复杂,难以维护。
- 中介者模式可能会导致系统变得过于集中化,使得系统的灵活性降低。
引用场景:
- 当系统中多个对象之间存在复杂的交互关系,且这些关系难以维护时,可以考虑使用中介者模式。
- 当系统中的对象之间存在循环依赖关系时,可以使用中介者模式来解决这个问题。
- 当系统需要对某些对象进行集中控制时,可以使用中介者模式。
中介者模式的实现方法包括以下几个步骤:
- 定义中介者接口:中介者接口定义了中介者需要实现的方法,这些方法用于与其他对象进行通信。
- 实现中介者类:中介者类实现了中介者接口,负责协调其他对象之间的交互。
- 定义对象接口:对象接口定义了对象需要实现的方法,这些方法用于与中介者进行通信。
- 实现对象类:对象类实现了对象接口,负责与中介者进行通信,并根据中介者的指示执行相应的操作。
- 在对象类中保存中介者对象:对象类需要保存中介者对象的引用,以便能够与中介者进行通信。
- 在客户端代码中创建中介者对象和对象对象:客户端代码需要创建中介者对象和对象对象,并将对象对象的中介者引用设置为中介者对象。
// Step 1: 定义中介者接口
class Mediator {
send(message, colleague) {}
}
// Step 2: 实现中介者类
class ConcreteMediator extends Mediator {
constructor() {
super();
this.colleague1 = null;
this.colleague2 = null;
}
set colleague1(value) {
this._colleague1 = value;
}
set colleague2(value) {
this._colleague2 = value;
}
send(message, colleague) {
if (colleague === this._colleague1) {
this._colleague2.notify(message);
} else {
this._colleague1.notify(message);
}
}
}
// Step 3: 定义对象接口
class Colleague {
constructor(mediator) {
this._mediator = mediator;
}
send(message) {}
notify(message) {}
}
// Step 4: 实现对象类
class ConcreteColleague1 extends Colleague {
constructor(mediator) {
super(mediator);
}
send(message) {
this._mediator.send(message, this);
}
notify(message) {
console.log(`Colleague 1 received message: ${message}`);
}
}
class ConcreteColleague2 extends Colleague {
constructor(mediator) {
super(mediator);
}
send(message) {
this._mediator.send(message, this);
}
notify(message) {
console.log(`Colleague 2 received message: ${message}`);
}
}
// Step 5: 在对象类中保存中介者对象
const mediator = new ConcreteMediator();
const colleague1 = new ConcreteColleague1(mediator);
const colleague2 = new ConcreteColleague2(mediator);
mediator.colleague1 = colleague1;
mediator.colleague2 = colleague2;
// Step 6: 在客户端代码中创建中介者对象和对象对象
colleague1.send('Hello, colleague 2!');
colleague2.send('Hi, colleague 1!');
//运行结果
Colleague 2 received message: Hello, colleague 2!
Colleague 1 received message: Hi, colleague 1!
27. 原型模式(Prototype Pattern):
答:
原型模式是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而不是通过实例化类来创建。这种模式适用于需要创建多个相似对象的情况,因为它可以提高性能并减少代码重复。在原型模式中,原型对象是一个可定制的对象,它包含了创建新对象所需的所有属性和方法。新对象可以通过复制原型对象来创建,然后根据需要进行修改。原型模式可以通过浅拷贝或深拷贝来实现。
优点:
- 可以动态添加和删除产品类。
- 可以改变具体的产品类。
- 可以随意添加新的产品类。
- 可以减少子类的构造。
- 可以用于动态配置系统。
缺点:
- 需要为每一个类配备一个克隆方法。
- 克隆方法位于类的内部,当对已有类进行改造时,需要修改源代码,违背了开闭原则。
应用场景:
- 当需要创建的对象的类型由运行时决定时,可以使用原型模式,通过克隆已有的对象来创建新的对象。
- 当需要避免使用构造函数创建对象时,可以使用原型模式。
- 当需要动态配置系统时,可以使用原型模式。
// 定义一个抽象原型类
class Prototype {
constructor() {
this.name = '';
}
clone() {}
}
// 定义具体原型类
class ConcretePrototype extends Prototype {
constructor() {
super();
this.name = 'ConcretePrototype';
}
clone() {
return Object.create(this);
}
}
// 客户端代码
const prototype = new ConcretePrototype();
const clone = prototype.clone();
console.log(clone.name); // 输出:ConcretePrototype