设计模式之: 策略模式
什么是策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
什么时候使用策略模式
1、 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
2、 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
3、 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
区分策略模式与状态设计模式
状态模式中会变化的因素是状态, 维护子类当前状态的一个实例(定义了当前状态)
策略模式中会变化的因素是算法, 配置为具体策略对象, 这是一个封装的算法.
在状态模式中, 具体实现Context中会包含一个变量来保存当前的具体状态, 这个具体状态提供了一些方法, 可以从Context变量中记录的当前状态变迁到另一个状态. 而策略模式中的Context参与者并没有记录当前使用的策略, 这是因为, 与不断改变状态不同, 一般来讲, 改变的算法并不依赖于当前正在使用的算法.显然, 有些情况下, 执行一个算法之前可能首先要使用另一个算法, 如试图访问一个表中的数据之前, 需要先在表中插入数据, 不过这并不妨碍使用算法尝试从一个空表获得数据.但是在状态模式中很容易出现这样的一种情况: 即一个状态只能进入某些状态, 而不能转移到另外一些状态.
简单示例
这个例子会使用多个php触发器脚本, 这些触发器脚本调用客户的不同方法, 客户再通过上下文调用所请求的具体策略.
对于每个策略, HTML文件中分别包含相应的表单, 表单数据通过一个PHP触发器脚本传递到Client中的方法. Client再通过Context进一步将请求传递到一个具体策略. 连接辅助类包括一个接口和类, 可以用来连接一个MySQL数据库.
Client.php
<?php class Client { public function insertData() { $context = new Context(new DataEntry()); $context->algorithm(); } public function findData() { $context = new Context(new SearchData()); $context->algorithm(); } public function showAll() { $context = new Context(new DisplayData()); $context->algorithm(); } public function changeData() { $context = new Context(new UpdateData()); $context->algorithm(); } public function killer() { $context = new Context(new DeleteRecord()); $context->algorithm(); } }
为了触发不同具体策略(封装的算法)的方法, HTML会调用以下一个PHP触发器脚本.
insertTrigger.php
<?php function __autoload($class_name) { include $class_name . '.php'; } $trigger = new Client(); $trigger->insertData();
displayTrigger.php
<?php function __autoload($class_name) { include $class_name . '.php'; } $trigger = new Client(); $trigger->showAll();
findTrigger.php
<?php function __autoload($class_name) { include $class_name . '.php'; } $trigger = new Client(); $trigger->findData();
updateTrigger.php
<?php function __autoload($class_name) { include $class_name . '.php'; } $trigger = new Client(); $trigger->changeData();
killTrigger.php
<?php function __autoload($class_name) { include $class_name . '.php'; } $trigger = new Client(); $trigger->killer();
Context类和Strategy接口
在状态模式设计中, Context相当于一个"跟踪者"(track keeper), 它会跟踪当前的状态.
在策略设计模式中, Context则有完全不同的功能, 用于将请求与具体策略分离, 使策略和请求可以独立地工作.这体现了请求与后果之间的一种松绑定.与此同时, 它还有利于从Client发出请求.
Context不是一个接口, 不过它与Strategy接口有聚合关系. "四人帮"指定了以下特征:
- 用一个具体策略对象来配置
- 维护Strategy对象的一个引用
- 可以定义一个接口, 允许Strategy访问其数据.
在下面的Context代码中, 可以看到上述特征:
Context.php
<?php class Context { private $strategy; public function __construct(IStrategy $strategy) { $this->strategy = $strategy; } public function algorithm() { $this->strategy->algorithm(); } }
对于以上三个特征
第一,构造函数希望有一个IStrategy实现作为参数.
第二,通过一个封装的属性$strategy(可见性为私有)来维护Strategy对象的一个引用.$strategy属性从构造函数参数接收Strategy对象实例, 这将成为一个具体策略实例.
第三,algorithm()方法实现了IStrategy的algorithm()方法, 实现为通过Client选择的具体策略.由于 Context和IStrategy构成一个聚合关系, 所以Context具有抽象类或接口的某些特征.实际上, 最好通过聚合来理解Context.查看策略接口IStrategy时, 可以看到要实现的方法是algorithm():
IStrategy.php
<?php interface IStrategy { public function algorithm(); }
具体策略
构成具体策略的封装算法族提供了所有可能的策略. 对于这个例子, 关键是要了解策略设计模式中不同的参与者如何协同工作.
具体的策略有如下几个,这些具体策略分别表示结合使用PHP和MySQL的典型算法.
DataEntry.php
<?php class DataEntry implements IStrategy { public function algorithm() { $hookup = UniversalConnect::doConnect(); $test = $hookup->real_escape_string($_POST['data']); echo "该数据已经输入: " . $test .'<br />'; } }
DisplayData.php
<?php class Display implements IStrategy { public function algorithm() { $test = "这里是所有的数据"; echo $test .'<br />'; } }
SearchData.php
<?php class SearchData implements IStrategy { public function algorithm() { $hookup = UniversalConnect::doConnect(); $test = $hookup->real_escape_string($_POST['data']); echo "这是你要查询的数据: " . $test .'<br />'; } }
UpdateData.php
<?php class UpdateData implements IStrategy { public function algorithm() { $hookup = UniversalConnect::doConnect(); $test = $hookup->real_escape_string($_POST['data']); echo "新的数据是: " . $test .'<br />'; } }
DeleteData.php
<?php class DeleteData implements IStrategy { public function algorithm() { $hookup = UniversalConnect::doConnect(); $test = $hookup->real_escape_string($_POST['data']); echo "该记录: " . $test .'已经被删除<br />'; } }
增加数据安全性和参数化算法来扩展策略模式
下面这个例子会为不同策略增加功能, 还增加一个辅助类, 来处理数据从HTML客户到MySQL数据库的安全移动.Client类可以自己处理安全性, 不过这样一来, 除了做出请求外, 会为Client类增加额外的责任.
增加一个SecureData类来辅助创建MySQL连接
SecureData.php
<?php class SecureData { private $changeField; private $company; private $devdes; private $device; private $disappear; private $field; private $hookup; private $lang; private $newData; private $oldData; private $plat; private $style; private $term; private $dataPack; public function enterData() { $this->hookup=UniversalConnect::doConnect(); $this->company = $this->hookup->real_escape_string($_POST['company']); $this->devdes = $this->hookup->real_escape_string($_POST['devdes']); $this->lang = $this->hookup->real_escape_string($_POST['lang']); $this->plat = $this->hookup->real_escape_string($_POST['plat']); $this->style = $this->hookup->real_escape_string($_POST['style']); $this->device = $this->hookup->real_escape_string($_POST['device']); $this->dataPack = array( $this->company, $this->devdes, $this->lang, $this->plat, $this->style, $this->device ); $this->hookup->close(); } public function conductSearch() { $this->hookup=UniversalConnect::doConnect(); $this->field = $this->hookup->real_escape_string($_POST['field']); $this->term = $this->hookup->real_escape_string($_POST['term']); $this->dataPack = array( $this->field, $this->term ); $this->hookup->close(); } public function makeChange() { $this->hookup=UniversalConnect::doConnect(); $this->changeField = $this->hookup->real_escape_string($_POST['update']); $this->oldData = $this->hookup->real_escape_string($_POST['old']); $this->newData = $this->hookup->real_escape_string($_POST['new']); $this->dataPack = array( $this->changeField, $this->oldData, $this->newData ); $this->hookup->close(); } public function removeRecord() { $this->hookup=UniversalConnect::doConnect(); $this->disappear = $this->hookup->real_escape_string($_POST['delete']); $this->dataPack = array($this->disappear); $this->hookup->close(); } public function setEntry() { return $this->dataPack; } }
除了setEntry()之外, 所有方法都生成一个名为dataPack的数组.setEntry()方法会返回这个dataPack数组的当前内容.取决于具体的请求, SecureData类生成将置于数组中的值, 这会传回到Client, 并通过algorithm()方法作为请求的一部分发送到一个具体策略.
为算法方法增加参数
第二个要增加的特性是修改Strategy算法方法. 我们将增加一个数组作为函数的一个参数,这样可以增加灵活性, 处理更多的内容. 每个算法函数调用都包含一个数组, 其中包含从HTML表单传递的数据:
IStrategy.php
<?php interface IStrategy { const TABLENOW = "survey"; public function algorithm(Array $dataPack); }
同样, 还要为接口增加一个常量TABLENOW. 由于这个实现中各个具体策略都使用相同的表, 而且PHP能够通过接口传递常量. 因此可以建立一个松耦合而且可重用的代码. 显然, 如果不同的具体策略要使用不同的表, 就必须在各个具体策略中指定表引用. 参数中的类型提示要将数组用作一个实参
数据输入模块
利用SecureData辅助类和修改后的IStrategy接口(可以为algorithm()方法包含一个参数),对于不同的HTML表单请求, Client可以根据相应的方法更容易地做出请求.
客户请求帮助
Client.php
<?php class Client { public function insertData() { $secure = new SecureData(); $context = new Context(new DataEntry()); $secure->enterData(); $context->algorithm($secure->setEntry()); } public function findData() { $secure = new SecureData(); $context = new Context(new SearchData()); $secure->conductSearch(); $context->algorithm($secure->setEntry()); } public function showAll() { $dummy = array(0); $context = new Context(new DisplayData()); $context->algorithm($dummy); } public function changeData() { $secure = new SecureData(); $context = new Context(new UpdateData()); $secure->makeChange(); $context->algorithm($secure->setEntry()); } public function killer() { $secure = new SecureData(); $context = new Context(new DeleteRecord()); $secure->removeRecord(); $context->algorithm(); } }
除了showAll()方法外, Client中的所有方法都会首先实例化SecureData类,然后使用具体方法作为参数来创建一个上下文对象.接下来, SecureData对象调用具体策略的相应方法创建所需的数组.最后Client方法调用Context->algorithm(),并使用SecureData类返回$secure->setEntry()数组作为参数.数组内容取决于HTML表单发送的用户输入以及所请求的策略类型.
Context类重要的小改变
Context类几乎没有改动, 只是为algorithm()方法增加了一个参数, 这是更新后的IStrategy接口提出的要求.由于Context类和IStrategy之间有一种聚合关系, Context类必须包含IStrategy的一个引用.类似于前面的例子,同样要用一个具体策略对象创建Context.
Context.php
<?php class Context { private $strategy; private $dataPack; public function __construct(IStrategy $strategy) { $this->strategy = $strategy; } public function algorithm(Array $dataPack) { $this->dataPack = $dataPack; $this->strategy->algorithm($this->dataPack); } }
具体策略
通过一个数组向具体策略传递数据的根本目的是允许不同的策略对不同的请求做出响应.这样可以为设计提供灵活性, 因为利用数组可以传递大量数据.
DataEntry.php
<?php class DataEntry implements IStrategy { private $tableMaster; private $dataPack; private $hookup; private $sql; public function algorithm(Array $dataPack) { $this->dataPack = $dataPack; $comval = $this->dataPack[0]; $devdesval = $this->dataPack[1]; $langval = $this->dataPack[2]; $platval = $this->dataPack[3]; $cstyleval = $this->dataPack[4]; $deviceval = $this->dataPack[5]; $this->tableMaster = IStrategy::TABLENOW; $this->hookup = UniversalConnect::doConnect(); $this->sql = "INSERT INTO $this->tableMaster ( company, devdes, lang, plat, style, device ) VALUES ( '$comval', '$devdesval', '$langval', '$platval', '$cstyleval', '$deviceval' )"; if($this->hookup->query($this->sql)) { printf("成功插入数据到表:$this->tableMaster<br />"); } else { printf("无效的SQL: %s <br /> 语句是: %s<br />",$this->hookup->error,$this->sql); exit; } $this->hookup->close(); } }
DisplayData.php
<?php class DisplayData implements IStrategy { private $tableMaster; private $hookup; public function algorithm(Array $dataPack) { $this->tableMaster = IStrategy::TABLENOW; $this->hookup = UniversalConnect::doConnect(); $sql = "SELECT * FROM $this->tableMaster"; if($result = $this->hookup->query($sql)) { printf("查询返回%d条记录<br />",$result->num_rows); echo "<table>"; while($finfo = mysqli_fetch_field($result)) { echo "<th>{$finfo->name}</th>"; } while($row = mysqli_fetch_row($result)) { echo "<tr>"; foreach($row as $ceil) { echo "<td>$ceil</td>"; } echo "</tr>"; } echo "</table>"; $result->close(); } } }
SearchData.php
<?php class SearchData implements IStrategy { private $tableMaster; private $dataPack; private $hookup; private $sql; public function algorithm(Array $dataPack) { $this->tableMaster = IStrategy::TABLENOW; $this->hookup = UniversalConnect::doConnect(); $this->dataPack = $dataPack; $field = $this->dataPack[0]; $term = $this->dataPack[1]; $this->sql = "SELECT * FROM $this->tableMaster WHERE $field='$term'"; if($result = $this->hookup->query($this->sql)) { echo "<table>"; while($finfo = mysqli_fetch_field($result)) { echo "<th>{$finfo->name}</th>"; } while($row = mysqli_fetch_row($result)) { echo "<tr>"; foreach($row as $ceil) { echo "<td>$ceil</td>"; } echo "</tr>"; } echo "</table>"; $result->close(); } $this->hookup->close(); } }
UpdateData.php
<?php class UpdateData implements IStrategy { private $tableMaster; private $dataPack; private $hookup; private $sql; public function algorithm(Array $dataPack) { $this->tableMaster = IStrategy::TABLENOW; $this->hookup = UniversalConnect::doConnect(); $this->dataPack = $dataPack; $channelfield = $this->dataPack[0]; $oldData = $this->dataPack[1]; $newData = $this->dataPack[2]; $this->sql = "UPDATE $this->tableMaster SET changeField='$newData' WHERE changeField='$oldData'"; if($result = $this->hookup->query($this->sql)) { echo "$channelfield 从 $oldData 变到 $newData"; } else { echo 'Change faield: '.$this->hookup->error; } } }
DeleteData.php
<?php class DeleteData implements IStrategy { private $tableMaster; private $dataPack; private $hookup; private $sql; public function algorithm(Array $dataPack) { $this->tableMaster = IStrategy::TABLENOW; $this->hookup = UniversalConnect::doConnect(); $this->dataPack = $dataPack; $field = $this->dataPack[0]; $term = $this->dataPack[1]; $this->sql = "SELECT *FROM $this->tableMaster WHERE $field='$term'"; if($result = $this->hookup->query($this->sql)) { echo "表$this->tableMaster 记录已经被删除"; } else { echo '删除失败'.$this->error; } } }
灵活的策略模式
策略模式很灵活,改变算法时可以只改变一个实现,不仅如此,模式本身还可以有多个实现. 一方面,它可以调用不同的算法这些算法独立于具体策略之外的数据; 另一方面,可以向具体策略传递安全的数据.
特定的策略模式实现依赖于特定算法的需求以及它具体需要些什么. 一些策略模式实现会存储其上下文的一个引用,因此没有必要传递数据. 不过这样一来,Context和Strategy会更紧密地耦合.
还有一个问题来考虑: 策略模式所生成的对象(具体策略)个数.在上面的例子可以看到, 它们都构建了大量的对象(类)来处理一个简单MySQL的不同请求.还有可能构建更多对象. 不过, 相对于重用性以及改变模式所带来的好处, 这可能不算太大的问题.构建设计模式是为了提高一个应用的速度, 而不是为了提高执行代码的速度. 如果采用了良构的策略模式, 开发人员可以很容易地优化和重新优化封装的算法, 而不会搞得一团乱麻. 所以速度表面在重用和修改时间上,而且额外对象的开销很小.