四、行为型模式(Behavior pattern)
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式可以分为两种类型:
- 类行为型模式:这些模式使用继承关系在多个类之间分配行为。主要通过多态等方式在父类和子类之间分配职责。
- 对象行为型模式:这些模式使用对象的聚合关联关系来分配行为。主要通过对象关联等方式在两个或多个类之间分配职责。基于“合成复用原则”,它们强调在系统中尽量使用关联关系取代继承关系。
1. 策略模式(Strategy Pattern)
策略模式的核心思想是将不同的算法封装成独立的策略类,然后在上下文中根据需要动态选择不同的策略,使得算法的变化不影响到使用算法的客户端。
策略模式适用于当一个系统有多种算法,且这些算法经常需要在运行时切换,或者当一个对象有多种行为并且需要动态选择时,能够提高系统的灵活性、可维护性和可扩展性。
在软件开发中, 经常会遇到这种情况, 开发一个功能可以通过多个算法去实现。 我们可以将所有的算法集中在一个类中,在这个类中提供多个方法, 每个方法对应一个算法;或者我们也可以将这些算法都封装在一个统一的方法中,使用 if...else... 等条件判断语句进行选择。但是这两种方式都存在硬编码的问题,后期需要增加算法就需要修改源代码,这会导致代码的维护变得困难。
比如网购,你可以选择工商银行、农业银行、建设银行等等,但是它们提供的算法都是一致的,就是帮你付款。
在软件开发中也会遇到相似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。
类图
在策略模式中,有三个主要角色:
- Context(上下文) :上下文类是包含了一个策略接口的引用,通过这个接口与策略对象交互,可以让客户端选择不同的算法。
- Strategy(策略) :策略接口是定义了一个算法族的公共接口,具体的策略类实现了这个接口,每个策略类封装了一个具体的算法。
- ConcreteStrategy(具体策略) :具体策略类实现了策略接口,包含了具体的算法实现。
举个例子
//购物车的折扣策略
//定义策略
const discountStrategies = {
"none": function(price) {
return price;
},
"percentage": function(price, percentage) {
return price * (1 - percentage / 100);
},
"fixedAmount": function(price, amount) {
return price - amount;
},
"buyTwoGetOneFree": function(price, quantity) {
if (quantity >= 3) {
return price * (2 / 3);
}
return price;
}
};
//创建一个上下文对象,它使用这些策略来计算价格
class ShoppingCart {
constructor(discountStrategy = "none", ...args) {
this.discountStrategy = discountStrategies[discountStrategy];
this.args = args; // 用于传递给折扣策略的参数
}
setDiscountStrategy(discountStrategy, ...args) {
this.discountStrategy = discountStrategies[discountStrategy];
this.args = args;
}
calculateTotal(price, quantity) {
return this.discountStrategy(price, ...this.args);
}
}
//创建一个购物车对象,并应用不同的折扣策略来计算总价
// 创建一个购物车,没有折扣
let cart = new ShoppingCart("none");
console.log(cart.calculateTotal(100, 2)); // 输出: 200
// 设置百分比折扣策略 (10%)
cart.setDiscountStrategy("percentage", 10);
console.log(cart.calculateTotal(100, 2)); // 输出: 180
// 设置固定金额折扣策略 (20)
cart.setDiscountStrategy("fixedAmount", 20);
console.log(cart.calculateTotal(100, 2)); // 输出: 180
// 设置买二送一策略
cart.setDiscountStrategy("buyTwoGetOneFree", 3);
console.log(cart.calculateTotal(100, 3)); // 输出: 66.666..
例子中,ShoppingCart类是上下文类,负责设置折扣策略并计算最终价格。不同的折扣策略对应不同的具体策略类,每个具体策略类实现了discountStrategies接口,并根据具体的折扣逻辑提供不同的打折方法。通过设置不同的折扣策略,商场可以根据顾客的类型应用不同的折扣,实现了动态切换不同的算法。
优缺点
优点:
- 可扩展性:策略模式允许在运行时动态地添加、删除或更改算法,而无需修改上下文或客户端代码。
- 易于维护:每个具体策略都是相对独立的,易于理解、修改和测试。
- 减少代码重复:策略模式可以避免使用大量的条件语句或switch语句,因此减少了代码重复。
- 符合开闭原则:可以通过添加新的策略类来扩展系统的行为,而无需修改现有代码。
- 解耦性良好:策略模式将算法与使用算法的客户端分离,降低了彼此之间的依赖关系。
缺点:
- 客户端必须知晓策略:客户端需要了解不同的策略类,选择合适的策略并将其传递给上下文,这可能增加客户端的复杂性。
- 上下文选择策略的责任:上下文类需要选择合适的策略,可能需要依赖外部信息来做出选择,这可能使得上下文类本身变得复杂。
策略模式在需要动态地选择算法或行为时非常有用,能够提高代码的灵活性和可维护性。然而,要根据具体情况权衡使用,避免过度使用策略模式。
2. 状态模式
状态模式将每个状态封装成一个单独的类,并将状态间的转换逻辑封装在一个上下文对象中。
状态模式的关键在于根据不同的状态将相应的行为封装在具体状态类中,使得状态之间的转换变得简单灵活。对象的行为会随着状态的改变而改变,但对象本身的接口不会改变,这符合开闭原则。
类图
在状态模式中,有三个主要角色:
- Context(环境) :它定义了客户感兴趣的接口,维护一个当前状态对象的引用,可以是状态对象的上下文,负责实际使用状态对象处理请求。
- State(状态) :定义了一个接口,封装了与Context的一个特定状态相关的行为。具体的状态类实现了这个接口,每个具体状态类负责对应状态下的行为逻辑。
- ConcreteState(具体状态) :具体状态类实现了状态接口,定义了在状态转移时的具体行为。
代码实现
//假设我们有一个订单处理系统,订单有不同的状态(如:已创建、已支付、已发货、已完成)。
//在不同的状态下,订单可以执行不同的操作。
//定义状态接口
class State {
handle(order) {
throw new Error("Subclass must implement abstract method");
}
}
//实现具体状态类
class CreatedState extends State {
handle(order) {
if (order.pay()) {
order.setState(new PaidState());
console.log("Order has been paid.");
} else {
console.log("Payment failed.");
}
}
}
class PaidState extends State {
handle(order) {
order.ship();
order.setState(new ShippedState());
console.log("Order has been shipped.");
}
}
class ShippedState extends State {
handle(order) {
order.deliver();
order.setState(new DeliveredState());
console.log("Order has been delivered.");
}
}
class DeliveredState extends State {
handle(order) {
console.log("Order is already delivered. No further actions can be taken.");
}
}
order.handle(); // 交付(如果已发货)
order.handle(); // 无法进一步操作(已完成)
例子中,Order类是环境类,持有一个当前状态对象。不同的状态对应不同的具体状态类,每个具体状态类实现了订单状态接口,并根据自身状态提供了不同的处理方法。通过改变订单状态并处理订单,模拟了订单在不同状态下的处理场景。
优缺点
优点:
- 封装状态:将不同状态的行为封装在不同的状态类中,使得每个状态的变化都成为一个独立的模块,易于管理和维护。
- 简化条件逻辑: 避免了大量的条件语句,因为状态模式使得对象的行为与其状态相关联,状态变化时对象的行为也相应改变。
- 开闭原则:新状态的加入不会影响现有状态类的代码,易于扩展。
- 符合单一职责原则: 每个状态都有对应的类,使得各个状态的逻辑分离清晰,每个类只负责自己的行为。
缺点:
- 类数量增多:如果状态过多,会产生大量的具体状态类,导致类的数量增加,维护成本增加。
- 逻辑分散:如果状态转换逻辑复杂,状态之间相互影响,可能会使代码变得复杂且难以理解。
- 状态切换开销:对象状态切换时需要改变对象的状态,可能引入一定的开销。
状态模式在需要根据状态改变对象行为的情况下非常有用,能够提高代码的可维护性和可扩展性。然而,在应用状态模式时,需要根据具体情况权衡好处和代价,避免过度设计。
3. 观察者模式(Observer Pattern)
观察者模式定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。
理解:观察者模式它建立了一种一对多的依赖关系,让多个观察者对象同时监听并且被通知目标对象的任何变化或事件。
类图
包含角色:
- Subject(目标):目标又称为主题,指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify(),目标类可以是接口,也可以是抽象类或具体类。
- ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法,如果无须扩展目标类,则具体目标类可以省略。
- Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
- ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法,通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。
代码实现
//使用类和原型链来实现观察者模式
// 定义一个观察者类,每个观察者都有一个 update 方法,用于接收通知。
class Observer {
constructor(name) {
this.name = name;
}
update(message) {
console.log(`${this.name} received message: ${message}`);
}
}
// Subject 维护一个观察者列表
//并提供 addObserver、removeObserver 和 notifyObservers 方法来管理观察者以及通知它们。
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(message) {
this.observers.forEach(observer => {
observer.update(message);
});
}
}
// 使用示例
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
const observer3 = new Observer('Observer 3');
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);
// 主题状态变化,通知所有观察者
subject.notifyObservers('Hello Observers!');
// 移除一个观察者
subject.removeObserver(observer2);
// 再次通知所有观察者(observer2 将不会被通知)
subject.notifyObservers('Another message for observers.');
优缺点
优点:
- 松耦合:观察者模式将被观察者和观察者解耦,被观察者不需要知道观察者的具体细节,只需要知道观察者接口,降低了对象之间的耦合度。
- 可扩展性:可以根据需求在任何时候增加或删除观察者,而不需要对被观察者进行修改,使得系统更加灵活,易于扩展。
- 通知机制:观察者模式通过订阅/通知机制,实现了一对多的依赖关系,当被观察对象的状态发生变化时,能够通知所有观察者对象。
- 支持广播通信:被观察者状态改变时能够通知多个观察者,适用于需要广播通知的场景。
缺点:
- 可能导致性能问题:当观察者过多或者观察者的处理操作耗时时,可能会影响被观察者的性能,因为被观察者需要遍历通知所有观察者。
- 循环依赖问题:观察者和被观察者相互持有引用时,容易产生循环依赖,需要谨慎设计。
- 避免消息过多问题:观察者模式的通知是广播式的,当被观察者状态频繁变化时,可能会导致观察者接收大量的无用通知。
观察者模式适用于场景中的一对多的依赖关系,当一个对象的改变需要通知其他多个对象时,观察者模式是一个很好的选择。但需要注意在设计时避免上述的缺点,合理控制观察者数量和通知频率,避免性能问题和循环依赖。
⭐发布 - 订阅模式与观察者模式的区别
| 发布-订阅者模式 | 观察者模式 | |
|---|---|---|
| 含义 | 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作 | 发布者直接触及到订阅者的操作 |
| 例子 | 老王把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者 | 老王把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员 |
| 其他 | 发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。 | 并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用 |
4.迭代器模式(Iterator Pattern)
迭代器模式提供了一种统一的方法来遍历不同类型的集合,而无需暴露集合内部的表示细节。它包括两个主要组件:迭代器和集合。迭代器负责遍历集合并提供统一的访问接口,而集合负责实际存储元素。迭代器和集合之间的解耦使得可以独立地改变它们的实现,而不会影响到客户端代码。解决遍历问题。
ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。在ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。
在迭代器模式中,我们定义一个抽象的迭代器类,它包含两个方法:一个是hasNext()方法,用于判断是否还有下一个元素;另一个是next()方法,用于获取下一个元素。然后,每个容器类都实现自己的迭代器类,以访问容器中的元素。
类图
迭代器模式包含如下角色:
- Iterator(抽象迭代器):定义访问和遍历容器元素的接口,通常有 hasNext、next 等方法;
- Concretelterator(具体迭代器):实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历;
- Aggregate(抽象聚合):定义新增、修改、删除聚合元素以及创建迭代器对象的接口;
- ConcreteAggregate(具体聚合):实现抽象聚合,返回一个具体的迭代器实例。
代码实现
//自定义数组迭代器
// 定义集合类
class MyCollection {
constructor(items) {
this.items = items;
}
// 获取迭代器的方法
getIterator() {
return new MyIterator(this.items);
}
}
// 定义迭代器类
class MyIterator {
constructor(items) {
this.items = items;
this.index = 0;
}
// 检查是否还有下一个元素
hasNext() {
return this.index < this.items.length;
}
// 获取下一个元素
next() {
if (this.hasNext()) {
return this.items[this.index++];
}
return null; // 或者可以抛出异常
}
}
// 使用示例
const collection = new MyCollection([1, 2, 3, 4, 5]);
const iterator = collection.getIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
优缺点
优点:
- 简化遍历:提供了一种统一的方式遍历集合对象,不需要暴露集合的内部结构,使得遍历更加简单和统一。
- 解耦合:分离了集合对象的遍历行为和集合对象本身,降低了彼此之间的依赖关系,提高了代码的灵活性和可维护性。
- 支持多种遍历方式:可以针对同一个集合提供多种不同的遍历方式,满足不同场景的需求。
- 隐藏内部细节:客户端只需关心如何遍历集合,而不需要关心集合的具体实现细节,提高了代码的封装性。
缺点:
- 性能问题:在某些特定场景下,迭代器模式可能引起性能问题,尤其是在大数据集合的情况下,可能会有一定的性能开销。
- 增加复杂性:如果仅仅需要遍历一个简单的数据结构,使用迭代器模式可能会增加代码的复杂性,不利于代码的可读性。
迭代器模式是一种非常有用的设计模式,特别适用于需要遍历集合对象的场景,并且能够使得遍历行为更加灵活和可控。但在某些特定场景下,可能需要权衡使用该模式带来的性能开销和代码复杂性。