Template Method Pattern 和 Strategy Pattern 是两种常用的行为设计模式。他们分别用了继承inheritance和委托delegation两种不同的实现方法,因为上篇文章讲过了UML图,所以这篇顺便可以把两种不同模式的UML图都带出来一起说明。
Template Method Pattern
模板方法模式(Template Method Pattern)定义了算法的骨架,我们通过继承inheritance的方式在子类(subtypes)中写具体的实现细节。这种方法允许我们在不修改模板方法的情况下,通过子类来改变算法的某些部分。
Behavioral Subtyping
在讲继承之前,首先要提到一个概念Behavioral subtyping ,这是面向对象编程中一个重要的原则,它提供了一种更正式的方法来确定何时应该使用继承(inheritance)和扩展(extension)。Behavioral subtyping 的一个核心观点是:当使用子类型替换父类型时,客户端代码不应受到影响。也就是说,子类型应该能够在语法和行为上满足其父类型的要求。这种思想与里氏替换原则(Liskov Substitution Principle, LSP)密切相关。实现这一观点的三个重要条件如下:
-
Same or stronger invariants than super class: 子类型应具有与父类型相同或更强的不变式。不变式是类的属性在整个对象生命周期中需要满足的条件。这意味着子类型需要保持和父类型一致的属性约束,或者增加更严格的约束。这有助于确保子类型对象在行为上与父类型兼容,同时满足更特定的需求。这一点有些抽象,我们举个例子:比如有一个父类叫做
Bird
,其中有一个属性叫做speed
。Bird
类的不变式可能是速度永远不为负数,即speed >= 0
。现在,我们创建一个子类Penguin
来扩展Bird
类。在这种情况下,Penguin
类需要满足与Bird
类相同或更强的不变式。这意味着Penguin
类也需要确保速度永远不为负数(相同的不变式),或者可以增加更严格的约束,例如速度上限(更强的不变式),如0 <= speed <= 10
。 -
Same or weaker preconditions for all methods in super class: 子类型的方法应具有与父类型相同或更弱的前置条件。前置条件是方法调用之前需要满足的条件,通常涉及方法输入参数的约束。这意味着子类型的方法应该对输入参数的限制更宽松或与父类型相同。这样,在使用子类型替换父类型时,原有的调用不会因为输入限制而出现错误。
-
Same or stronger postconditions for all methods in super class: 子类型的方法应具有与父类型相同或更强的后置条件。后置条件是方法调用之后必须满足的条件,通常涉及方法输出结果的约束。这意味着子类型方法的输出结果应该满足父类型的要求或具有更强的保证。这样,当子类替换父类时,程序依赖于父类的输出结果的正确性不会受到影响。
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < old(speed)
void brake() { … }
}
class Hybrid extends Car {
int charge;
//@ invariant charge >= 0;
//@ requires (charge > 0 || fuel > 0)
&&
!engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
//@ ensures charge > \old(charge)
void brake() { … }
在上面的例子中,Hybrid 就是 Car的behavioral subtyping。在这个例子中,Car 类有一个不变式://@ invariant fuel >= 0;
,表示汽车的燃料量不能为负数。Hybrid 类作为 Car 类的子类,也应当满足这个不变式。同时,Hybrid 类有一个额外的不变式://@ invariant charge >= 0;
,表示混合动力汽车的电量不能为负数。因此,Hybrid 类作为子类,满足了 Car 类的不变式(关于燃料量的限制)以及它自己的额外不变式(关于电量的限制)。这就是第一个条件。
对于另外两个条件,我们可以很轻易地知道它满足被覆写的方法 start 具有较弱的前置条件,以及被覆写的方法 brake 具有较强的后置条件。这两个条件能够让我们以后在任何地方把父类替换为子类都不会让程序出错。
一般满足behavioral subtyping时,我们就可以用template method pattern,下面template method pattern的UML图以及实例代码:
这里UML中的空心实线箭头表示的是继承关系,我们通过具体的类class1和class2来实现抽象类并实现在抽象类中定义好的功能。
abstract class AbstractOrder {
public abstract boolean lessThan(int i, int j);
}
class AscendingOrder extends AbstractOrder {
public boolean lessThan(int i, int j) {
return i < j;
}
}
class DescendingOrder extends AbstractOrder {
public boolean lessThan(int i, int j) {
return i > j;
}
}
// ...
static void sort(int[] list, AbstractOrder order) {
// ...
boolean mustSwap = order.lessThan(list[j], list[i]);
// ...
}
上面是一个用template method pattern实现的扩展排序方式的代码示例。我们使用了一个抽象基类 AbstractOrder
。AscendingOrder
和 DescendingOrder
分别继承自 AbstractOrder
类,并覆写了其中的 lessThan
方法。通过继承,AscendingOrder
和 DescendingOrder
类隐式地获得了 AbstractOrder
类的所有方法和属性。这里,我们主要关注的是类型层次结构和代码重用。
Strategy Pattern
策略模式(Strategy Pattern)是一种行为设计模式,它允许在运行时根据需要切换不同的算法或策略。策略模式是通过委托delegation实线的,将算法的实现与使用算法的对象分离,从而提高了代码的灵活性和可扩展性。一般来说,能用template method pattern实现的,我们都可以用strategy pattern来实现,并且往往有更多的好处。
在strategy pattern中,我们会把策略抽象出一个接口来,然后用具体不同的策略去实现这个策略接口,在这里空心箭头加虚线表示类之间的实现关系,这里要和上面实线表示的继承关系做区分。在strategy pattern中,我们需要用哪个策略,就直接把哪个策略的具体对象放进Navigator里使用,我们使用了routeStrategy类来定义了这个策略,通过Java的dynamic dispatch可以自由地将指针指向不同的具体策略。
上图是strategy pattern的interaction diagram,系统基于不同的策略去调用不同具体策略对象中的方法。这样做的好处是可以轻松地将策略引入到系统中,而无需修改客户端代码。策略模式允许将算法的实现与使用它们的客户端代码分离,提高了代码的可维护性和灵活性。
一个strategy pattern的代码例子:
interface Order {
boolean lessThan(int i, int j);
}
class AscendingOrder implements Order {
public boolean lessThan(int i, int j) { return i < j; }
}
class DescendingOrder implements Order {
public boolean lessThan(int i, int j) { return i > j; }
}
…
static void sort(int[] list, Order order) {
…
boolean mustSwap =
order.lessThan(list[j], list[i]);
…
}
其实看起来这个代码和上面的很像,但在委托的写法中,我们使用了一个接口 Order
。AscendingOrder
和 DescendingOrder
分别实现了 Order
接口。然后我们直接把要使用的策略(AscendingOrder或DescendingOrder
)传给sort中的Order类型的变量,这样我们就以非常灵活且低耦合的方式实现了策略的自由切换。相当于我们把排序的具体算法“委托”给了这个具体的order对象,直接把人家叫过来使用,而不在乎他们具体的实现细节。在delegation中,我们关注的是定义一组行为(通过 Order
接口)并将这些行为分别实现。委托强调的是行为和组合,而不是类型层次结构。
关于这两种方法的总结:
继承(Inheritance)和组合+委托(Composition + Delegation)都是面向对象设计中的关键概念。继承在强耦合关系中可以实现大量代码重用,但使用时应谨慎。而良好的设计通常更倾向于使用组合和委托,因为它们支持编程接口的重用和封装,有助于信息隐藏,并产生更易于测试的代码(而使用继承的话因为需要重写父类的代码, 往往需要知道父类的具体实现信息,不利于信息隐藏)。虽然继承在某些情况下用起来更顺手,但在设计时应优先考虑delegation。