频道栏目
读书频道 > web开发 > php > PHP核心技术与最佳实践
2.1.1 单一职责原则
2012-12-03 13:09:32     我来说两句
收藏   我要投稿

本文所属图书 > PHP核心技术与最佳实践

这是一本致力于为希望成为中高级PHP程序员的读者提供高效而有针对性指导的经典著作。本书系统归纳和深刻解读了PHP开发中的编程思想、底层原理、核心技术、开发技巧、编码规范和最佳实践。 全书分为5个部分:第一...  立即去当当网订购

亚当·斯密曾就制针业做过一个分工产生效率的例子?。对于一个没有受过相应训练,又不知道怎样使用这种职业机械的工人来讲,即使他竭尽全力地工作,也许一天连一根针也生产不出来,当然更生产不出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 工厂模式

<bead id="feedServiceFactory" class="FeedServiceFactory">

<property name="feedMap">

<map>

<entry key="friend" value-ref="friendFeed" />

<entry key="album" value-ref="albumFeed" />

<entry key="reply" value-ref="replyFeed" />

<entry key="share" value-ref="shareFeed" />

<entry key="video" value-ref="videoFeed" />

<entry key="group" value-ref="groupFeed" />

</map>

</property>

</bean>

以上代码是一个动态的生成配置,通过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是最简单的原则之一,也是最难做好的原则之一。我们会很自然地将职责连接在一起。找到并且分离这些职责是软件设计需要达到的目的。


 

一些简单的应该遵循的做法如下:

根据业务流程,把业务对象提炼出来。如果业务流层的链路太复杂,就把这个业务对象分离为多个单一业务对象。当业务链标准化后,对业务对象的内部情况做进一步处理。把第一次标准化视为最高层抽象,第二次视为次高层抽象,以此类推,直到“恰如其分”的设计层次。

职责的分类需要注意。有业务职责,还要有脱离业务的抽象职责,从认识业务到抽象算法是一个层层递进的过程。就好比命令模式中的顾客,服务员和厨师的职责,作为老板(即设计师)的你需要规划好各自的职责范围,既要防止越俎代庖,也要防止互相推诿。

您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:2.1 面向对象设计的五大原则
下一篇:2.1.2 接口隔离原则
相关文章
图文推荐
排行
热门
最新书评
文章
下载
读书
特别推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站