注:本文内容来<<深刻PHP面向对象、模式与实践>>中6.2节。php
面向对象设计和过程式编程有什么不一样呢?可能有些人认为最大的不一样在于面向对象编程中包含对象。事实上,这种说法不许确。在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:val1
key2: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 );
本文为做者本身读书总结的文章,因为做者的水平限制,不免会有错误,欢迎你们指正,感激涕零。