<<深入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: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 );
本文爲作者自己讀書總結的文章,由於作者的水平限制,難免會有錯誤,歡迎大家指正,感激不盡。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章