资讯专栏INFORMATION COLUMN

<<深入PHP面向对象、模式与实践>>读书笔记:面向对象设计和过程式编程

xiao7cn / 815人阅读

摘要:注本文内容来深入面向对象模式与实践中节。面向对象设计与过程式编程面向对象设计和过程式编程有什么不同呢可能有些人认为最大的不同在于面向对象编程中包含对象。面向对象编程和过程式编程的一个核心区别是如何分配职责。

注:本文内容来<<深入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:val1
key2:val2
key3:val3

现在,我们被告知这个工具需要支持如下所示XML格式:


    
        my key
        my val
    

  如果参数文件以.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)指将职责相关的组件紧紧结合在一起,而与外部系统环境隔开,保持独立。在<>(中文名<<程序员修炼之道:从小工到专家 >>)一书中有所介绍。

  正交主张重用组件,期望不需要任何特殊配置就能把一个组件插入到新系统中。这样的组件有明确的与环境无关的输入和输出。正交代码使修改变得更简单,因为修改一个实现只会影响到被改动的组件本身。最后,正交代码更加安全。bug的影响只局限于它的作用域之中。内部高度相互依赖的代码发生错误时,很容易在系统中引起连锁反应。

  如果只有一个类,松散耦合和高聚合是无从谈起的。毕竟,我们可以把整个过程示例的全部代码塞到一个被误导的类里。(这想想就挺可怕的。)

职责和耦合的英文翻译原文是没有的,我通过Goole翻译加上的。
代码示例

过程式编程

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, "
" );
        foreach ( $params as $key=>$val ) {
            fputs( $fh, "	
" );
            fputs( $fh, "		$key
" );
            fputs( $fh, "		$val
" );
            fputs( $fh, "	
" );
        }
        fputs( $fh, "
" );
    } else {
        foreach ( $params as $key=>$val ) {
            fputs( $fh, "$key:$val
" );
        }
    }
    fclose( $fh );
}

面向对象设计

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, "
" );
        foreach ( $this->params as $key=>$val ) {
            fputs( $fh, "	
" );
            fputs( $fh, "		$key
" );
            fputs( $fh, "		$val
" );
            fputs( $fh, "	
" );
        }
        fputs( $fh, "
" );
        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
" );
        }
        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 );
本文为作者自己读书总结的文章,由于作者的水平限制,难免会有错误,欢迎大家指正,感激不尽。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30162.html

相关文章

  • 《JavaScript设计模式开发实践》 —— &lt;阅读小札·一&gt;

    摘要:阅读小札一阅读前自大学课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。第二部分是核心部分,由浅到深讲解个设计模式。设计模式遵循的原则所有设计模式罪训的一条原则就是找出程序中变化的地方,并将变化封装起来。 阅读小札 · 阅读前 自大学Java课上,就开始接触设计模式,但对设计模式却鲜有研究与实践。最近向公司反映和游说技术提升,得以获得公司提供购书机会,借此认真学习前端学习之路的...

    Yangder 评论0 收藏0
  • Python超细腻研究面向对象设计

      面向对象设计是一类编程方式,此编程方式的落地式需要使用类和目标来达到,因此,面向对象设计本身就是对类和目标的应用,今日给大家介绍一下python面向对象设计开发设计及本质特征,感兴趣的小伙伴一起了解一下吧  序言  面向对象设计对新手而言不难理解但无法运用,尽管我们给大家汇总过面向对象战略部署方式(定义类、创建对象、给目标发信息),可是看似简单其实不简单。大量程序编写训练与阅读高质量的编码有可...

    89542767 评论0 收藏0
  • 初探面向对象编程之oop设计模式

    摘要:为什么要采用面向对象编程解决问题更容易设计计算机程序就是为了解决人类的问题。面向对象编程需要对业务及代码的架构是有一定的要求的。 1. 编程方式 我们目前的编程方式大体可以有以下三种编程方式: 顺序编程 过程式编程 面向对象编程 在讲面向对象编程时先讲一下什么是顺序编程,什么是过程式编程,什么是面向对象编程: 顺序编程: 就是只用一个单线程去执行一段代码,执行过程根据代码依次从上...

    BingqiChen 评论0 收藏0
  • Python面向对象的三大特性封装、继承、多态

      小编写这篇文章的主要目的,主要是来给大家介绍关于Python的一些事情,主要还是涉及到面对面对象编程的一些实例,其中,主要涉及到的内容涵盖封装、继承、多态等多种形式,就具体的形式,下面就给大家详细解答下。  Python是一门面向对象的语言。面向对象都有三大特性:封装、继承、多态。  下面分别来说说这三大特性:  1、封装  隐藏对象的属性和实现细节,仅对外提供公共访问方式。在python中用...

    89542767 评论0 收藏0
  • python命令 – 解释器、交互式、面向对象编程语言

    Python 是一种解释型的、交互式的、面向对象的编程语言,它结合了非凡的功能和非常清晰的语法。Python Library Reference 记录了内置的和标准的类型、常量、函数和模块。语法格式:python [参数]常用参数:参数 描述-c 直接运行 python 语句-v 会输出每一个模块引用信息-i 运行完 python 脚本文件以后打开一个 python 环境-m 将模块按照脚本执行命...

    社区管理员 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<