亚当·斯密曾就制针业做过一个分工产生效率的例子©。对于一个没有受过相应训练,又不知道怎样使用这种职业机械的工人来讲,即使他竭尽全力地工作,也许一天连一根针也生产不出来,当然更生产不出20根针了。但是,如果把这个行业分成各种专门的组织,再把这种组织分成许多个部门,其中大部分部门也同样分为专门的组织。把制针分为18种不同工序,这18种不同操作由18个不同工人来担任。那么,尽管他们的机器设备都很差,但他们尽力工作,一天也能生产12磅针。每磅中等型号针有4000根,按这个数字计算,十多个人每天就可以制造48000根针,而每个人每天能制造4800根针。如果他们各自独立地工作,谁也不专学做一种专门的业务,那么他们之中无论是谁都绝不可能一天制造20根针,也许连1根针也制造不出来。这就是企业管理中的分工,在面向对象的设计里,叫做单一职责原则(Single Pesponsibility Principle,SRP)。
在《敏捷软件开发》中,把“职责”定义为“变化的原因”,也就是说,就一个类而言,应该只有一个引起它变化的原因。这是一个最简单,最容易理解却最不容易做到的一个设计原则。说得简单一点,就是怎样设计类以及类的方法界定的问题。这种问题是很普遍的,比如在MVC的框架中,很多人会有这样的疑惑,对于表单插入数据库字段过滤与安全检查应该是放在control层处理还是model层处理,这类问题都可以归到单一职责的范围。
再比如在职员类里,将工程师、销售人员、销售经理等都放在职员类里考虑,其结果将会非常混乱。在这个假设下,职员类里的每个方法都要用if…else判断是哪种情况,从类结构上来说将会十分臃肿,并且上述三种职员类型,不论哪一种发生需求变化,都会改变职员类,这是我们所不愿意看到的!
从上面的描述中应该能看出,单一职责有两个含义:一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多职责。
那为什么要遵守SRP呢?
(1)可以减少类之间的耦合
如果减少类之间的耦合,当需求变化时,只修改一个类,从而也就隔离了变化;如果一个类有多个不同职责,它们耦合在一起,当一个职责发生变化时,可能会影响其他职责。
(2)提高类的复用性
修理电脑比修理电视机简单多了。主要原因就在于电视机各个部件之间的耦合性太高,而电脑则不同,电脑的内存、硬盘、声卡、网卡、键盘灯部件都可以很容易地单独拆卸和组装。某个部件坏了,换上新的即可。
上面的例子就体现了单一职责的优势。由于使用了单一职责,使得“组件”可以方便地“拆卸”和“组装”。
不遵守SRP会影响对该类的复用性。当只需要复用该类的某一个职责时,由于它和其他的职责耦合在一起,也就很难分离出。
遵守SRP在实际代码开发中有没有什么应用?有的。以数据持久层为例,所谓的数据持久层主要指的是数据库操作,当然,还包括缓存管理等。以数据库操作为例,如果是一个复杂的系统,那么就可能涉及多种数据库的相互读写等,这时就需要数据持久层支持多种数据库。应该怎么做?定义多个数据库操作类?你的想法已经很接近了,再进一步,就是使用工厂模式。
工厂模式(Factory)允许你在代码执行时实例化对象。它之所以被称为工厂模式是因为它负责“生产”对象。以数据库为例,工厂需要的就是根据不同的参数,生成不同的实例化对象。最简单的工厂就是根据传入的类型名实例化对象,如传入MySQL,就调用MySQL的类并实例化,如果是SQLite,则调用SQLite的类并实例化,甚至可以处理TXT、Excel等“类数据库”。工厂类也就是这样的一个类,它只负责生产对象,而不负责对象的具体内容。
先定义一个接口,规定一些通用的方法,如代码清单2-1所示。
代码清单2-1 定义一个适配器接口
<©php
interface Db_Adapter{
/**
*数据库连接
*@param $config 数据库配置
*@return resource
*/
public function connect($config);
/**
*执行数据库查询
*@param string $query 数据库查询SQL字符串
*@param mixed $handle 连接对象
*@return resource
*/
public function query($query, $handle);
}
©>
这是一个简化的接口,并没有提供所有方法,其定义了MySQL数据库的操作类,这个类实现了Db_Adapter接口,具体如代码清单2-2所示。
代码清单2-2 定义MySQL数据库的操作类
<©php
class Db_Adapter_Mysql implements Db_Adapter
{
private $_dbLink;// 数据库连接字符串标示
/**
*数据库连接函数
*
*@param $config 数据库配置
*@throws Db_Exception
*@return resource
*/
public function connect($config)
{
if ($this->_dbLink = @mysql_connect($config->host .
(empty($config->port) © '' : ':' .$config->port),
$config->user, $config->password, true)) {
if (@mysql_select_db($config->database, $this->_dbLink)) {
if ($config->charset) {
mysql_query("SET NAMES '{$config->charset}'", $this->_dbLink);
}
return $this->_dbLink;
}
}
/**数据库异常 */
throw new Db_Exception(@mysql_error($this->_dbLink));
}
/**
*执行数据库查询
*
*@param string $query 数据库查询SQL字符串
*@param mixed $handle 连接对象
*@return resource
*/
public function query($query, $handle)
{
if ($resource = @mysql_query($query, $handle)) {
return $resource;
}
}
}
©>
接下来是SQLite数据库的操作类,同样实现了Db_Adapter接口,如代码清单2-3所示。
代码清单2-3 SQLite数据库的操作类
<©php
class Db_Adapter_sqlite implements Db_Adapter
{
private $_dbLink;// 数据库连接字符串标示
/**
*数据库连接函数
*@param $config 数据库配置
*@throws Db_Exception
*@return resource
*/
public function connect($config)
{
if ($this->_dblink = sqlite_open($config->file, 0666, $error)) {
return $this->_dblink;
}
/** 数据库异常 */
throw new Db_Exception($error);
}
/**
*执行数据库查询
*@param string $query 数据库查询SQL字符串
*@param mixed $handle 连接对象
*@return resource
*/
public function query($query, $handle)
{
if ($resource = @sqlite_query($query, $handle)) {
return $resource;
}
}
}
好了,如果现在需要一个数据库操作的方法的话怎么做?只需定义一个工厂类,根据传入不同的参数生成需要的类即可,如代码清单2-4所示。
代码清单2-4 定义一个工厂类
<©php
class sqlFactory
{
public static function factory($type)
{
if (include_once 'Drivers/' .$type .'.php') {
$classname = 'Db_Adapter_' .$type;
return new $classname;
} else {
throw new Exception ('Driver not found');
}
}
}
©>
要调用时,就可以这么写:
$db =sqlFactory::factory('MySQL');
$db= sqlFactory::factory('SQLite');
我们把创建数据库连接这块程序单独拿出来,程序中的CURD就不用关心是什么数据库了,只要按照规范使用对应的方法即可。
工厂方法让具体的对象解脱了出来,使其并不再依赖具体的类,而是抽象。除了数据库操作这种显而易见的设计外,还有什么地方会用到工厂类呢?那就是SNS中的动态实现。
下面的图片来自国内某SNS网站,属于当前新鲜事页面,可以看到针对不同行为,其生成了不同动态。比如,参加了某个小组,动态显示的就是“XX参加了YY小组”;收到某某的礼物,别人看到的多台就是“XX收到了YY的ZZ礼物”,如图2-1所示。
以上这种动态应该怎么设计呢,最容易想到的就是用工厂模式,根据传入的操作不同,结合模板而生成不同的动态,如代码清单2-5所示。
代码清单2-5 工厂模式
以上代码是一个动态的生成配置,通过FEED的类型匹配到key,取到对应的bean,然后创建不同的动态,用的就是工厂模式。
设计模式里面的命令模式也是SRP的体现,命令模式分离“命令的请求者”和“命令的实现者”方面的职责。举一个很好理解的例子,就是你去餐馆吃饭,餐馆存在顾客、服务员、厨师三个角色。作为顾客,你只要列出菜单,传给服务员,由服务员通知厨师去实现。作为服务员,只需要调用准备饭菜这个方法(对厨师大喊“该炒菜了”),厨师听到要炒菜的请求,就立即去做饭。在这里,命令的请求和实现就完成了解耦。
模拟这个过程,首先定义厨师角色,厨师进行实际的做饭、烧汤的工作。详细代码如代码清单2-6所示。
代码清单2-6 餐馆的示例
/**
厨师类,命令接受者与执行者
***/
class cook{
public function meal(){
echo '番茄炒鸡蛋',PHP_EOL;
}
public function drink(){
echo '紫菜蛋花汤',PHP_EOL;
}
public function ok(){
echo '完毕',PHP_EOL;
}
}
// 然后是命令接口
interface Command{
// 命令接口
public function execute();
}
现在轮到服务员出场,服务员是命令的传送者,通常你到饭馆吃饭都是叫服务员吧,不可能直接叫厨师,一般都是叫“服务员,给我来盘番茄炒西红柿”,而不会直接叫“厨师,给我来盘番茄炒西红柿”。所以,服务员是顾客和厨师之间的命令沟通者。模拟这个过程的代码如代码清单2-7所示。
代码清单2-7 模拟服务员与厨师的过程
class MealCommand implements Command {
private $cook;
// 绑定命令接受者
public function construct(cook $cook){
$this->cook = $cook;
}
public function execute(){
$this->cook->meal();// 把消息传递给厨师,让厨师做饭,下同
}
}
class DrinkCommand implements Command {
private $cook;
// 绑定命令接受者
public function construct(cook $cook){
$this->cook = $cook;
}
public function execute(){
$this->cook->drink();
}
}
现在顾客可以按照菜单叫服务员了,如代码清单2-8所示。
代码清单2-8 模拟顾客与服务员的过程
class cookControl{
private $mealcommand;
private $drinkcommand;
// 将命令发送者绑定到命令接收器上面来
public function addCommand(Command $mealcommand,Command $drinkcommand){
$this->mealcommand = $mealcommand;
$this->drinkcommand = $drinkcommand;
}
public function callmeal(){
$this->mealcommand->execute();
}
public function calldrink(){
$this->drinkcommand->execute();
}
}
好了,现在完成整个过程,如代码清单2-9所示。
代码清单2-9 实现命令模式
$control=new cookControl;
$cook=new cook;
$mealcommand=new MealCommand($cook);
$drinkcommand=new DrinkCommand($cook);
$control->addCommand($mealcommand,$drinkcommand);
$control->callmeal();
$control->calldrink();
从上面的例子可以看出,原来设计模式并非纯理论的东西,而是来源于实际生活,就连普通的餐馆老板都懂设计模式这门看似高深的学问。其实,在经济和管理活动中,对流程的优化就是对各种设计模式的摸索和实践。所以,设计模式并非计算机编程中的专利。事实上,设计模式的起源不是计算机学科,而是源于建筑学。
在设计模式方面,不仅以上这两种体现了SRP,还有别的(比如代理模式)也体现了SRP。SRP不只是对类设计有意义,对以模块、子系统为单位的系统架构设计同样有意义。
模块、子系统也应该仅有一个引起它变化的原因,如MVC所倡导的各个层之间的相互分离其实就是SRP在系统总体设计中的应用。图2-2是来自CI框架的流程图。
SRP是最简单的原则之一,也是最难做好的原则之一。我们会很自然地将职责连接在一起。找到并且分离这些职责是软件设计需要达到的目的。
一些简单的应该遵循的做法如下:
根据业务流程,把业务对象提炼出来。如果业务流层的链路太复杂,就把这个业务对象分离为多个单一业务对象。当业务链标准化后,对业务对象的内部情况做进一步处理。把第一次标准化视为最高层抽象,第二次视为次高层抽象,以此类推,直到“恰如其分”的设计层次。
职责的分类需要注意。有业务职责,还要有脱离业务的抽象职责,从认识业务到抽象算法是一个层层递进的过程。就好比命令模式中的顾客,服务员和厨师的职责,作为老板(即设计师)的你需要规划好各自的职责范围,既要防止越俎代庖,也要防止互相推诿。