第2章 面向对象的设计原则

正文开始

第2章 面向对象的设计原则

在面向对象的设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是设计者极为关注的问题。为此不少OO先驱提出了很多有关面向对象的设计原则用于指导OO的设计和开发。下面是几条与类设计相关的设计原则。

面向对象设计的五大原则分别是单一职责原则、接口隔离原则、开放-封闭原则、替换原则、依赖倒置原则。这五大原则也是23种设计模式的基础©。

2.1.1 单一职责原则

亚当·斯密曾就制针业做过一个分工产生效率的例子©。对于一个没有受过相应训练,又不知道怎样使用这种职业机械的工人来讲,即使他竭尽全力地工作,也许一天连一根针也生产不出来,当然更生产不出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是最简单的原则之一,也是最难做好的原则之一。我们会很自然地将职责连接在一起。找到并且分离这些职责是软件设计需要达到的目的。


 

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

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

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

2.1.2 接口隔离原则

设计应用程序的时候,如果一个模块包含多个子模块,那么我们应该小心对该模块做出抽象。设想该模块由一个类实现,我们可以把系统抽象成一个接口。但是要添加一个新的模块扩展程序时,如果要添加的模块只包含原系统中的一些子模块,那么系统就会强迫我们实现接口中的所有方法,并且还要编写一些哑方法。这样的接口被称为胖接口或者被污染的接口,使用这样的接口将会给系统引入一些不当的行为,这些不当的行为可能导致不正确的结果,也可能导致资源浪费。

1.接口隔离

接口隔离原则(Interface Segregation Principle,ISP)表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口代替它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好得多。

ISP的主要观点如下:

1)一个类对另外一个类的依赖性应当是建立在最小的接口上的。

ISP可以达到不强迫客户(接口的使用方)依赖于他们不用的方法,接口的实现类应该只呈现为单一职责的角色(遵守SRP原则)。

ISP还可以降低客户之间的相互影响——当某个客户程序要求提供新的职责(需求变化)而迫使接口发生改变时,影响到其他客户程序的可能性会最小。

2)客户端程序不应该依赖它不需要的接口方法(功能)。

客户端程序不应该依赖它不需要的接口方法(功能),那依赖什么?依赖它所需要的接口。客户端需要什么接口就提供什么接口,把不需要的接口剔除,这就要求对接口进行细化,保证其纯洁性。

比如在应用继承时,由于子类将继承父类中的所有可用的方法;而父类中的某些方法,在子类中可能并不需要。例如,普通员工和经理都继承自雇员这个接口,员工需要每天写工作日志,而经理则不需要。因此不能用工作日志来卡经理,也就是经理不应该依赖于提交工作日志这个方法。

可以看出,ISP和SRP在概念上是有一定交叉的。事实上,很多设计模式在概念上都有交叉,甚至你很难判断一段代码属于哪一种设计模式。

ISP强调的是接口对客户端的承诺越少越好,并且要做到专一。当某个客户程序的要求发生变化,而迫使接口发生改变时,影响到其他客户程序的可能性小。这实际上就是接口污染的问题。

2.对接口的污染

过于臃肿的接口设计是对接口的污染。所谓接口污染就是为接口添加不必要的职责,如果开发人员在接口中增加一个新功能的主要目的只是减少接口实现类的数目,则此设计将导致接口被不断地“污染”并“变胖”。

接口污染会给系统带来维护困难和重用性差等方面的问题。为了能够重用被污染的接口,接口的实现类就被迫要实现并维护不必要的功能方法。

“接口隔离”其实就是定制化服务设计的原则。使用接口的多重继承实现对不同的接口的组合,从而对外提供组合功能——达到“按需提供服务”。

看下面这个例子,如图2-3所示。


 

客户A需要A服务,只要针对客户A的方法发生改变,客户B和客户C就会受到影响。故这种设计需要对接口进行隔离,如图2-4所示。


 

由图2-4可知,如果针对客户A的方法发生改变,客户B和客户C并不会受到任何影响。你可能会想,这样做接口那岂不是会很多?这个问题问得很好,接口既要拆,但也不能拆得太细,这就得有个标准,这就是高内聚。接口应该具备一些基本的功能,能独一完成一个基本的任务。

图2-4所示只是个抽象的例子,在实际应用中,会遇到如下问题:比如,我需要一个能适配多种类型数据库的DAO实现,那么首先应实现一个数据库操作的接口,其中规定一些数据库操作的基本方法,如连接数据库、增删查改、关闭数据库等。这是一个最少功能的接口。对于一些MySQL中特有的而其他数据库不具有或性质不同的方法,如PHP里可能用到的MySQL的pconnect方法,其他数据库里并不存在和这个方法相同的概念,这个方法也就不应该出现在这个基本的接口里,那这个基本的接口应该有哪些基本的方法呢?PDO已经告诉你了。

PDO是一个抽象的数据接口层,它告诉我们一个基本的数据库操作接口应该实现哪些基本的方法。接口是一个高层次的抽象,所以接口里的方法应该是通用的、基本的、不易变化的。

还有一个问题,那些特有的方法应该怎么实现?根据ISP原则,这些方法可以在另一个接口中存在,让这个“异类”同时实现这两个接口。

对于接口的污染,可以考虑下面这两条处理方式:

利用委托分离接口。

利用多继承分离接口。

委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理,如策略模式、代理模式等中都应用到了委托的概念。至于其实现,在反射那一节其实已经实现了,这里就不再细讲了。

利用多继承分离接口,在接口一节也做了相应的讲解,这里不再重复。

2.1.3 开放-封闭原则

1.什么是“开放-封闭”

随着软件系统的规模不断增大,软件系统的维护和修改的复杂性不断提高,这种困境促使法国工程院院士Bertrand Meyer在1998年提出了“开放-封闭”(Open-Close Principle,OCP)原则,这条原则的基本思想是:

Open(Open for extension)模块的行为必须是开放的、支持扩展的,而不是僵化的。

Closed(Closed for modification)在对模块的功能进行扩展时,不应该影响或大规模地影响已有的程序模块。

换句话说,也就是要求开发人员在不修改系统中现有功能代码(源代码或者二进制代码)的前提下,实现对应用系统的软件功能的扩展。用一句话概括就是:一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。

从生活中,最容易想到的例子就是电脑,我们可以轻松地对电脑进行功能的扩展,而只需通过接口连入不同的设备。

开放-封闭能够提高系统的可扩展性和可维护性,但这也是相对的,对于一台电脑不可能完全开放,有些设备和功能必须保持稳定才能减少维护上的困难。要实现一项新的功能,你就必须升级硬件,或者换一台更高性能的电脑。以电脑中的多媒体播放软件为例,作为一款播放器,应该具有一些基本的、通用的功能,如打开多媒体文件,停止播放、快进、音量调节等功能。但不论是什么播放器,不论是在什么平台下,遵循这个原则设计的播放器都应具有统一风格和操作习惯,无论换用哪一款播放器,都应保证操作者能快速上手。

以播放器为例,先定义一个抽象的接口,代码如下所示。

interface process{

public function process();

}
然后,对此接口进行扩展,实现解码和输出的功能,如代码清单2-10所示。

代码清单2-10 实现播放器的编码功能

class playerencode implements process{

public function process(){

echo "encode\r\n";

}

}

class playeroutput implements process{

public function process(){

echo "output\r\n";

}

}

对于播放器的各种功能,这里是开放的,只要你遵照约定,实现了process接口,就能给播放器添加新的功能模块。这里只实现解码和输出模块,还可以依据需求,加入更多新的模块。

接下来为定义播放器的线程调度管理器,播放器一旦接收到通知(可以是外部单击行为,也可以是内部的notify行为),将回调实际的线程处理,如代码清单2-11所示。

代码清单2-11 播放器的“调度管理器”

class playProcess{

private $message=null;

public function construct(){

}

public function callback(event $event){

$this->message=$event->click();

if($this->message instanceof process){

$this->message->process();

}

}

}

具体的产品出来了,在这里定义一个MP4类,这个类是相对封闭的,其中定义事件的处理逻辑,如代码清单2-12所示。

代码清单2-12 播放器的事件处理逻辑

class mp4{

public function work(){

$playProcess=new playProcess();

$playProcess->callback(new event('encode'));

$playProcess->callback(new event('output'));

}

}

最后为事件分拣的处理类,此类负责对事件进行分拣,判断用户或内部行为,以产生正确的“线程”,供播放器内置的线程管理器调度,如代码清单2-13所示。

代码清单2-13 播放器的事件处理类
 
class event{

private $m;

public function construct($me){

$this->m=$me;

}

public function click(){

switch($this->m){

case 'encode':

return new playerencode();

break;

case 'output':
  
return new playeroutput();

break;

}

}

}

最后,运行下下面的代码:

$mp4=new mp4;

$mp4->work();

输出结果如下:

encode

output

这就实现了一个基本的播放器,此播放器的功能模块是对外开放的,但是内部处理应该是相对封闭和稳定的。但这个实现还存在一些问题,这就需要你来发现了。有时候为了降低系统的复杂性,也会不完全遵守设计模式,而是对其进行增删改。

2.如何遵守开放-封闭原则

实现开放-封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,这样的修改就是封闭的;而通过面向对象的继承和对多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。

1)在设计方面充分应用“抽象”和“封装”的思想。

一方面也就是要在软件系统中找出各种可能的“可变因素”,并将之封装起来;

另一方面,一种可变性因素不应当散落在多个不同代码模块中,而应当被封装到一个对象中。

2)在系统功能编程实现方面应用面向接口的编程。

当需求发生变化时,可以提供该接口新的实现类,以求适应变化。

面向接口编程要求功能类实现接口,对象声明为接口类型。在设计模式中,装饰模式比较明显地用到了OCP。

2.1.4 替换原则

替换原则由MIT计算机科学实验室的Liskov女士在1987年的OOPSLA大会上的一篇文章《Data Abstraction and Hierarchy》中提出,主要阐述有关继承的一些原则,故又称里氏替换原则。

2002年,Robert C.Martin出版了一本名为《Agile Software Development Principles Patterns and Practices》的书,在书中他把里氏代换原则最终简化为一句话:“Subtypes must be substitutable for their base types”。(子类必须能够替换成它们的基类。)

1.LSP的内容

里氏替换原则(Liskov Substitution Principle,LSP)的定义和主要的思想如下:由于面向对象编程技术中的继承在具体的编程中过于简单,在许多系统的设计和编程实现中,我们并没有认真地、理性地思考应用系统中各个类之间的继承关系是否合适,派生类是否能正确地对其基类中的某些方法进行重写等问题。因此经常出现滥用继承或者错误地进行了继承等现象,给系统的后期维护带来不少麻烦。这就需要我们有一个设计原则来遵循,它就是替换原则。

LSP指出:子类型必须能够替换掉它们的父类型、并出现在父类能够出现的任何地方。它指导我们如何正确地进行继承与派生,并合理地重用代码。此原则认为,一个软件实体如果使用一个基类的话,那么一定适用于其子类,而且这根本不能察觉出基类对象和子类对象的区别。想一想,是不是和第一章提到的多态的概念比较像?

2.LSP主要是针对继承的设计原则

因为继承与派生是OOP的一个主要特性,能够减少代码的重复编程实现,从而实现系统中的代码复用,但如何正确地进行继承设计和合理地应用继承机制呢?

这就是LSP所要解决的问题:

如何正确地进行继承方面的设计?

最佳的继承层次如何获得?

怎样避免所设计的类层次陷入不符合OCP原则的状况?

那如何遵守该设计原则呢?

父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中声明的方法,而不应当给出多余的方法定义或实现。

在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期绑定(动态多态)。

如果A、B两个类违反了LSP的设计,通常的做法是创建一个新的抽象类C,作为两个具体类的超类,将A和B的共同行为移动到C中,从而解决A和B行为不完全一致的问题。

在前面的多态,继承这几节的内容里,已经涉及LSP,包括使用多态实现隐藏基类和派生类对象的区别,以及使用组合的方式解决继承中的基类与派生类(即子类)中的不符合语意的情况。PHP对LSP的支持并不好,缺乏向上转型等概念,只能通过一些曲折的方法实现。对于这个原则,这里就不再细讲了。

在接口那节提到了一个缓存的实现接口,试试用抽象类做基类,遵循LSP实现其设计。

这里给出其抽象类代码,如代码清单2-14所示。

代码清单2-14 缓存实现抽象类

<©php

abstract class Cache {

/**

*设置一个缓存变量

*

*@param String $key  缓存Key

  
*@param mixed $value 缓存内容

*@param int $expire缓存时间(秒)

*@return boolean是否缓存成功

*/

public abstract function set($key, $value, $expire = 60);

/**

*获取一个已经缓存的变量

*@param String $key缓存Key

*@return mixed缓存内容

*/

public abstract function get($key);

/**

*删除一个已经缓存的变量

*@return boolean   是否删除成功

*/

public abstract function del($key);

/**

*删除全部缓存变量

*

*@return boolean   是否删除成功

*/

public abstract function delAll();

/**

*检测是否存在对应的缓存

*

*/

public abstract function has($key);

}

如果现在要求实现文件、memcache、accelerator等各种机制下的缓存,只需要继承这个抽象类并实现其抽象方法即可。

现在,再来思考本书开头提到的白马非马的问题,试着用里氏替换原则阐释。

注意 LSP中代换的不仅仅是功能,还包括语意。试思考:白马可以代换马,而牛同样作为劳力,可代换马否?高跟鞋也是鞋子,男人穿高跟鞋又是否能接受?

 
2.1.5 依赖倒置原则


    什么是依赖倒置呢?简单地讲就是将依赖关系倒置为依赖接口,具体概念如下:

    上层模块不应该依赖于下层模块,它们共同依赖于一个抽象(父类不能依赖子类,它们都要依赖抽象类)。

    抽象不能依赖于具体,具体应该要依赖于抽象。

    注意,这里的接口不是狭义的接口。

    为什么要依赖接口?因为接口体现对问题的抽象,同时由于抽象一般是相对稳定的或者是相对变化不频繁的,而具体是易变的。因此,依赖抽象是实现代码扩展和运行期内绑定(多态)的基础:只要实现了该抽象类的子类,都可以被类的使用者使用。这里,我想强调一下扩展性这个概念。通常扩展性是指对已知行为的扩展,在讲述接口那一节,我也提到,接口应该是相对稳定的。这就告诉我们,无论使用多么先进的设计模式,也无法做到不需要修改代码即可达到以不变应万变的地步。在面向对象的这五大原则里,我认为依赖倒置是最难理解,也是最难实现的。

    这个例子以前面提到的雇员类为蓝本,实现代码如代码清单2-15所示。

    代码清单2-15 employee.php

    <©php

    interface employee{

    public function working();

    }

    class teacher implements employee{

    public function working(){

    echo 'teaching...';

    }

    }

    class coder implements employee{

    public function working(){

    echo 'coding...';

    }

    }

    class workA{

    public function work(){

    $teacher=new teacher;

    $teacher->working();

    }

    }

    class workB{

    private $e;

    public function set(employee $e){

    $this->e=$e;

    }

    public function work(){

    $this->e->working();

    }

    }

    $worka=new workA;

    $worka->work();

    $workb=new workB;

    $workb->set(new teacher());

    $workb->work();

    在classA中,work方法依赖于teacher实现;在classB中,work转而依赖于抽象,这样可以把需要的对象通过参数传入。上述代码通过接口,实现了一定程度的解耦,但仍然是有限的。不仅是使用接口,使用工厂等也能实现一定程度的解耦和依赖倒置。

    在workB中,teacher实例通过setter方法传入中,从而实现了工厂模式。由于这样的实现仍然是硬编码的,为了实现代码的进一步扩展,把这个依赖关系写在配置文件里,指明classB需要一个teacher对象,专门由一个程序检测配置是否正确(如所依赖的类文件是否存在)以及加载配置中所依赖的实现,这个检测程序,就称为IOC容器。

    很多文章里看到IOC(Inversion of Control)概念,实际上,IOC是依赖倒置原则(Dependence Inversion Principle,DIP)的同义词。而在提IOC的时候,你可能还会看到有人提起DI等概念。DI,即依赖注入,一般认为,依赖注入(DI)和依赖查找(DS)是IOC的两种实现。不过随着某些概念的演化,这几个概念之间的关系也变得很模糊,也有人认为IOC就是DI。有人认为,依赖注入的描述比起IOC来更贴切,这里不纠缠于这几个概念之间的关系。

    在经典的J2EE设计里,通常把DAO层和Service层细分为接口层和实现层,然后在配置文件里进行依赖关系的配置,这是最常见的DIP的应用。Spring框架就是一个很好的IOC容器,把控制权从代码剥离到IOC容器,这里是通过XML配置文件实现的,Spring在执行时期根据配置文件的设定,建立对象之间的依赖关系。

    如下面代码所示:

    <bean scope="prototype"

    class="cn.notebook.action.NotebookListOtherAction"

    id="notebookListOtherAction">

   

   

   

   

   

    但是这样设置一样存在问题,配置文件会变得越来越大,其间关系会越来越复杂。同样逃脱不了随着应用和业务的改变,不断修改代码的恶魇(这里认为配置文件是代码的一部分。并且在实际开发中,很少存在单纯修改配置文件的情况。一般配置文件修改了,代码也会做相应修改)。

    在PHP里,也有类似模仿Spring的实现,即把依赖关系写在了配置文件里,通过配置文件来产生需要的对象。我觉得这样的代码是还是为了实现而实现。在Spring里,配置文件里配置的不仅仅是一个类运行时的依赖关系,还可以实现事务管理、AOP、延迟加载等。而PHP要实现上面的种种特性,其消耗是巨大的。从语言层面讲,PHP这种动态脚本型语言在实现一些多态特性上和编译型的语言不同。其次PHP作为敏捷性的开发语言,更强调快速开发、逻辑清晰、代码简单易懂,如果再附加了各种设计模式的框架,从技术实现和运行效率上来看,都是不可取的。依赖倒置的核心原则是解耦。如果脱离这个最原始的原则,那就是本末倒置。

    事实上,很多的设计模式里已经隐含了依赖倒置原则,我们也在有意或无意地做着一些依赖反转的工作。只是作为PHP,目前还没有一个比较完善的IOC容器,或许是PHP根本不需要。

    如何满足DIP:

    每个较高层次类都为它所需要的服务提出一个接口声明,较低层次类实现这个接口。

    每个高层类都通过该抽象接口使用服务。

2.2 一个面向对象留言本的实例

在这一节,用面向对象的思想完成一个简单的留言本模型,这个模型不涉及实际的数据库操作以及界面显示,只是一个demo,用来演示面向对象的一些思维。

在面向过程的思维里,要设计一个留言本,一切都将以留言本为核心,抓到什么是什么,按流程走下来,即按用户填写信息→留言→展示的流程进行。

现在用面向对象的思维思考这个问题,在面向对象的世界,会想尽办法把肉眼能看见的以及看不见的,但是实际存在的物或者流程抽象出来。既然是留言本,那么就存在留言内容这个实体,这个留言实体(domain)应该包括留言者的姓名、E-mail、留言内容等要素,如代码清单2-16所示。

代码清单2-16 留言实体类message.php

class message{

public $name;  // 留言者姓名

public $email;// 留言者联系方式

public $content;// 留言内容

public function set($name, $value) {

$this->$name= $value;

}

public function get($name) {

if(!isset($this->$name)){

$this->$name=NULL;

}

}

}

上面的类也就是所说的domain,是一个真实存在的、经过抽象的实体模型。然后,需要一个留言本模型,这个留言本模型包括留言本的基本属性和基本操作,如代码清单2-17所示。

代码清单2-17 留言本模型gbookModel.php

/**

*留言本模型,负责管理留言本

*$bookPath:留言本属性

*/

class gbookModel{

private $bookPath;// 留言本文件

private $data;// 留言数据
  
public function setBookPath($bookPath){

$this->bookPath=$bookPath;

}

public function getBookPath(){

return $this->bookPath;

}

public function open(){

}

public function close(){

}

public function read(){

return file_get_contents($this->bookPath);

}

// 写入留言

public function write($data){

$this->data=self::safe($data)->name."&".self::safe($data)->email."\r\nsaid:\r\n".self::safe($data)->content;

return

file_put_contents($this->bookPath,$this->data,FILE_APPEND);

}

// 模拟数据的安全处理,先拆包再打包

public static function safe($data){

$reflect = new ReflectionObject($data);

$props = $reflect->getProperties();

$messagebox =new stdClass();

foreach ($props as $prop) {

$ivar=$prop->getName();

$messagebox->$ivar=trim($prop->getValue($data));

}

return $messagebox;

}

public function delete(){

file_put_contents($this->bookPath,'it's empty now');  }

}

实际留言的过程可能会更复杂,可能还包括一系列准备操作以及Log处理,所以应定义一个类负责数据的逻辑处理,如代码清单2-18所示。

代码清单2-18 留言本业务逻辑处理leaveModel.php
  
class leaveModel{

public function write(gbookModel $gb,$data){

$book=$gb->getBookPath();

  
$gb->write($data);

// 记录日志

}

}

最后,通过一个控制器,负责对各种操作的封装,这个控制器是直接面向用户的,所以包括留言本查看、删除、留言等功能。可以形象理解为这个控制器就是留言本所提供的直接面向使用者的功能,封装了操作细节,只需要调用控制器的相应方法即可,如代码清单2-19所示。

代码清单2-19 前端控制部分代码

class authorControl{

public function message(leaveModel $l,gbookModel $g,message $data){

// 在留言本上留言

$l->write($g, $data);

}

public function view(gbookModel $g){

// 查看留言本内容

return $g->read();

}

public function delete(gbookModel $g){

$g->delete();

echo self::view($g);

}

}

测试代码如下所示:

$message=new message;

$message->name='phper';

$message->email='phper@php.net';

$message->content='a crazy phper love php so much.';

$gb=new authorControl();// 新建一个留言相关的控制器

$pen=new leaveModel();// 拿出笔

$book=new gbookModel();// 翻出笔记本

$book->setBookPath("g:\\bak\\temp\\tempcode\\a.txt");

$gb->message($pen,$book,$message);

echo $gb->view($book);

$gb->delete($book);

这样看起来是不是比面向过程要复杂多了?确实是复杂了,代码量增多了,也难以理解了。似乎也体现不出优点来。但是你思考过以下问题吗?

如果让很多人来负责完善这个留言本,一部分负责实体关系的建立,一部人负责数据操作层的代码,这样是不是更容易分工了呢?

如果我要把这个留言本进一步开发,实现记录在数据库中,或者添加分页功能,又该如何呢?

要实现上面第二个问题提出的功能,只需在gbookModel类中添加分页方法,代码如下所示:

public function readByPage(){

$handle = file($this->bookPath);

$count=count($handle);

$page=isset ($_GET['page'])©intval($_GET['page']):1;

if($page<1 page="">$count) $page=1;

$pnum=9;

$begin=($page-1)*$pnum;

$end=($begin+$pnum)>$count©$count:$begin+$pnum;

for($i=$begin;$i<$end;$i++){

echo '',$i+1,'',$handle[$i],'
';

}

for($i=1;$i<=ceil($count/$pnum);$i++){

echo "${i}";

}

}
然后到前端控制器里添加对应的action,代码如下所示:

public function viewByPage(gbookModel $g){

return $g->readByPage();

}

运行结果如图2-5所示。
 

只需要这么简单的两步,就可实现所需要的分页功能,而且已有的方法都不用修改,只需在相关类中新增方法即可。当然,这个分页在实际点击时是有问题的,因为我没有把Action分开,而是通通放在一个页面里。对照着上面的思路,还可以把留言本扩展为MySQL数据库的。

在这个程序里只体现了非常简单的设计模式,这个程序还有许多要改进的地方,每个程序员心中都有一个自己的OO。项目越大越能体现模块划分、面向对象的好处。

思考 试着找找这个小程序里体现了哪些设计原则,并且试着加上一些异常处理等。

2.3 面向对象的思考

PHP的特色是简单、快速、适用。在PHP的世界里,一切以解决问题为主,所以很多设计方面的东西往往被忽视或排斥。虽然PHP的面向对象提出很多年了,但一直被排斥,很多人提倡原生态开发方式,甚至有人提倡彻底面向过程。伴随着对OO的质疑,PHP框架一方面如雨后春笋般遍地开花,另一方面一直受到抵制和质疑。

有一点是肯定的,PHP不是一门很好的面向对象的语言,因为其无法做到完全面向对象,也无法优雅实现面向对象。所以现在比较流行的还是以类为主的开放方式,即抛弃或精简经典的MVC理论,很少用和几乎不用设计模式,以类加代码模块的方式进行代码组织。这种开发方式在PHP的开源项目里是最流行的,也是最适合二次开发的,而比较纯的面向对象的产品有Zend Framework。这类产品入门的门槛比较高,代码看似“臃肿”,开发成本比较高,这类产品一般比较少见,市场占有率也比较低。

所有产品最终都是为市场服务的,PHP面向的是Web开发市场,所以并不需要高端的、复杂的设计和开发技巧。但是前面讲的那些并不是没有作用。

一些基本理论,在任何一门语言里都有共性。语法和函数库只是学好一门语言的必要条件,而不是充要条件。语法和函式只是表层的东西。只要掌握面向对象的思想,即使没有一点Java和.NET基础,也能看懂用它们写成的代码。

PHP只是一个脚本语言、一门工具而已。在Web开发中,PHP语言自身所占的分量越来越低,但却涉及程序设计的方方面面,而面向对象只是其中之一,也是最主要的一个方面。PHP是一种经典思想,能实现低耦合、易扩展的代码,其可用最经济的方式干一件事。

理论是重要的,但是理论也不是一成不变的。比如我们提到的一些设计模式,也没必要完全遵守,可以做一些精简和变形。

基于以上思考,我们认为在PHP的开发中应该灵活使用面向对象的特性和设计原则。

对于流程明确、需求清晰、需求变更风险小的业务逻辑,过程化开发(传统软件开发模式)最适合,这就像解一道数学题,总需要一步步去解,上一步的结果作为下一步的条件。这个时候,面向过程的开发更符合人的思维。

但是对于流程复杂、需求不完善、存在很大需求变更风险的业务逻辑,此时用过程化开发将使程序变得非常的繁琐臃肿,实现难度很大,并且后期的维护代价高得惊人。此时,抽象思维将是最适合的,用面向对象的思维去抽象业务模型并随需求不断精化,最终交付使用,其扩展度和可维护性都要比过程化方法更好。

由于面向对象是更高一层的抽象,它有一些优点较之面向过程是比较突出的:

其一,新成员的加入和融合不再困难,高度抽象有利于高度总结。

其二,代码即文档,团队中的任何人都可以轻松地获得产品各个模块的基本信息,而不再需要通读大部分代码。

说到这里,可能就会有人有疑问了:本书一直在推崇面向对象的开发模式,说面向对象的好,说OO适合复杂的项目,那Linux这种复杂的项目,使用面向过程的C语言编写的,这又如何解释?

这个问题问得好,现解释如下:

其一,Linux虽然是用面向过程的C语言编写的,但是Linux的操作系统是使用内核+模块的方式构建的,这种模块化的思想是所有编程范式中的普适原则。

其二,面向对象和各种设计模式就是已经提供好的模式,使用已有的模式本比像Linux那样自己摸索出一个模式更方便快捷,开发成本更低,代码更易阅读

其实,面向过程也好,面向对象也好,目的只有两个:一个是功能实现,一个是代码维护和扩展。只要能做好这两点,那就是成功的。

PHP不是一门很好的OOPL,但却是一门很好的Web设计语言。我们有理由相信,在Web开发领域,PHP还将继续发挥其作用,以其简单、快速吸引更多的开发者加入。

2.4 本章小结

本章主要讲解面向对象设计的五大原则,穿插一些设计模式的例子。在第1章的最后提到面向对象的设计思想存在一些问题,其本质在于面向对象强调对现实的建模,而现实和开发中并没有一一对应,因此五大原则和设计模式就是对OO的补充。

最后一节给出的留言本demo,只是一个很小的模型。一般来说,越是规模较大的项目,越能体现设计模式的前瞻性和必要性。

可能很多读者对一些设计模式有不同的见解和困惑,这是正常的。一段代码往往很难明确地归属于某一种设计模式,其可能有多种设计模式的影子。设计模式只是一种成熟的、可供借鉴的思考模式,而不是公式。

我们既要深入了解面向对象的思想,又不能执着于面向对象。


正文结束

PHP接口(interface)和抽象类(abstract) 第3章 正则表达式基础与应用1