1. 程式人生 > >PHP設計模式-觀察者模式(訂閱者模式)

PHP設計模式-觀察者模式(訂閱者模式)

相信大家都用過QQ(沒用過QQ的大叔不要扔我),而且大家都很討厭QQ的小彈窗,不時地就會跳出一個小視窗,真心煩人。那麼如果我們是騰訊訊息推送的服務端開發人員。如果要用PHP來實現這種訊息傳送那麼如果做到呢?

    方案一。被動推送方式

我們採用推的方式來接收訊息。也說說,由服務端向各位使用者直接推送訊息。我們考慮地簡單一點,畢竟我們只是學習設計模式嘛。首先,我們需要有一個使用者類。可以展示推送的訊息。其次,我們需要一個訊息推送器的類,有個推送訊息的方法,它可以指定把訊息推給哪個使用者。

class User
{

	//展示推送過來的訊息
    public function showMessage($msg)
    {
        echo "Message: $msg".PHP_EOL;
    }

}

class Messager
{

	//推送訊息給某使用者
    public function push(User $user, String $msg)
    {
        $user->showMessage($msg);
    }
	
}

好,需要的東西都有了。那麼,該把訊息推給誰呢?嗯,有了,我先把要推送訊息的使用者都存在一個列表中。當有新訊息需要推送的時候,我直接推送給每個使用者就可以了。於是,把推送器稍做修改如下:

class Messager
{

    //用來儲存需要推送訊息的使用者
    protected $_users = array();

    //推送訊息給某使用者
    public function push(User $user, String $msg)
    {
        $user->showMessage($msg);
    }

    //將訊息推送給所有使用者
    public function pushAll($msg)
    {
        foreach ($this->_users as $user) {
            $this->push($user, $msg);
        }
    }
}

推送器可以通過pushAll()方法給所有使用者推送訊息了。我們發現,我們還缺少一些東西,對,我們需要一些把使用者從推送器新增或刪除的方法。同時,我們需要注意一下,使用者需要一個唯一的標識來區分是哪個使用者。於是在使用者類中新增一個使用者標識userId,並且,建立使用者時需指定這個Id.而推送器在新增和刪除使用者時,都會使用這個Id。程式碼如下:

class User
{

    //使用者的唯一標識
    private $_userId;

    //使用者初始化時需指定ID
    public function __construct($userId)
    {
        $this->_userId = $userId;
    }

    //獲取使用者ID
    public function getUserId()
    {
        return $this->_userId;
    }

    //展示推送過來的訊息 
    public function showMessage($msg)
    {
        echo "Message: $msg".PHP_EOL;
    }
}

class Messager
{

    //用來儲存需要推送訊息的使用者
    protected $_users = array();

    //推送訊息給某使用者
    public function push(User $user, $msg)
    {
        $user->showMessage($msg);
    }

    //將訊息推送給所有使用者
    public function pushAll($msg)
    {
        foreach ($this->_users as $user) {
            $this->push($user, $msg);
        }
    }

    //新增使用者
    public function addUser(User $user)
    {
        $this->_users[$user->getUserId()] = $user;
    }

    //刪除使用者
    public function delUser($userId)
    {
        unset($this->_users[$userId]);
    }                       

    //清除所有使用者
    public function clearUsers()
	{
        $this->_users = array();
    }
}

        推送方式的程式碼完成了。現在我們開始測試一下,給兩個使用者傳送訊息。

$messager = new Messager();
$user1 = new User(1);
$user2 = new User(2);
$messager->addUser($user1);
$messager->addUser($user2);
$messager->pushAll("test");

我們看到,推送器建立了一個數組,用來存所有需要推送訊息的使用者。其實我們可以把這些使用者叫做觀察者,或者叫訂閱者。把它們加入到這個陣列中,就表示他們對這個訊息器中的訊息很關心。那麼訊息推送器在有新訊息的時候,就遍歷這個陣列,把訊息推送出去。

        方案二。拉取方式

如果這個使用者量很大呢?陣列存不下怎麼辦?如果使用者不線上,訊息也會推送不出去。那這種推的方式就不適用了。

我們就會想了,那我們改成拉的模式吧。這樣我們不用維護大的使用者列表,也不用關心使用者在不線上了。當用戶上線後,可以連線到訊息器上獲取訊息。

要實現拉的模式。首先使用者還是要有顯示訊息的方法,但不同之處在於,它需要知道訊息器是什麼?那麼訊息器就簡單了,要有一個可以獲取訊息的方法。並且有一個訊息器的唯一標識(messageId)如下:

class User
{

    //使用者的唯一標識
    private $_userId;

    //使用者初始化時需指定ID
    public function __construct($userId)
    {
        $this->_userId = $userId;
    }

    //獲取使用者ID
    public function getUserId()
    {
        return $this->_userId;
    }

    //展示訊息推送器的訊息,它需要傳遞進來一個訊息器
    public function showMessage(Messager $messager)
    {
        echo "Message".$messager->getMessage();
    }
}

class Messager 
{

    //訊息器Id
    private $_messagerId;

    //訊息器初始化時需要指定Id
    public function __construct($messagerId)
    {
        $this->_messagerId = $messagerId;
    }

    //獲取Messager的Id
    public function getMessagerId()
    {
        return $this->_messagerId;
    }

    //從遠端獲取訊息資訊
    public function getMessage()
    {
        //此處實現從服務端拿訊息 ...
        return "遠端拿到的訊息";
    }
}

在上面,使用者可以支援從一個訊息器拉訊息。那麼,如果有多個訊息器呢?我們需要在使用者中儲存一個推送器的列表,然後定時遍歷推送器列表,獲取訊息。於是最終程式碼如下:

class User
{

    //使用者的唯一標識
    private $_userId;

    //推送器列表
    protected $_messagers;

    //使用者初始化時需指定ID
    public function __construct($userId)
    {
        $this->_userId = $userId;
    }

    //獲取使用者ID
    public function getUserId()
    {
        return $this->_userId;
    }

    //新增訊息器
    public function addMessager(Messager $messager)
    {
        $this->_messagers[$messager->getMessagerId()] = $messager;
    }

    //移除訊息器
    public function removeMessager($messagerId)
    {
        unset($this->_messagers[$messagerId]);
    }

    //清除訊息器
    public function clearMessagers()
    {
        $this->_messagers = array();
    }

    //顯示所有訊息並顯示
    public function showAllMessage()
    {
        foreach ($this->_messagers as $messager) {
            $this->showMessage($messager);
        }
    }

    //展示訊息推送器的訊息,它需要傳遞進來一個訊息器
    public function showMessage(Messager $messager)
    {
        echo "Message".$messager->getMessage();
    }
}

class Messager
{

    //訊息器Id
    private $_messagerId;

    //訊息器初始化時需要指定Id
    public function __construct($messagerId)
    {
        $this->_messagerId = $messagerId;
    }

    //獲取Messager的Id
    public function getMessagerId()
    {
        return $this->_messagerId;
    }


    //從遠端獲取訊息資訊
    public function getMessage()
    {
        //此處實現從服務端拿訊息 ...
        return "遠端拿到的訊息";
    }
}

        拉模式的程式碼我們也完成了。現在來測試一下效果:

$user = new User(1);
$messager1 = new Messager(1);
$messager2 = new Messager(2);
$user->addMessager($messager1);
$user->addMessager($messager2);
$user->showAllMessage();

        從上面推拉模式兩種程式碼我們可以看出,它們的區別在於,一個是在訊息器中儲存使用者列表,另一個是在使用者類中儲存訊息器列表,要獲取和推送訊息時,都要遍歷這個列表進行推送和拉取訊息。

對於拉模式而言,它只需維護感興趣的訊息器列表。但它並不知道,訊息器什麼時候會有新訊息。它只能定時地去試探是否有新訊息過來。而且,每個使用者都必須維護一個訊息器列表。所以會重複地試探性拉資料。效率不高。但訊息器服務端服務都是正常的,只要使用者去取訊息,那麼基本上都能正常取到,因此更穩定些。故而,兩種模式要根據不同的業務情況合理使用。