深入 PHP 面向對象、模式與實踐
需要操作原對象,但又不想影響原對象.
$K_back = clone $K;
基本數據類型和數組都為真復制,即為真副本,當屬性為對象時,為假復制,改變副本仍會影響原對象.解決方案:
//在原對象中添加
function __clone(){
$this->對象 = clone $this->對象
} __clone 在clone前自動觸發,可以執行一些在備份前的屬性操作.
</li>傳遞引用
方法引用傳遞,改變源對象
function set_K(& $K){...}
function & get_K(){...}延遲靜態綁定
應用場景:Dog類和Person類都需要一個返回實例化的方法,Dog類和Person類都繼承于Animal抽象類.
abstract class Animal{
public static function create(){
//實例化調用類
return new static();
}
}
class Person extends Animal{...}
//返回Person實例化類
Person::create();- __get($property) ,訪問未定義的屬性時調用.
- __set($property,$value) ,給未定義的屬性賦值時被調用.
- __isset($property) ,對未定義屬性調用isset()方法時調用.
- __unset($property) ,對未定義屬性調用unset()方法時調用.
- __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 "超過指定金額{$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_methods('類名') :獲取類中所有方法.
get_class_vars('類名') :獲取類中所有public參數;
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 觀察者模式
當被觀察者發生變化,觀察者需要被通知.
當數據發生變化,頁面需要被通知.
使用步驟:
- 觀察者加載到被觀察者中.
- 被觀察者通知觀察者.
例如登陸類(被觀察)狀態改變,要出發郵件系統和日志系統(觀察者)
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();