设计模式:策略模式
如何衡量代码的好坏
如果你是一个有着多年开发经验的工程师,你有没有想过一个问题,什么样的代码才能被称之为好代码?针对这个问题,有着五花八门的答案,有的人会说容易理解的代码就是好代码,有些人会说易于维护的代码就是好代码,还有人会说简洁的代码就是好代码。这些答案都对,而且你也会发现这些特征被人们总结成一个个短词,比如可维护性(Maintainability)、可读性(Readability)、稳定性(Statility)、可拓展性(Extensibility)、可伸缩性(Scalability)、可测试性(Testability)、可移植性(Portability)、可观测性(Observability)。。。
一开始看到这些短词,觉得很有道理,甚至觉得就是金科玉律,但是慢慢地你会经常看到这些词。这些词好像就是万金油,放到哪都对,可以用来衡量代码的好坏,也可以用来衡量一个系统的好坏,甚至还可以用来衡量软件中任何一个部件的好坏。但问题是这些词仅仅提供一个目标,而且是相当模糊的目标,比如什么才叫稳定?是这部分代码,或一个系统永远不出问题才能被称为稳定吗?我们都知道这是不可能的。什么才叫可维护?是每当遇到 bug 任何一个人都能修吗?这也很难。
对于这些词,我们其实看看就好了,不必太过于纠结,多读、多写、多思考、多总结,最后你写出来的代码自然就会越来越好。
但我想说的是,在重构和优化代码时,我们只需要做好两点——去重 和 解耦,这样你写出来的代码不会差。你会发现很多的重构技巧也好,设计模式也好,都是围绕着这两点展开的。重复的代码意味着,针对相同的逻辑和操作,你维护两份甚至多份代码,这样每当需求发生变化时,你就需要去更改多份代码,这显然不是我们想要看到的。如果一个系统中的模块之间相互耦合紧密,那么这个系统就不是 正交 的,模块与模块之间相互依赖,一个模块的代码逻辑被更改,另一个模块也跟着发生变化,整个系统牵一发动全身,拓展和维护难上加难。
只要在一定程度上做到了这两点,就是好代码。注意,我这里说的是一定程度上,很多时候,特别对于一个复杂的系统,我们很难一下子就做到完全没有重复,模块和模块完全解耦,这是一个循序渐进的过程,随着我们知识和经验的增加,我们会更清楚如何写出好的代码。或许,多年以后,回过头去看自己现在写的代码,就更加明白什么样的代码才是好代码了。
场景再现
假设现在我们要给一家餐厅做一个接口,帮助管理订单,我们可以得到下面的代码结构:
1
2
3
4
5
6
7
class Order {}
class DineInOrder extends Order {}
class PickUpOrder extends Order {}
class DeliveryOrder extends Order {}
这里,我们向对应的类添加功能,仅考虑支付渠道和菜品打包:
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
abstract class Order {
abstract public void order();
abstract public void package();
}
class DineInOrder extends Order {
public void order() {
System.out.println("Make order in the restaurant");
}
public void package() {
// no need package
}
public void service() {
System.out.println("Service Table");
}
}
class PickUpOrder extends Order {
public void order() {
System.out.println("Make order through telephone");
}
public void package() {
System.out.println("Wrap food by lunch box");
}
}
class DeliveryOrder extends Order {
public void order() {
System.out.println("Make order through 3rd party app");
}
public void package() {
System.out.println("Wrap food by lunch box");
}
}
很明显,这里有重复的代码,PickUpOrder
和 DeliveryOrder
中的 package()
干的是相同的事情。另外,DineInOrder
并不需要 package()
,它需要的是 service()
,当然,我们可以考虑将 service()
与 package()
合并,可这样的话,名字跟做的事情不匹配,代码的可读性就变差了。
这里我们尝试着去对着两个行为进行抽象:
1
2
3
4
5
6
7
interface OrderAction {
void order();
}
interface ServiceAction {
void service();
}
因为 order
和 package
是变化的,它在不同的场景(子类)下,都会有所不同,我们把这变化的部分抽象成两个接口,一个是 OrderAction
,用来表示 order
这个业务逻辑,另一个是 ServiceAction
,用来表示 service
这个业务逻辑,其实之前的 package
也归属于 service
,想想看,叫外卖或者网上订餐,除了准备菜品,餐厅所做的服务是不是就是打包?
这样的话,我们就可以把之前的逻辑提取出来了:
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
class OrderInRestaurant implements OrderAction {
public void order() {
System.out.println("Make order in the restaurant");
}
}
class OrderByPhone implements OrderAction {
public void order() {
System.out.println("Make order through telephone");
}
}
class OrderThroughPlatform implements OrderAction {
public void order() {
System.out.println("Make order through 3rd party app");
}
}
class NormalPackage implements ServiceAction {
public void service() {
System.out.println("Wrap food by lunch box");
}
}
class TableService implements ServiceAction {
public void service() {
System.out.println("Service Table");
}
}
之前的各种行为,现在就变成了相互独立的模块,剩下的事情就是组合了:
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
abstract class Order {
protected OrderAction orderAction;
protected ServiceAction serviceAction;
public void order() {
orderAction.order();
}
public void package() {
serviceAction.service();
}
}
class DineInOrder extends Order {
DineInOrder() {
this.orderAction = new OrderInRestaurant();
this.serviceAction = new TableService();
}
}
class PickUpOrder extends Order {
PickUpOrder() {
this.orderAction = new OrderByPhone();
this.serviceAction = new NormalPackage();
}
}
class DeliveryOrder extends Order {
DeliveryOrder() {
this.orderAction = new OrderThroughPlatform();
this.serviceAction = new NormalPackage();
}
}
我们通过组合的方式构建了整个依赖关系,变化的部分被提取出来,不变统一的业务逻辑放在了抽象的父类 Order
中。
策略模式解析
下图可以很好地总结概括上面的样例:
你可能觉得好奇,策略模式究竟好在哪呢?这看上去也不比最开始的代码简洁呀,多引入了接口和变量不说,还增加了许多关联。
单从例子来看,确实如此,但是在更为复杂的情况下,又会是另一回事,而且也不能拿代码的多少来衡量设计的好坏。这里我们可以从下面三个方面来看:
- 变量分离:一个模块中有些业务逻辑会经常变动,将容易变化的部分提取出来单独封装和管理,这样后期的改动和拓展就只会针对这些变化的部分,其他部分不会受影响,减小了出错的概率,增加了代码的可维护性。比如这里的
order
和service
方法,这些方法会根据子类的不同而有所不同,想象一下,如果Order
类是一个非常大的类的话,下面有诸多子类,每个子类都有着自己的order
和service
,那么如果我们想对某些order
和service
的逻辑进行更改,那么我们是不是需要把所有的子类都过一遍?另外,这里面也有重复的代码,比如NormalPackage
中的service
就出现在不同的Order
中。我们通过策略模式,将变量分离的同时也达到了之前提到的 去重 和 解耦。 - 依赖于接口和抽象,而不是具体实现(Program to an ‘interface’, not an ‘implementation’):这也是 SOLID 原则中的依赖倒置原则(Dependence Inversion Principle)。依赖于具体实现的话,你的代码就被限定只能去使用已经存在的、特定的功能,没法很好地拥抱变化。而依赖于抽象,就不存在这方面的问题,在例子中我们创建了
OrderAction
和ServiceAction
接口。在Order
类中使用这两个接口时我们不需要知道具体实现是什么,我们只知道实现了这个接口的对象必定存在对应的方法,我们直接调用方法即可。当有新的方法需要添加,只需要创建并实现这两个接口即可。只要接口不变,这里的逻辑均不会被更改。 - 尽量使用组合,而不是使用继承(Favor ‘object composition’ over ‘class inheritance’):面向对象中存在两种关系——组合(HAS-A) 和 继承(IS-A),继承是先天的,发生在编译阶段,而组合是后天的,发生在运行时。组合的成本比较低,一个对象中只需要保存另一个对象的引用即可,而继承的成本高,尤其是多继承,子类需要把父类的很多行为再拷贝一份。很明显,如果组合能解决问题,优先考虑组合,它能让我们在类中添加多个功能,比如这里的
OrderAction
和ServiceAction
。这样做成本低不说,因为是对象构建时传入的,我们还可以在运行时依照具体情况对其更改。
策略模式就是如上所述,定义一组方法,对它们进行封装,并保障它们之间可以相互替换。这样这些算法之间相互独立,并且客户端使用的时候也可以动态地进行挑选。