0%

javascript-design-pattern-strategy

策略模式

策略模式是一种行为设计模式,它定义了一系列算法,将每个算法封装到一个类中,并使它们可以相互替换。策略模式让算法独立于使用它的客户端。

现实中的例子

假设我们经营一家电影院,目前是销售淡季,我们推出了各种折扣,学生票八折,儿童票九折,VIP会员半价。我们要设计一个系统,根据不同的折扣算法计算电影票的价格。

不假思索的你可能写下了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum UserType {
Regular,
Student,
Child,
VIP,
}

class MovieTicket {
private ticketPrice = 100;
private userType?: UserType;

setUserType(userType: UserType) {
this.userType = userType;
}

getPrice() {
switch (this.userType) {
case UserType.Student:
return this.ticketPrice * 0.8;
case UserType.Child:
return this.ticketPrice * 0.9;
case UserType.VIP:
return this.ticketPrice * 0.5;
default:
return this.ticketPrice;
}
}
}

const ticket = new MovieTicket();
ticket.setUserType(UserType.Student);
console.log(ticket.getPrice());

我们来看看这段代码有什么问题:

  1. 违反了开闭原则。如果我们要添加新的折扣类型,比如老年人票,就需要修改MovieTicket类,违反了开闭原则。
  2. 违反了单一职责原则。MovieTicket类不仅负责计算价格,还负责判断折扣类型,违反了单一职责原则。
  3. 耦合性太高,不易维护。如果折扣算法发生变化,需要修改MovieTicket类,不易维护。
  4. 其他类如果想要使用折扣算法的话,只能复制粘贴一份,导致代码重复。

下面我们通过策略模式来重构这段代码,策略模式的精髓就是将算法和使用算法的类解耦,使得算法可以独立于使用它的类进行变化。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface Discount {
getDiscount(): number;
}

class StudentDiscount implements Discount {
getDiscount(): number {
return 0.8;
}
}

class ChildrenDiscount implements Discount {
getDiscount(): number {
return 0.9;
}
}

class VIPDiscount implements Discount {
getDiscount(): number {
return 0.5;
}
}

class MovieTicket {
private ticketPrice = 100;
private discount?: Discount;

setDiscount(discount: Discount) {
this.discount = discount;
}

getPrice() {
if (this.discount) {
return this.ticketPrice * this.discount.getDiscount();
}

throw new Error("No discount set");
}
}

const ticket = new MovieTicket();
ticket.setDiscount(new StudentDiscount());
console.log(ticket.getPrice());

代码解析:

  1. 首先定义一个接口Discount,它有一个getDiscount方法,返回折扣值。所有具体的折扣类都要实现这个接口。
  2. 然后定义三个具体的折扣类:StudentDiscountChildrenDiscountVIPDiscount。在每个折扣类中,我们实现了getDiscount方法,返回不同的折扣值。
  3. 最后定义一个MovieTicket类,它有一个ticketPrice属性,表示电影票的价格,还有一个discount属性,表示折扣。我们定义了一个setDiscount方法,用来设置折扣。getPrice方法用来计算折扣后的价格。

这种实现使得折扣算法和使用折扣的类解耦,使得折扣算法可以独立于使用它的类进行变化。好处如下:

  1. 便于扩展,可以很容易地添加新的折扣类。只要这个类实现了Discount接口,就可以被MovieTicket类使用。
  2. 由于折扣算法和使用折扣的类解耦,所以折扣算法可以独立于使用它的类进行变化。如果折扣算法发生变化,只需要修改折扣类即可,不需要修改使用折扣的类, 比如儿童票从9折改成8折,只需要修改ChildrenDiscount类使其返回0.8即可。
  3. 如果有其他类想使用折扣算法的话,也可以直接使用。

以上代码其实使用了依赖注入的方式,MovieTicket类依赖于Discount接口,我们通过Setter的方式将具体的折扣类注入到MovieTicket类中。

注意这里discount属性是可选的,因为我们在getPrice方法中判断了discount是否存在,如果不存在就抛出异常。这样做是为了防止忘记设置折扣而导致的错误。如果不设置为可选的话会报错:Property 'discount' has no initializer and is not definitely assigned in the constructor. 由于TypeScript配置文件默认开启了strictPropertyInitialization选项导致的。

策略模式UML图

image

  • Context:消费策略的类相当于上例中的MovieTicket类,内涵一个抽象策略类,相当于上例中的Discount接口。
  • Strategy:抽象策略类,相当于上例中的Discount接口。
  • ConcreteStrategy:具体策略类,相当于上例中的StudentDiscountChildrenDiscountVIPDiscount类。
  • execute:执行策略的方法,相当于上例中的getDiscount方法。

策略模式的应用场景

  1. Sorting Algorithms: If you have a collection of data that can be sorted in several ways, you can use the Strategy pattern to switch between sorting algorithms (like quicksort, merge sort, heap sort) at runtime.
  2. Payment Methods: In an e-commerce application, you can use the Strategy pattern to switch between different payment methods (like credit card, PayPal, Bitcoin) at runtime.
  3. Compression Algorithms: If you have a file that can be compressed using different algorithms (like zip, rar, 7z), you can use the Strategy pattern to choose the compression algorithm at runtime.
  4. Travel Planning: If you have a travel planning application, you can use the Strategy pattern to switch between different travel strategies (like by car, by train, by plane) at runtime.