+2

[Phần 2] Observer pattern

Mở đầu

Trong bài viết trước, mình đã giới thiệu khá chi tiết về Design Pattern cùng với 2 ví dụ về Factory Pattern. Ở bài viết này chúng ta cùng tiếp tục tìm hiểu về 1 pattern khá phổ biến trong PHP đó là Observer

Observer là gì?

Định nghĩa

Mình xin được trích dẫn 1 đoạn trong mô tả của Wikipedia về observer

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

Có thể hiểu Observer thuộc nhóm pattern Behavioral là một mẫu thiết kế dành cho việc một đối tượng khi thay đổi trạng thái của bản thân nó thì các đối tượng đính kèm theo cũng sẽ được thông báo. Trong trường hợp của EDP, một đối tượng phát nổ (trigger) lên một sự kiện, thì các listener được đính kèm sẽ lắng nghe và thực hiện (nếu có).

Tưởng tượng giống như việc 1 giám sát hay 1 ông chủ gửi thông báo cho nhân viên của mình làm gì đó, hay điều gì đó tương tự thế 😄 và đương nhiên vị giám sát này cũng có thể "nghỉ chơi" hoặc điều thêm 1 nhân viên, cũng có thể là "cho đi nghỉ mát" toàn bộ (facepalm). Chúng ta sẽ tìm hiểu kỹ hơn qua ví dụ dưới đây.

Cấu trúc

observer-pattern-uml.png

Sơ đồ 1 mẫu Observer

Mô hình cơ bản của 1 mẫu Observer thường bao gồm 4 thành phần sau.

Subject

Ở đây subject được hiểu như "ông chủ" mà mình đã đề cập, 1 interface với các phương thức

  • attach điều thêm 1 nhân viên đến làm việc (attach observer object)
  • detach rút 1 nhân viên đi nơi khác (detach observer object)
  • notify thông báo cho toàn bộ nhân viên làm 1 việc gì đó (notify observer update)

Observer

Cũng là một interface được định nghĩa với method update, phương thức này sẽ nhận thay đổi mỗi khi có notify từ Subject

ConcreteSubject

Nhiệm vụ là lưu trữ status của các ConcreteObserver objects và từ các state này sẽ gửi đi thông báo mỗi khi state bị thay đổi

  • setState optional - set state cho Subject
  • getState optional - lấy state hiện tại của Subject

ConcreteObserver

  • Luôn duy trì 1 reference đến một ConcreteSubject
  • Cần phải lưu trữ state phù hợp với state của Subject
  • Implements phương thức update của Observer để state luôn đồng nhất với state của Subject (sử dụng mỗi khi có notify)

Ví dụ

Giả sử mỗi lần có người đăng nhập vào hệ thống, chúng ta sẽ ghi log hoặc báo là người đó mới đăng nhập và block ip nếu user đó là không hợp lệ. Hoặc sẽ check nếu tài khoản hết hạn thì sẽ gửi email thông báo...

Như vậy, chúng ta sẽ làm 1 ví dụ và ở đây mình sẽ áp dụng với mô hình Account login cùng với Logger và Mailer, Security, cụ thể như sau.

Flow

  1. ConcreteSubject (Account) sẽ thông báo tới tất cả các observers(Logger và Mailer, Security) bất cứ khi nào có một user thực hiện đăng nhập.
  2. Sau khi nhận được thông báo rằng có user đăng nhập và gọi tới update, các ConcreteObserver (Logger và Mailer, Security ) sẽ sử dụng dữ liệu từ ConcreteSubject và lấy state để xử lý.

Bước 1

Tạo SubjectObserver

Observer
<?php

interface Observer
{
	public function update();
}
Subject
<?php

interface Subject
{
	public function attach(Observer $observer);
	public function detach(Observer $observer);
	public function notify();
}

Bước 2

Tạo ConcreteSubject implements Subject tạo ở trên, mình đặt tên là Account

<?php

class Account implements Subject
{

	const LOGIN_SUCCESS = 1;
	const LOGIN_FAILURE = 2;
    const LOGIN_IVALID = 3;
    const EXPIRED = 4;

	private $state;
	private $storage;

    private $data;

	public function __construct()
	{
		$this->storage = array();
		$this->data = array();
	}
    // Attach 1 Observer
	public function attach(Observer $observer)
	{
		$isContain = array_search($observer, $this->storage);
        if ($isContain === false) {
            $this->storage[] = $observer;
        }
	}
    // Xóa 1 Observer ra khỏi danh sách
	public function detach(Observer $observer)
	{
      foreach($this->storage as $key => $val) {
        if ($val == $observer) {
          unset($this->storage[$key]);
        }
      }
    }
    // Gửi thông báo update tới tất cả các observers trong hệ thống
    public function notify() {
	    foreach($this->storage as $observer) {
	     	$observer->update($this);
	     }
    }

    public function login($email, $ip)
    {
    	$this->setData([
    	    'email' => $email,
    	    'ip' => $ip
    	]);
    	if ($email == 'hack@framgia.com' && $ip == '10.0.0.1') {
    	    $this->setState(Account::LOGIN_INVALID)
    	} else {
    	    $login = $this->process($email);
    	   if ($login) {
    		    $this->setState(Account::LOGIN_SUCCESS);
    	    } else {
			    $this->setState(Account::LOGIN_FAILURE);
    	    }
    	}

    	$this->notify();
    }

    public function save()
    {
        $this->notify();
    }

    public function setState($state)
    {
        $this->state = $state;
    }

    public function getState()
    {
        return $this->state;
    }

    public function process($email)
    {
        if ($email == 'thanhsm93@gmail.com') {
            return true;
        }
        return false;
    }

    public function setData($data)
    {
        $this->data = $data;
    }

    public function getData()
    {
        return $this->data;
    }
}

Bước 3

Tạo các ConcreteSubject

Logger

class Logger implements Observer
{
    public function update(Account $account)
    {
        $state = $account->getState();
        $data = $account->getData()
        if ($state == Account::LOGIN_SUCCESS) {
            // thực hiện log thời gian user online blahh..
            echo  "User {$data['email']} vừa online";
        }
    }
}

Mailer

class Mailer implements Observer
{
    public function update(Account $account)
    {
        $state = $account->getState();
        $data = $account->getData();
        if ($state == Account::EXPIRED) {
            // Gửi email thông tin tài khoản đã hết hạn
            Email::send($email, "Account hết hạn rồi bạn ei");
            echo "Account $data['email'] has expired. Email sent!";
        }
    }
}

Security

class Security implements Observer
{
    public function update(Account $account)
    {
        $state = $account->getState();
        $data = $account->getData();
        if ($state == Account::LOGIN_INVALID) {
            // Block ip
            echo "Account $data['email'] with ip $data['ip'] are trying to hack our system";
        }
    }
}

Bước 5

Chạy thử thôi 😄

<?php
// Include các class
include ...
$account = new Account();
//Attach các observer vào subject
$security = new Security();
$account->attach(new Logger());
$account->attach(new Mailer());
$account->attach($security);
// Đăng nhập
$account->login('thanhsm93@gmail.com', '192.168.0.1');
// Thay đổi state
$account->setState(Account::EXPIRED);
$account->save();
$account->login('hack@framgia.com', '10.0.0.1');
// Xóa security observer
$account->detach($security);
$account->login('hack@framgia.com', '10.0.0.1'); //will success

Output

> User thanhsm93@gmail.com vừa online
> Account thanhsm93@gmail.com has expired. Email sent!
> Account thanhsm93@gmail.com with ip 10.0.0.1 are
trying to hack our system

Kết

Như vậy là đã tìm hiểu xong về Observer và cách ứng dụng trong PHP, nếu không dùng PHP các bạn vẫn có thể áp dụng theo cách xây dựng tương tự 😄 Mình sẽ tìm hiểu thêm về các pattern phổ biến hơn ở tất cả ngôn ngữ và chia sẻ lại trong các bài viết tiếp theo.

Nguồn tham khảo

Feedback

Trong quá trình tìm hiểu có thể còn sai sót, rất mong các bạn nếu thấy điều gì không đúng trong bài viết xin vui lòng comment lại để mình chỉnh sửa và cải thiện trong các bài viết sau. Cảm ơn!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí