
面向对象和设计模式(十九) 模板模式 和 策略模式——如何优雅的在代码中留下扩展点
01 模板模式
模板模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”。
下面是模板模式的一个示例,templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为
abstract,是为了强迫子类去实现。不过,这些都不是必须的,你也可以将method1和method2定义为是一个空方法,当子类真正需要的时候才重写它们。
<?php
abstract class AbstractClass { public final function templateMethod() { //... $this->method1(); //... $this->method2(); //... } protected abstract function method1(); protected abstract function method2();}
class ConcreteClass1 extends AbstractClass { protected function method1() { //... } protected function method2() { //... }}
class ConcreteClass2 extends AbstractClass { protected function method1() { //... } protected function method2() { //... }}
$demo = new ConcreteClass1();$demo->templateMethod();
模板模式的作用
模板模式有两大作用:复用和扩展。
模板模式的第一个作用:复用。模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分
method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2
来实现。所有的子类都可以复用父类templateMethod() 中的流程代码。
模板模式的第二个作用:是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似之前章节讲到的控制反转。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。
模板模式是基于继承来实现的。
02 策略模式
策略模式会定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指调用算法的代码)。
最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点。
工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式解耦的是策略的定义、创建、使用这三部分。
策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。
interface Strategy { public function algorithmInterface();}
class ConcreteStrategyA implements Strategy { public function algorithmInterface() { //具体的算法… }}
class ConcreteStrategyB implements Strategy { public function algorithmInterface() { //具体的算法… }}
因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,也就是说,上层调用使用不同的策略对象的方式是统一规范的,可以轻松地替换不同的策略。
策略的创建
因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们可以把根据 type
创建策略对象的逻辑抽离出来,放到工厂类中。
<?php
class StrategyFactory { private static $strategies = [];
public static function getStrategy($type) { if(!empty(self::$strategies[$type])){ return self::$strategies[$type]; }
if (empty($type)) { throw new \Exception("type should not be empty."); }
switch($type){ case 'A': $stratety = new ConcreteStrategyA(); break; case 'B': $stratety = new ConcreteStrategyB(); break; } self::$strategies[$type] = $stratety; return $stratety; }}
如果我们希望每次从工厂方法中,获得的都是新创建的策略对象,也可以不使用缓存容器self::$strategies[$type]。
当然,在这个代码示例中,switch case并没有真的避免,而是将业务代码里if else/switch case
转移到了策略工厂类中。一般来说这是可以接受的,因为具体的逻辑都被封装到策略了里面了。
不过如果真的想要在策略工厂类中也彻底避免switch case/if else,我们可以创建一个静态的"类型-
策略类"的映射表。在实例化策略对象的时候,直接查表获取策略类并实例化策略对象。
<?php
class StrategyFactory { private static $strategyMap = [ 'A' => ConcreteStrategyA::class, 'B' => ConcreteStrategyB::class, ];
private static $strategies = [];
public static function getStrategy($type) { if(!empty(self::$strategies[$type])){ return self::$strategies[$type]; }
if (empty($type)) { throw new \Exception("type should not be empty."); }
$class = self::$strategyMap[$type]; $stratety = new $class(); self::$strategies[$type] = $stratety; return $stratety; }}
如果策略的实例化需要传参,而且每个策略类构造函数的传参个数、类型不一样,甚至于有些参数本身也是对象。此时我们有两种解决方法:一个是使用IOC容器+反射类获取该对象,一种是将实例化策略类的逻辑放到一个方法中,然后映射表存放
“类型-策略工厂的实例化策略对象的方法名”来解决,如下所示。
<?php
class StrategyFactory { private static $strategyMap = [ 'A' => 'buildStrategyA', 'B' => 'buildStrategyB', ];
private static $strategies = [];
public static function getStrategy($type, $params=[]) { if(!empty(self::$strategies[$type])){ return self::$strategies[$type]; }
if (empty($type)) { throw new \Exception("type should not be empty."); }
$buildMethod = self::$strategyMap[$type]; self::$strategies[$type] = call_user_func_array([static, $buildMethod], [$params]); return $stratety; }
public static function buildStrategyA($params=[]){ $strategyA = new ConcreteStrategyA(...); $strategyA->setXXX($params[...]); return $strategyA; }
public static function buildStrategyB($params=[]){ // ... }}
策略的使用
策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?
最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。
<?php
// 策略接口:EvictionStrategy// 具体策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy…// 策略工厂:EvictionStrategyFactory
class UserCache { private $cacheData = []; private $eviction;
public function __constract(EvictionStrategy $eviction) { $this->eviction = $eviction; }
//…}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略class Application { public static function main(){ $evictionStrategy = null; $evictionType = Cache::get(‘user’, “eviction_type”); $evictionStrategy = EvictionStrategyFactory::getEvictionStrategy($evictionType); $userCache = new UserCache($evictionStrategy); //… }}
// 非运行时动态确定,在代码中指定使用哪种策略class Application { public static function main() { //… $evictionStrategy = new LruEvictionStrategy(); $userCache = new UserCache($evictionStrategy); //… }}
从写法上来看,策略模式就是工厂方法模式的一种变体,或者说工厂模式是策略模式的一个手段和工具。策略模式的目标是为上层业务更优雅的提供多种策略逻辑,而工厂模式在策略模式中发挥的作用是将策略对象的创建和使用解耦开来。
如果你的策略逻辑不过于复杂,甚至可以不使用策略对象,而是直接将策略逻辑写到策略工厂类的方法中,如下面的代码所示。
<?php
class StrategyFactory { private static $strategyMap = [ 'A' => 'strategyA', 'B' => 'strategyB', ];
// 直接运行策略,而不生成策略对象
public static function doStrategy($type, $params=[]) {
if (empty($type)) { throw new \Exception(“type should not be empty.”); }
$buildMethod = self::$strategyMap[$type]; return call_user_func_array([static, $buildMethod], [$params]);
}
public static function strategyA(…){ // do something you want }
public static function strategyB(…){ // do something you want }}
不过必须将 strategyA 和 strategyB 的传参个数和格式规范成一致的。