深入PHP物件導向、模式與實踐

mzkmzk發表於2016-01-18

1 語法

1.1 基礎語法

clone

需要操作原物件,但又不想影響原物件.

$K_back = clone $K;

基本資料型別和陣列都為真複製,即為真副本,當屬性為物件時,為假複製,改變副本仍會影響原物件.解決方案:

//在原物件中新增
function __clone(){
    $this->物件 = clone $this->物件
}

__clone在clone前自動觸發,可以執行一些在備份前的屬性操作.

&傳遞引用

方法引用傳遞,改變源物件

function set_K(& $K){...}

function & get_K(){...}

static延遲靜態繫結

應用場景:Dog類和Person類都需要一個返回例項化的方法,Dog類和Person類都繼承於Animal抽象類.

abstract class Animal{
    public static function create(){
        //例項化呼叫類
        return new static();
    }
}

class Person extends Animal{...}

//返回Person例項化類
Person::create();

攔截器

  1. __get($property),訪問未定義的屬性時呼叫.
  2. __set($property,$value),給未定義的屬性賦值時被呼叫.
  3. __isset($property),對未定義屬性呼叫isset()方法時呼叫.
  4. __unset($property),對未定義屬性呼叫unset()方法時呼叫.
  5. __call($method,$arg_array),呼叫未定義方法時呼叫.__call很有用,但要慎用,因為太靈活.應用場景:有一個專門列印Person類資訊的Person_Writer類,如果通過Person類呼叫Person_Writer類.
    //Person委託Person_Writer類處理列印事務.
    class Person {
        private $writer;
        ...
    
        function __call($method_name,$args){
            if(methood_exists($this->wirter,$method_name)){
                return $this->writer->$method_name($this);
            }
        }
    
        //高階__call寫法,當委託方法引數不確定時使用.
        function __call($method_name,$args){
            //當然這裡這樣寫法意義不大,但是call一般都是用call_user_func_array呼叫
            $args = $this ;
            if(methood_exists($this->wirter,$method_name)){
                return call_user_func_array(
                    array($this->writer,$method_name),$args);
                )
            }
        }
    
    }

回撥函式

應用場景: 3個類,Product類,Product_Sale類,Product_Totalizer類,要實現:當賣出Product總共價格超過指定金額時,輸出警告.

//Product
class Product {
    public $name;
    public $price;
}

//Product_Sale
class Product_Sale {
    private $callbacks;

    //記錄回撥函式
    function register_callback ($callback) {
        if(! is_callback($callback)){
            thow new Exception('callback not callable');
        }
        $this->callbacks[] = $callback;
    }

    //執行回撥函式
    function sale ($product){
        print "{$product->name} : 處理中 n";
        foreach($this->callbacks as $callback){
            call_user_func($callback , $product);
        }
    }
}

//Produce_Totalizer
class Produce_Totalizer {
    static function warn_amount ($amt) {
        $count = 0;
        return function ($produce) use ($amt , &count) {
            $count += $produce->price;
            print " count : {count}n"
            if($count>$amt){
                print "quot;超過指定金額{$amt}啦~";
            }
        };
    }
}

//模擬場景
$product_sale = new Produce_Sale();
//指定報警金額為8塊
$product_sale = register_callback(Produce_Totalizer::warn_amount(8)); 

//賣商品
$product_sale->sale(new Product("Durex",6));
$product_sale->sale(new Produce("Jissbon",5));

//輸出結果
Durex : 處理中
    count :6 

Jissbon : 處理中 
    count: 11

超過指定金額8塊啦~

get_class()instanceof

get_class(類)用於判斷是否精準等於類名;

instanceof 可以判斷是否其本身或繼承於某父類.

類中的方法和類中的屬性

get_class_methods('類名'):獲取類中所有方法.

get_class_vars('類名'):獲取類中所有public引數;

反射API

2 模式

2.1 組合

問題:課堂類被演講類和研討會類繼承著.但是演講類和研討類都要實現一次性計費和上N次課計費的方法.和輸出計算的方式.

解決方案1: 在課堂類中新增計算一次性付費的方法,上N次課的計費方法和輸出計算方式的方法.

解決方案2: 運用組合,將處理計費和輸出計算方式單獨封裝為一個計費策略類.

組合模式

abstract class Cost_Strategy {
    protected $duration;
    abstract function cost ();
    abstract function charge_type();

    public __construct($duration){
        $this->duration = $duration;
    }
}

class Timed_Const_Strategy extends Cost_Stratedy {
    function cost () {
        //上一次課給5塊錢- -.
        return $this->duration * 5;
    }

    function charge_type(){
        return "多次課結算";
    }
}

class Fixed_Const_Strategy extends Cost_Stratedy {
    function cost (){
        return 30 ;
    }

    function charge_type(){
        return "一次性課結算";
    }
}

abstract class Leason {

    private $cost_strategy;

    public __construct(Const_Strategy $cost_strategy){
        $this->cost_strategy = $cost_strategy;
    }

    function __call($method_name,$args){
        $args = $cost_strategy ;
        if(methood_exists($this->cost_strategy,$method_name)){
            return call_user_func_array(
                array($this->writer,$method_name),$args);
            )
        }
    }
}

//運用
$leasons[] = new Seminar(new Timed_Const_Strategy(4));
$leasons[] = new Lecture(new Fixed_Const_Strategy(null));

foreach ($leasons as $leason){
    print "leason charge : {$leason->const()}";
    print "charge_type : {$leason->charge_type()}"
}

leason charge 20. charge_type : 多次課結算;
leason charge 30. charge_type : 一次課結算;

組合既委託.同級委託.

繼承既父子關係.

3 生成物件

3.1 單例模式

確保系統中只有唯一一個用例.例如系統配置檔案.

重點

1: 構造方法私有.

2: 類本身包含自己的例項化屬性.

單例模式

class Preferences {
    private static $instance;
    private function __construct(){ ... }

    public static function get_instance(){
        if(empty(self::$instance)){
            self::$instance = new Preferences();
        }
        return self::$instance;
    }
    ...
}

//使用
$preferences = Preferences::get_instance();

3.2 工廠模式

通過一個父類,生產處多個不同功能的子類.

特點:產品方(新浪微博)和需求方(顯示新浪微博)一一對應.

問題:印象筆記中,來源可能為新浪微博,或者開發者頭條,在印象筆記顯示的時候,兩者的頁首和頁尾是不一樣的.

工廠模式

3.3 抽象模式

RLGL!!!.印象筆記不只要顯示新浪微博內容!!!還要顯示我的新浪賬號,還要該微博啊!!臥槽~憋著急,吻我.

工廠模式主要用於生產一一對應的產品方和需求方,而抽象模式要做的是一個需求方(印象筆記_顯示新浪微博),要多個工廠(把需求方抽象為多個需求方),例如提供新浪內容的工廠,提供新浪賬號的工廠.提供微博內容的評論的工廠等.

抽象模式

程式碼:

abstract class Show_Evernote {
    abstract function get_header_text();
    abstract function get_context();
    abstract function get_footer_text();
    abstract function get_user();
    abstract function get_comment();

}

class 顯示新浪微博 extends Show_Evernote{
    function get_header_text(){...};
    function get_context(){new 新浪微博_內容;}
    function get_footer_text(){...};
    function get_user(){new 新浪微博_賬號 ;}
    function get_comment(){new 新浪微博_評論;}
}

//使用
印象筆記控制元件類->內容 = 顯示新浪微博->get_context;
印象筆記控制元件類->賬號 = 顯示新浪微博->get_context;
...

3.4 平行模式

當使用工廠/抽象模式必須要制定具體的建立者(需求方).

平行模式和抽象模式的模型圖一致,但程式碼實現不一樣.

抽象模式中父類均為抽象類,而平行模式中,所以類都為普通類,方便父類的例項化.

在這裡列出顯示印象筆記類的實現程式碼

class Show_Evernote{
    private $內容;
    private $賬號;
    private $評論;

    function __construct(內容,賬號,評論){
        $this->內容 = 內容;
        $this->賬號 = 賬號;
        $this->評論 = 評論;
    }

    function get_內容(){
        return clone $this->內容);
    }

    function get_賬號(){
        return clone $this->賬號);
    }

    function get_評論(){
        return clone $this->評論;
    }
}

//使用
$factory = new Show_Evernote( 
    new 新浪微博內容(),
    new 新浪微博賬號(),
    new 新浪微博評論()
);

印象筆記控制元件類->顯示印象筆記 = $factory;

其實大家可以發現,原型模式只不過只在最頂層類中包裝了一下各元件子類而已,然而這樣可以輕鬆的組合他們,例如實現一個顯示新浪微博內容,但要顯示開發者頭條賬號的需求?

4 使用物件

4.1 組合模式

組合模式,可以理解為單一物件管理組合物件(聚合元件),最終組合體下的各個組合部件最好型別一致.不然特殊性越多,需要判斷就越多.

假設捶背男,洗腳男,洗髮男,用來服務一個人(妹子).

假設妹子的幾個部位可用的服務男均為無限個.

組合模式

//建立一個妹子
$妹子 = new 人();

//新增洗腳男、捶背男
$妹子->add_man(new 洗腳男);
$妹子->add_man(new 捶背男);

//迴圈所有男的給予舒服的方法.
$妹子->計算舒服程度();

這是一個很理想的組合模式,在現實情況,我們使用組合模式,可能不得不建立多種型別的洗腳男,需要新增許多判斷條件.

4.2 裝飾模式

裝飾模式,首先洗腳男,洗髮男,捶背男都是人,但是如果,一個男的又捶背,又洗髮,這怎麼玩?.add_man兩次?這不科學吧,來給這些男的裝飾一下吧~

裝飾模式

abstract class 人{
    ...
    abstract function get_well();
}   

class 男 extends 人 {
    //無論你是神馬男,服務你,你就能獲得10點舒服度.
    private $well = 10;
    function get_well(){
        return $this->well();
    }
}

abstract class 裝飾男型別 extends 人 {
    protected $人;
    function __construct(人 $人){
        $this->人 = $人;
    } 
}

class 捶背裝飾 extends 型別男裝飾{
    function get_well(){
        return $this->人->get_well()+30;
    }
}

class 洗髮裝飾 extends 型別男裝飾{
    function get_well(){
        return $this->人->get_well()+20;
    }
}

class 洗褪裝飾 extends 型別男裝飾{
    //老子不喜歡別人碰我的毛褲.
    function get_well(){
        return $this->人->get_well()-20;
    }
}

//建立捶背,能給予的舒服指數 - -嘻嘻.
$人 = new 捶背裝飾(new 男);
$人->get_well(); // 10+30 = 40

//來來來,全能選手,捶背、洗髮、洗腿一起來
$人 = new 洗腳裝飾(new 洗髮裝飾(new 捶背裝飾(new 男()))); //10+30+20-20 = 40,注意順序,由裡到外執行.

裝飾模式,既(組合+繼承),基類方法一定要儘量少,不然子類可能有它不該有的方法.直接類繼承,她只可能是一種形態,而她的多種形態可能一併擁有的時候,應該運用組合.

繼承即單一多型,組合既多種多型.

這個例子中,你可以新增女,然後把裝飾男型別改為裝飾通用型別,但每個get_well()都要多一個判斷是男還是女(如果給予的舒服程度不一樣).

這只是確保不可能出現在,之外的第三種人,如果基類為動物,給予服務的可能是雞,鵝,鴨,那麼裝飾型別應該運用工廠模式,動物形態和裝飾形態一一對應.方便擴充.

除了服務型別,服務男的樣子也很重要,這就多了一種裝飾,現在有裝飾男型別相貌男型別,這種情況怎麼破,其實類似.

兩個裝飾

//如何獲取捶背的帥哥麥?,
$人 =new 男型別(new 捶背(new 帥哥麥(new 男())));

4.3 外觀模式

即給外部系統提供清晰介面

例如當Model層寫得很混亂,但是裡面的方法還能用,那我們的Controller層應該列舉一些清晰的訪問方法來供View層訪問.外觀模式,強調的是清晰的訪問介面.

5 執行任務

5.1 策略模式

給類新增功能.物件要顯式的呼叫它.

繼續剛才的洗腳男和人的故事吧…你丫的爽完了要給錢吧?支付寶?微信?現金?

這個付款方式有多種,實現方法不應該放在類中,而是應該委託給別的類

abstract class 人 {

    protectd $支付方式;

    function set_支付方式(){...}

    function 付款(金額){
        return $支付方式->付款($金額);
    }
}

abstract class 付款{
    abstract function 付款($金額);
}

class 支付寶付款 extends 付款{

    function 付款($金額){
        return 外接支付寶付款流程($金額);
    }
}
...

//使用
$男 =new 男();

///爽爽爽
...

//結賬
$支付寶支付賬單 = new 支付寶付款($金額);
$人 = new 男();
$人->set_支付方式(new 支付寶付款());
$人->付款();

5.2 觀察者模式

當被觀察者發生變化,觀察者需要被通知.

當資料發生變化,頁面需要被通知.

使用步驟:

  1. 觀察者載入到被觀察者中.
  2. 被觀察者通知觀察者.

觀察者模式

例如登陸類(被觀察)狀態改變,要出發郵件系統和日誌系統(觀察者)

interface 被觀察者{
    function attach(觀察者);
    function detatch(觀察者);
    function notify();
}

class Login implements 被觀察者{
    private $觀察者;

    function __construct(){
        $this->觀察者 = array();
    }

    function attach($觀察者){
        $this->觀察者 = $觀察者; 
    }

    function detach($觀察者){
        //刪除某個觀察者的操作;
    }

    function notify(){
        foreach ($this->觀察者 as $單個觀察者){
            $單個觀察者->update($this);
        }
    }       
}

interface 觀察者{
    function update(被觀察者);
}

abstract class Login_觀察者 implements 觀察者{
    private $login;
    function __construct (Login $login){
        $this->login = $login;
        $login->attach($this);
    }

    function update(觀察者 $觀察者){
        if ($觀察者 ===$this->login){
            $this->do_update($觀察者);
        }
    }
    abstract function do_update(Login $login);
}

class 郵件觀察者 extends 登陸觀察者 {
    function do_update(Login $login){
        //判斷條件 傳送郵件
    }
}

class 日誌觀察者 extends 登陸觀察者 {
    function do_update(Login $login){
        //判斷條件 記錄到日誌;
    }
}

//使用
$login = new Login();
new 郵件觀察者 ($login);
new 日誌觀察者 ($login);

PHP有內建的SPL實現上述的觀察者模式.

5.3 訪問者模式

問題: 在一個軍隊中,有很多軍隊,軍隊下面可能包含軍隊/步兵/弓箭手,這時我們要顯示一個軍隊的戰鬥力/需要糧食的各級分配?(遍歷物件並設定顯示方法).怎麼辦?.解決辦法是軍隊還是儲存自己的基本資訊,設定一個訪問者,訪問者包含總戰鬥力方法和總糧食的方法.

訪問者模式

訪問者

abstract class 軍隊訪問者{
    abstract function 訪問(單元);

    function 訪問軍隊($軍隊){
         $this->訪問($軍隊);
    }
    function 訪問弓箭手($弓箭手){
        $this->訪問($弓箭手);
    }

    //這裡重複定義了大量程式碼,其實可以用call來替代
    function __call($method_name,$args){
        if(strrpos($method_name, "訪問")){
            return call_user_func_array(
                array($this,"訪問"),$args
            );
        }
    }
}

class 軍隊戰鬥力訪問者 extends 軍隊訪問者{
    private $text="";

    function 訪問($單元){
        $ret = "";
        $pad = 4*$單元->getDpth(); //設定顯示深一級前面多4個空格.
        $ret .= sprintf( "%{$pad}s","");
        $ret .= get_class($單元). ": ";
        $ret .= "戰鬥力: " .$單元->bombardStrenth()."n";
        $this->text .=$ret;
    }

    function get_text(){
        return $this->text;
    }
}

被訪問者

abstract class 單元{
    function 接受($軍隊訪問者){
        $method = "訪問_".get_class($this);
        $軍隊訪問者->$method($this);
    }

    private $depth;
    protected function set_depath($depth){
        $this->depth=$depth;
    }

    function get_depth(){
        return $this->depth;
    }
    ...
}

abstract class 綜合單元 extends 單元{
    function 接受($軍隊訪問者){
        parent::接受($軍隊訪問者)
        foreach($this->單元集合 as $this_unit){
            $this->unit->接受($軍隊訪問者);
        }
    }
}

class 軍隊 extends 綜合單元{
    function bombardStrenth(){
        $ret =0;
        foreach($this-units() as $unit){
            $ret += $unit->bombardStrenth();
        }
        return $ret
    }
}

class 弓箭手 extends 單元{
    function bombardStrenth(){
        return 4;
    }
}

呼叫

$main_army = new Army();
$main_army->add_unit(new 步兵());
$main_army->add_unit(new 弓箭手());

$軍隊戰鬥力訪問者_例項 =new 軍隊戰鬥力訪問者();
$main_army->接受(均分戰鬥力訪問者);
print $軍隊戰鬥力訪問者->get_text();

輸出

軍隊: 戰鬥力: 50
    步兵: 攻擊力 :48
    弓箭手: 攻擊力: 4

5.4 命令模式

例子為Web頁面的login和feed_back,假如都需要使用ajax提交,那麼問題來了,將表單封裝好提交上去,得到了返回結果.如何根據返回結果跳轉不同的頁面?.

有些同學就說了,login和feed_back各自寫一個方法憋,提交的時候呼叫各自的方法.

然後再來個logout命令..增加..刪除..命令怎麼辦..

命令模式比較適合命令執行例如登陸,反饋等簡單隻需要判斷是否成功的任務

命令模式

命令:

abstract class Command{
    abstract function execute(Conmmand_Context $context);
}

class Login_Command extends Command{
    function execute(CommandContext $context){
        $managr =Register::getAccessManager();
        $user = $context->get("username");
        $pass = $context->get('pass');
        $user_obj = $manager->login($user,$pass);
        if(is_null($user_obj)){
            $context->setError($manager->getError());
            return false;
        }
        $context->addParam("user",$user_obj);
        return true;
    }
}

部署命令的呼叫者

class Command_Facotry{
    public function get_command($action){
        $class = UCFirst(strtolower($action))."_Command";
        $cmd = new $class();
        return $cmd;
    }

}

客戶端

class Controller{
    private $context;
    function __construct(){
        //Command_Context主要用來儲存request和params
        $this->context =new Command_Context();
    }
    function process(){
        $cmd Command_Factory::get_commad($this->context->get('action'));
        if(!$cmd-execute($this->context)){
            //錯誤處理
        }else{
            //成功 分發檢視
        }
    }
}

使用

$controller =new Controller();
$context = $controller->get_context();
$context->add_param('action','login');
$context->add_param('username','404_k');
$context->add_param('pass','123456');
$controller->process();

相關文章