<<深入PHP面向对象、模式与实践>>读书笔记:面向对象设计和过程式编程
注:本文内容来<<深入PHP面向对象、模式与实践>>中6.2节。
6.2 面向对象设计与过程式编程
面向对象设计和过程式编程有什么不同呢?可能有些人认为最大的不同在于面向对象编程中包含对象。事实上,这种说法不准确。在PHP中,你经常会发现过程式编程也使用对象,如使用一个数据库类,也可能遇到类中包含过程式代码的情况。类的出现并不能说明使用了面向对象设计。甚至对于Java这种强制把一切都包含在类中的语音(这个我可以证明,我在大三的时候学过Java),使用对象也不能说明使用了面向对象设计。
面向对象编程和过程式编程的一个核心区别是如何分配职责。过程式编程表现为一系列命令和方法的连续调用。控制代码根据不同的条件执行不同的职责。这种自顶向下的控制方式导致了重复和相互依赖的代码遍布于整个项目。面向对象编程则将职责从客户端代码中移到专门的对象中,尽量减少相互依赖。
为了说明以上几点,我们分别使用面向对象和过程式代码的方式来分析一个简单的问题。假设我们要创建一个用于读写配置文件的工具。为了重点关注代码的结构,示例中将忽略具体的功能实现。(文后有完整代码示例,来自于图灵社区)
我们先按过程式方式来解决这个问题。首先,用下面的格式来读写文本:
key:value只需要两个函数:
function readParams( $sourceFile ) { $params = array(); // 从$sourceFile中读取文本参数 return $params; } function writeParams( $params, $sourceFile ) { // 写入文本参数到$sourceFile }
readParams()函数的参数为源文件的名称。该函数试图打开文件,读取每一行内容并查找键/值对,然后用键/值对构建一个关联数组。最后,该函数给控制代码返回数组。writeParams()以关联数组和指向源文件的路径作为参数,它循环遍历关联数组,将每对键/值对写入文件。下面是使用这两个函数的客户端代码:
$file = './param.txt'; $array['key1'] = 'vall'; $array['key2'] = 'val2'; $array['key3'] = 'val3'; writeParams( $array, $file ); $output = readParams( $file ); print_r( $output );
这段代码较为紧凑并且易于维护。writeParams()被调用来创建Param.txt并向其写入如下的内容:
key1:val1key2:val2
key3:val3
现在,我们被告知这个工具需要支持如下所示XML格式:
<params> <param> <key>my key</key> <val>my val</val> </param> </params>
如果参数文件以.xml文件结尾,就应该以XML模式读取参数文件。虽然这不难调节,但可能会使我们的代码更难维护。这是我们有两个选择:可以在控制代码中检查文件扩展名,或者在读写函数中检测。我们使用后面那种写法。:
function readParams( $source ) { $params = array(); if ( preg_match( "/\.xml$/i", $source ) ) { // 从$source中读取XML参数 } else { // $source中读取文本参数 } return $params; } function writeParams( $params, $source ) { if ( preg_match( "/\.xml$/i", $source ) ) { // 写入XML参数到$source } else { // 写入文本参数到$source } }
如上所示,我们在两个函数中都要检查XML扩展名,这样的重复性代码会产生问题。如果我们还被要求支持其他格式的参数,就要保持readParams()和writeParams()函数的一致性。
下面我们用类来处理相同的问题。首先,创建一个抽象的基类来定义类型接口:
abstract class ParamHandler { protected $source; protected $params = array(); function __construct( $source ) { $this->source = $source; } function addParam( $key, $val ) { $this->params[$key] = $val; } function getAllParams() { return $this->params; } static function getInstance( $filename ) { if ( preg_match( "/\.xml$/i", $filename )) { return new XmlParamHandler( $filename ); } return new TextParamHandler( $filename ); } abstract function write(); abstract function read(); }
我们定义addParam()方法来允许用户增加参数到protected属性$params, getAllParams()则用于访问该属性,获得$params的值。
我们还创建了静态的getInstance()方法来检测文件扩展名,并根据文件扩展名返回特定的子类。最重要的是,我们定义了两个抽象方法read()和write(),确保ParamHandler类的任何子类都支持这个接口。
现在,我们定义了多个子类。为了实例简洁,再次忽略实现细节:
class XmlParamHandler extends ParamHandler { function write() { // 写入XML文件 // 使用$this->params } function read() { // 读取XML文件内容 // 并赋值给$this->params } } class TextParamHandler extends ParamHandler { function write() { // 写入文本文件 // 使用$this->params } function read() { // 读取文本文件内容 // 并赋值给$this->params } }
这些类简单地提供了write()和read()方法的实现。每个类都将根据适当的文件格式进行读写。客户端代码将完全自动地根据文件扩展名来写入数据到文本和XML格式的文件:
$file = "./params.xml"; $test = ParamHandler::getInstance( $file ); $test->addParam("key1", "val1" ); $test->addParam("key2", "val2" ); $test->addParam("key3", "val3" ); $test->write(); // 写入XML格式中
我们还可以从两种文件格式中读取:
$test = ParamHandler::getInstance( "./params.txt" ); $test->read(); // 从文本格式中读取
那么,我们可以从这两种解决方案中学习到什么呢?
职责
在过程式编程的例子中,控制代码的职责(duties)是判断文件格式,它判断了两次而不是一次。条件语句被绑定到函数中,但这仅是将判断的流程影藏起来。对readParams()的调用和对writeParams()的调用必须发生在不同的地方,因此我们不得不在每个函数中重复检测文件扩展名(或执行其他检测操作)。
在面向对象代码中,我们在静态方法getInstance()中进行文件格式的选择,并且仅在getInstance()中检测文件扩展名一次,就可以决定使用哪一个合适的子类。客户端代码并不负责实现读写功能。它不需要知道自己属于哪个子类就可以使用给定的对象。它只需要知道自己在使用ParamHandler对象,并且ParamHandler对象支持write()和read()的方法。过程式代码忙于处理细节,而面向对象代码只需一个接口即可工作,并且不要考虑实现的细节。由于实现由对象负责,而不是由客户端代码负责,所以我们能够很方便地增加对新格式的支持。
内聚
内聚(cohesion)是一个模块内部各成分之间相互关联程度的度量。理想情况下,你应该使各个组件职责清晰、分工明确。如果代码间的关联范围太广,维护就会很困难--因为你需要在修改部分代码的同时修改相关代码。
前面的ParamHandler类将相关的处理过程集中起来。用于处理XML的类方法间可以共享数据,并且一个类方法中的改变可以很容易地反映到另一个方法中(比如改变XML元素名)。因此我们可以说ParamHandler类是高度内聚的。
另一方面,过程式的例子则把相关的过程分离开,导致处理XML的代码在多个函数中同时出现。
耦合
当系统各部分代码紧密绑在一起时,就会产生精密耦合(coupling),这时在一个组件中的变化会迫使其他部件随之改变。紧密耦合不是过程式代码特有的,但是过程式代码比较容易产生耦合问题。
我们可以在过程代码中看到耦合的产生。在writeParams()和readParams()函数中,使用了相同的文件扩展名测试来决定如何处理数据。因此我们要改下一个函数,就不得不同时改写另一个函数。例如,我们要增加一种新的文件格式,就要在两个函数中按相同的方式都加上相应的扩展名检查代码,这样两个函数才能保持一致。
面向对象的示例中则将每个子类彼此分开,也将其余客户端代码分开。如果需要增加新的参数格式,只需简单地创建相应的子类,并在父类的静态方法getInstance()中增加一行文件检测代码即可。
正交
(orthogonality)指将职责相关的组件紧紧结合在一起,而与外部系统环境隔开,保持独立。在<<The Pragmatic Programmer>>(中文名<<程序员修炼之道:从小工到专家 >>)一书中有所介绍。
正交主张重用组件,期望不需要任何特殊配置就能把一个组件插入到新系统中。这样的组件有明确的与环境无关的输入和输出。正交代码使修改变得更简单,因为修改一个实现只会影响到被改动的组件本身。最后,正交代码更加安全。bug的影响只局限于它的作用域之中。内部高度相互依赖的代码发生错误时,很容易在系统中引起连锁反应。
如果只有一个类,松散耦合和高聚合是无从谈起的。毕竟,我们可以把整个过程示例的全部代码塞到一个被误导的类里。(这想想就挺可怕的。)
职责和耦合的英文翻译原文是没有的,我通过Goole翻译加上的。代码示例
过程式编程
<?php $file = "./texttest.proc.xml"; $array['key1'] = "val1"; $array['key2'] = "val2"; $array['key3'] = "val3"; writeParams( $array, $file ); $output = readParams( $file ); print_r( $output ); function readParams( $source ) { $params = array(); if ( preg_match( "/\.xml$/i", $source )) { $el = simplexml_load_file( $source ); foreach ( $el->param as $param ) { $params["$param->key"] = "$param->val"; } } else { $fh = fopen( $source, 'r' ); while ( ! feof( $fh ) ) { $line = trim( fgets( $fh ) ); if ( ! preg_match( "/:/", $line ) ) { continue; } list( $key, $val ) = explode( ':', $line ); if ( ! empty( $key ) ) { $params[$key]=$val; } } fclose( $fh ); } return $params; } function writeParams( $params, $source ) { $fh = fopen( $source, 'w' ); if ( preg_match( "/\.xml$/i", $source )) { fputs( $fh, "<params>\n" ); foreach ( $params as $key=>$val ) { fputs( $fh, "\t<param>\n" ); fputs( $fh, "\t\t<key>$key</key>\n" ); fputs( $fh, "\t\t<val>$val</val>\n" ); fputs( $fh, "\t</param>\n" ); } fputs( $fh, "</params>\n" ); } else { foreach ( $params as $key=>$val ) { fputs( $fh, "$key:$val\n" ); } } fclose( $fh ); }
面向对象设计
<?php abstract class ParamHandler { protected $source; protected $params = array(); function __construct( $source ) { $this->source = $source; } function addParam( $key, $val ) { $this->params[$key] = $val; } function getAllParams() { return $this->params; } protected function openSource( $flag ) { $fh = @fopen( $this->source, $flag ); if ( empty( $fh ) ) { throw new Exception( "could not open: $this->source!" ); } return $fh; } static function getInstance( $filename ) { if ( preg_match( "/\.xml$/i", $filename )) { return new XmlParamHandler( $filename ); } return new TextParamHandler( $filename ); } abstract function write(); abstract function read(); } class XmlParamHandler extends ParamHandler { function write() { $fh = $this->openSource('w'); fputs( $fh, "<params>\n" ); foreach ( $this->params as $key=>$val ) { fputs( $fh, "\t<param>\n" ); fputs( $fh, "\t\t<key>$key</key>\n" ); fputs( $fh, "\t\t<val>$val</val>\n" ); fputs( $fh, "\t</param>\n" ); } fputs( $fh, "</params>\n" ); fclose( $fh ); return true; } function read() { $el = @simplexml_load_file( $this->source ); if ( empty( $el ) ) { throw new Exception( "could not parse $this->source" ); } foreach ( $el->param as $param ) { $this->params["$param->key"] = "$param->val"; } return true; } } class TextParamHandler extends ParamHandler { function write() { $fh = $this->openSource('w'); foreach ( $this->params as $key=>$val ) { fputs( $fh, "$key:$val\n" ); } fclose( $fh ); return true; } function read() { $lines = file( $this->source ); foreach ( $lines as $line ) { $line = trim( $line ); list( $key, $val ) = explode( ':', $line ); $this->params[$key]=$val; } return true; } } //$file = "./texttest.xml"; $file = "./texttest.txt"; $test = ParamHandler::getInstance( $file ); $test->addParam("key1", "val1" ); $test->addParam("key2", "val2" ); $test->addParam("key3", "val3" ); $test->write(); $test = ParamHandler::getInstance( $file ); $test->read(); $arr = $test->getAllParams(); print_r( $arr );本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。