1. 程式人生 > >PHP設計模式:類自動載入、PSR-0規範、鏈式操作、11種面向物件設計模式實現和使用、OOP的基本原則和自動載入配置

PHP設計模式:類自動載入、PSR-0規範、鏈式操作、11種面向物件設計模式實現和使用、OOP的基本原則和自動載入配置

一、類自動載入

     SPL函式 (standard php librarys)

     類自動載入,儘管 __autoload() 函式也能自動載入類和介面,但更建議使用 spl_autoload_register('函式名') 函式。 spl_autoload_register('函式名') 提供了一種更加靈活的方式來實現類的自動載入(同一個應用中,可以支援任意數量的載入器,比如第三方庫中的)。因此,不再建議使用 __autoload()

 函式,在以後的版本中它可能被棄用。

     spl_autoload_register('函式名')自動載入類,可以重複使用,不會報函式名相同的錯誤!可以實現我們自定義函式的啟用,它的返回值是bool型別:true or false。

     如果不寫引數,那麼它會去呼叫 spl_autoload()方法,這個方法預設會執行下面的語句:require_once 類名.php 或 類名.inc

spl_autoload_register('函式名')

function 函式名($class){<br>
 require __DIR__.'/'.$class.'.php';<br> }

     我們可以在入口檔案中,使用spl_autoload_register()來完成類的自動載入

二、PSR-0規範

     1. 名稱空間必須與絕對路徑一致(檔案裡寫名稱空間從根目錄下它所在資料夾開始到它的上一層資料夾名)
     2. 類名首字母必須大寫
     3. 除入口檔案外,其它的".php"檔案中只能存在一個類,不能有外部可執行的程式碼

三、鏈式操作

     鏈式操作最重要的是return $this
     要求每個方法體內必須return $this

class Database
{
    static private $db; private function __construct() { } static function getInstance() { if (empty(self::$db)) { self::$db = new self; return self::$db; } else { return self::$db; } } function where($where) { return $this; } function order($order) { return $this; } function limit($limit) { return $this; } function query($sql) { echo "SQL: $sql\n"; } }

鏈式操作能簡化程式碼,比如:

$db=new DataBase();
$db->where("id>10")->order(2)->limit(10); 

四、魔術常量和魔術方法

4.1 魔術常量

      __FILE__ 檔案的完整路徑和檔名,如果用在被包含的檔案中,則返回被包含檔案路徑名。
      __DIR__ 檔案的所在目錄,不包括檔名。 等價於dirname(__FILE__) 除了根目錄,不包括末尾的反斜槓
,basename(__FILE__)返回的是檔名
     __FUNCTION__ 返回的是函式名稱
     __METHOD__ 返回的是類的方法名 
     __CLASS__ 返回的是類的名稱
     __NAMESPACE__ 返回的是當前名稱空間的名稱

4.2 PHP魔術方法的使用

__get/ __set   # 訪問不存在的屬性
__call/ __callStatic   # 呼叫不存在的方法
__toString   # 物件作為字串使用 __invoke # 物件作為方法使用 

五、三種基本設計模式

5.1 工廠模式

     我們定義一個專門用來建立其它物件的類。 這樣在需要呼叫某個類的時候,我們就不需要去使用new關鍵字例項化這個類,而是通過我們的工廠類呼叫某個方法得到類的例項。
     好處:當我們物件所對應的類的類名發生變化的時候,只需要在工廠類裡面修改即可,而不用一個一個的去修改

class A
{
    //不允許類直接例項化 或克隆
    private function __construct(){} private function __clone(){} } class B { //不允許類直接例項化 或克隆 private function __construct(){} private function __clone(){} } class Factory { public static function getInstance($class) { //類物件的獲取方式通過工廠類 產生 return new $class(); } } //使用 $a = Factory::getInstance('A'); $b = Factory::getInstance('B');

5.2 單例模式

     單例模式的最大好處就是減少資源的浪費,保證整個環境中只存在一個例項化的物件,特別適合資源連線類的編寫。
     只例項化一次,內部例項化,對外只有一個開放方法

// 單例模式(口訣:三私一公)
class Singleton{
    //私有化構造方法,禁止外部例項化物件 private function __construct(){} //私有化__clone,防止物件被克隆 private function __clone(){} //私有化內部例項化的物件 private static $instance = null; // 公有靜態例項方法 public static function getInstance(){ if(self::$instance == null){ //內部例項化物件 self::$instance = new self(); } return self::$instance; } }

5.3 註冊樹模式

     單例模式解決的是如何在整個專案中建立唯一物件例項的問題,工廠模式解決的是如何不通過new建立例項物件的方法。 那麼註冊樹模式想解決什麼問題呢? 在考慮這個問題前,我們還是有必要考慮下前兩種模式目前面臨的侷限。 首先,單例模式建立唯一物件的過程本身還有一種判斷,即判斷物件是否存在。存在則返回物件,不存在則建立物件並返回。 每次建立例項物件都要存在這麼一層判斷。 工廠模式更多考慮的是擴充套件維護的問題。 總的來說,單例模式和工廠模式可以產生更加合理的物件。怎麼方便呼叫這些物件呢?而且在專案內建立的物件好像散兵遊勇一樣,不便統籌管理安排啊。因而,註冊樹模式應運而生。不管你是通過單例模式還是工廠模式還是二者結合生成的物件,都統統給我“插到”註冊樹上。我用某個物件的時候,直接從註冊樹上取一下就好。這和我們使用全域性變數一樣的方便實用。 而且註冊樹模式還為其他模式提供了一種非常好的想法
     註冊器解決物件多次呼叫場景, 減少new新的物件的次數,防止重複建立物件。看物件是否是同一個物件方法,var_dump後看id#1#2等字樣id,它是php內部物件唯一標識

     註冊器模式:Register.php,用來將一些物件註冊到全域性的註冊樹上,可以在任何地方訪問。set():將物件對映到全域性樹上,_unset():從樹上移除,get():去註冊到樹上的物件。

class Register
{
    protected static $objects; //註冊 static function set($alias, $object) { self::$objects[$alias] = $object; } //獲取 static function get($key) { if (!isset(self::$objects[$key])) { return false; } return self::$objects[$key]; } //登出 function _unset($alias) { unset(self::$objects[$alias]); } }

     clipboard.png

5.4 三種基本模式總結

     1、工廠模式的特徵有一個統一生成物件的入口,使用工廠方式生成物件,而不是在程式碼直接new。為了後期更好的擴充套件和修改

     2、單例模式的特徵是物件不可外部例項並且只能例項化一次,當物件已存在就直接返回該物件

     3、註冊樹模式的特徵是物件不用在通過類建立,具有全域性物件樹類。解決物件多次呼叫場景, 減少new新的物件的次數

六、介面卡模式

  1. 可以將截然不同的函式介面封裝成統一的API
  2. 實際應用舉例:PHP的資料庫操作有mysql/mysqli/pdo三種,可以用介面卡模式統一成一致。類似的場景還有cache介面卡,可以將memcache/redis/file/apc等不同的快取函式統一成一致的介面。

     使用介面卡策略是為了更好的相容。類似於手機電源介面卡,如果能用一個充電器對所有手機充電當然是最方便的。無論什麼手機,都只需要拿一個充電器。否則,不同手機不同充電器,太麻煩。

     新建一個介面 IDatabase 然後在這個接口裡面申明統一的方法體,再讓不同的類去實現這個介面,和重寫其抽象方法。當我們在入口檔案使用到不同的類的時候,就只是例項化的類名不同,其它呼叫方法體的地方都一致。

/**
* 1、新建一個介面 IDatabase  然後在這個接口裡面申明統一的方法體
*/
interface IDatabase
{
    //連線 function connect($host, $user, $passwd, $dbname); //查詢 function query($sql); //關閉連線 function close(); } 
/**
* 2、讓不同的類去實現這個介面,和重寫其抽象方法。
*/
class MySQLi implements IDatabase { protected $conn; function connect($host, $user, $passwd, $dbname) { $conn = mysqli_connect($host, $user, $passwd, $dbname); $this->conn = $conn; } function query($sql) { return mysqli_query($this->conn, $sql); } function close() { mysqli_close($this->conn); } } 

七、策略模式

     策略模式:

  1. 策略模式,將一組特定的行為和演算法封裝成類,以適應某些特定的上下文環境,這種模式就是策略模式
  2. 實際應用舉例,假如一個電商網站系統,針對男性女性使用者要各自跳轉到不同的商品類名,並且所有廣告位展示不同的廣告,傳統的做法是加入if...else... 判斷。如果新增加一種使用者型別,只需要新增加一種策略即可
  3. 使用策略模式可以實現Ioc ,依賴倒置,控制反轉,面向物件很重要的一個思想是解耦

     策略模式實現:

  1. 定義一個策略介面檔案,定義策略介面,宣告策略
  2. 定義具體類,實現策略介面,重寫策略方法
// 策略的介面檔案:約定策略的所有行為
interface UserStrategy {
    function showAd(); function showCategory(); } // 實現介面的所有方法 class FemaleUserStrategy implements UserStrategy { function showAd() { echo "2018新款女裝"; } function showCategory() { echo "女裝"; } } // 實現介面的所有方法 class MaleUserStrategy implements UserStrategy { function showAd() { echo "iPhone X"; } function showCategory() { echo "電子產品"; } } 

class Page
{
    /**
     * @var \IMooc\UserStrategy */ protected $strategy; function index() { echo "AD:"; $this->strategy->showAd(); echo "<br/>"; echo "Category:"; $this->strategy->showCategory(); echo "<br/>"; } function setStrategy(\IMooc\UserStrategy $strategy) { $this->strategy = $strategy; } } $page = new Page; if (isset($_GET['female'])) { $strategy = new \IMooc\FemaleUserStrategy(); } else { $strategy = new \IMooc\MaleUserStrategy(); } $page->setStrategy($strategy); $page->index();

     不需要在page類中判斷業務邏輯,如果在index裡面寫邏輯判斷 if男else女 就會存在‘依賴’,這個是不好的,存在很大耦合,所以把邏輯寫在外部,並且在page裡面增加一個set的方法,這個方法的作用就是‘注入’一個物件。只有再使用時才繫結,這樣以後更方便的替換修改MaleUserStratey類,實現了兩個類的解耦,這就是策略模式的依賴倒置,實現了硬編碼到解耦

     依賴倒置原則:
A. 高層次的模組不應該依賴於低層次的模組,他們都應該依賴於抽象
B. 抽象不應該依賴於具體實現,具體實現應該依賴於抽象

     在這裡不管是Page,還是低層次的MaleUserStratey和FemaleUserStrategy都依賴於抽象userStrategy這個抽象,而UserStrategy不依賴於具體實現,具體實現Female和male都依賴於UserStrategy這個抽象。有點繞,應該是這個關係。

八、資料物件對映模式

    資料物件對映模式,是將物件和資料儲存對映起來,對一個物件的操作會對映為對資料儲存的操作,比我們在程式碼中new一個物件,那麼使用該模式就可以將對物件的一些操作,比如說我們設定的一些屬性,它就會自動儲存到資料庫,跟資料庫中表的一條記錄對應起來,資料物件對映模式就是將sql的操作轉化為物件的操作。

    物件關係對映(英語:Object Relation Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程式技術,用於實現面向物件程式語言裡不同型別系統的資料之間的轉換。從效果上說,它其實是建立了一個可在程式語言裡使用的--“虛擬物件資料庫”。
    面向物件是從軟體工程基本原則(如耦合、聚合、封裝)的基礎上發展起來的,而關係資料庫則是從數學理論發展而來的,兩套理論存在顯著的區別。為了解決這個不匹配的現象,物件關係對映技術應運而生。簡單的說:ORM相當於中繼資料

    例項,在程式碼中實現資料物件對映模式,我們將寫一個ORM類,將複雜的SQL語句對映成物件屬性的操作。結合使用資料物件對映模式,工廠模式,註冊模式混合使用

class User
{
    protected $id;
    protected $data; protected $db; protected $change = false; function __construct($id) { $this->db = Factory::getDatabase(); $res = $this->db->query("select * from user where id = $id limit 1"); $this->data = $res->fetch_assoc(); $this->id = $id; } function __get($key) { if (isset($this->data[$key])) { return $this->data[$key]; } } function __set($key, $value) { $this->data[$key] = $value; $this->change = true; } /** * 析構方法 */ function __destruct() { if ($this->change) { foreach ($this->data as $k => $v) { $fields[] = "$k = '{$v}'"; } $this->db->query("update user set " . implode(', ', $fields) . "where id = {$this->id} limit 1"); } } } 
class Factory
{
    /** 註冊$user
     * @param $id * @return User */ static function getUser($id) { $key = 'user_'.$id; $user = Register::get($key); if (!$user) { $user = new User($id); Register::set($key, $user); } return $user; } }

九、觀察者模式

1. 觀察者模式( `Observer` ),當一個物件狀態發生改變時,依賴它的物件全部會收到通知,並自動更新
2. 場景:一個事件發生後,要執行一連串更新操作。傳統的程式設計方式,就是在事件的程式碼之後直接加入處理邏輯。當更新的邏輯增多後,程式碼會變得難以維護。這種方式是耦合的,入侵式的,增加新的邏輯需要修改事件主體的程式碼
3. 觀察者模式實現了低耦合,非入侵式的通知與更新機制

    觀察者模式實現:

  1. 所有觀察者物件實現統一介面
  2. 被觀察物件持有觀察者控制代碼,使用Add觀察者()方法
  3. 某一場合,呼叫觀察方法。Foreach(觀察者控制代碼陣列 as 某一個觀察者)
//事件基類
abstract class EventGenerator { private $observers = array(); function addObserver(Observer $observer) { $this->observers[] = $observer; } function notify() { foreach($this->observers as $observer) { $observer->update(); } } } 
//觀察者介面
interface Observer
{
    function update($event_info = null); }
class Event extends EventGenerator { /** * 觸發事件 */ function trigger() { echo "Event<br/>\n"; $this->notify(); } } class Observer1 implements Observer { function update($event_info = null) { echo "邏輯1<br />\n"; } } class Observer2 implements Observer { function update($event_info = null) { echo "邏輯2<br />\n"; } } $event = new Event; $event->addObserver(new Observer1); $event->addObserver(new Observer2); $event->trigger();

十、原型模式

  1. 原型模式與工程模式作用類似,都是用來建立物件
  2. 與工廠模式的實現不同,原型模式是 先建立好一個原型物件,然後通過clone原型物件來建立新的物件。這樣就免去了類建立時的重複初始化操作
  3. 原型模式適用於大物件的建立,建立一個大物件需要很大的開銷,如果每次都new就會消耗很大,原型模式僅需記憶體拷貝即可

    clipboard.png

十一、裝飾器模式

  1. 裝飾器模式(Decorator),可以動態地新增修改類的功能
  2. 一個類提供了一項功能,如果要在修改並新增額外的功能,傳統的程式設計模式,需要寫一個子類繼承它,並重新實現類的方法
  3. 使用裝飾器模式,僅需在執行時新增一個裝飾器物件即可實現,可以實現最大的靈活性
class Canvas
{
    public $data;
    protected $decorators = array(); //新增裝飾器 function addDecorator(DrawDecorator $decorator) { $this->decorators[] = $decorator; } //執行裝飾器前置操作 先進先出原則 function beforeDraw() { foreach($this->decorators as $decorator) { $decorator->beforeDraw(); } } //執行裝飾器後置操作 先進後出原則 function afterDraw() { //注意,反轉 $decorators = array_reverse($this->decorators); foreach($decorators as $decorator) { $decorator->afterDraw(); } } function draw() { //呼叫裝飾器前置操作 $this->beforeDraw(); foreach($this->data as $line) { foreach($line as $char) { echo $char; } echo "<br />\n"; } //呼叫裝飾器後置操作 $this->afterDraw(); }
//裝飾器介面
interface DrawDecorator
{
    function beforeDraw(); function afterDraw(); }
//實現顏色裝飾器實現介面
class ColorDrawDecorator implements DrawDecorator { protected $color; function __construct($color = 'red') { $this->color = $color; } function beforeDraw() { echo "<div style='color: {$this->color};'>"; } function afterDraw() { echo "</div>"; } }

    clipboard.png

十二、迭代器模式

  1. 迭代器模式,在不需要了解內部實現的前提下,遍歷一個聚合物件的內部元素
  2. 相比傳統的程式設計模式,迭代器模式可以隱藏遍歷元素所需的操作

    應用場景:遍歷資料庫表,拿到所有的user物件,然後用佛 foreach 迴圈,在迴圈的過程中修改某些欄位的

class AllUser implements \Iterator { protected $index = 0; protected $data = []; public function __construct() { $link = mysqli_connect('192.168.0.91', 'root', '123', 'xxx'); $rec = mysqli_query($link, 'select id from doc_admin'); $this->data = mysqli_fetch_all($rec, MYSQLI_ASSOC); } //1 重置迭代器 public function rewind() { $this->index = 0; } //2 驗證迭代器是否有資料 public function valid() { return $this->index < count($this->data); } //3 獲取當前內容 public function current() { $id = $this->data[$this->index]; return User::find($id); } //4 移動key到下一個 public function next() { return $this->index++; } //5 迭代器位置key public function key() { return $this->index; } } //實現迭代遍歷使用者表 $users = new AllUser(); //可實時修改 foreach ($users as $user){ $user->add_time = time(); $user->save(); }

十三、代理模式

  1. 在客戶端與實體之間建立一個代理物件(proxy),客戶端對實體進行的操作全部委派給代理物件,隱藏實體的具體實現細節。
  2. Proxy還可以與業務程式碼分離,部署到另外的伺服器,業務程式碼中通過RPC來委派任務。

    代理模式:資料庫主從,通過代理設定主從讀寫設定

    傳統方式:

    clipboard.png

    需要手動的去選擇主庫和從庫。

    代理模式:

//做約束介面
interface IUserProxy
{
    function getUserName($id); function setUserName($id, $name); }
class Proxy implements IUserProxy { function getUserName($id) { $db = Factory::getDatabase('slave'); $db->query("select name from user where id =$id limit 1"); } function setUserName($id, $name) { $db = Factory::getDatabase('master'); $db->query("update user set name = $name where id =$id limit 1"); } }
$id = 1;
$proxy = new \IMooc\Proxy();
$proxy->getUser($id);
$proxy->setUser($id, array('name' => 'wang'));

十四、面向物件程式設計的基本原則

  1. 單一職責:一個類,只需做好一件事情。不要使用一個類來完成很複雜的功能,而是拆分設計成更小更具體的類。
  2. 開放封閉原則:一個類,應該可以擴充套件,而不可修改。一個類在實現之後,應該是對擴充套件開放,對修是改封閉的,不應該使用修改來增加功能,而是通過擴充套件來增加功能。
  3. 依賴倒置:一個類,不應該強制依賴另一個類。每個類對另外一個類都是可以替換的。如:有A、B兩個類,A需要依賴B類,不應該在A類中直接呼叫B類,而是要使用依賴注入的方式,通過使用注入,將A類依賴的B類的物件注入給A類,B類對於A類來說就是可以替換的。如果C類實現了和B類一樣的介面,那對於A類,B和C也是可以隨意替換的。
  4. 配置化: 儘可能的使用配置,而不是使用硬編碼。資料引數和常量應該放在配置檔案中。像類的關係的定義,也應該是可以配置的。
  5. 面向介面程式設計,而不是面向實現程式設計:只需要關心介面,不需要關心實現。所有的程式碼,它只需要關心某一個類實現了哪些介面,而不需要關心這個類的具體實現。

十五、自動載入配置

    如果實現ArrayAcess介面,則能使一個物件屬性的訪問,可以以陣列的方式進行

class Config implements \ArrayAccess { protected $path; protected $configs = array(); //配置檔案目錄 function __construct($path) { $this->path = $path; } //獲取陣列的key function offsetGet($key) { if (empty($this->configs[$key])) { $file_path = $this->path.'/'.$key.'.php'; $config = require $file_path; $this->configs[$key] = $config; } return $this->configs[$key]; } //設定陣列的key function offsetSet($key, $value) { throw new \Exception("cannot write config file."); } function offsetExists($key) { return isset($this->configs[$key]); } function offsetUnset($key) { unset($this->configs[$key]); } }

    controller.php

$config = array(
    'home' => array(
        'decorator' => array( //'App\Decorator\Login', //'App\Decorator\Template', //'App\Decorator\Json', ), ), 'default' => 'hello world', ); return $config;

    clipboard.png