zoukankan      html  css  js  c++  java
  • 设计模式:灵活编程(观察者模式)

    系统中的每个类应将重点放在某一个功能上,而不是其他方面。一个对象只做一件事情,并且将他做好。

    定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,有可能导致其它依赖对象的修改更新,那么开发任务会很快变成一个产生bug和消除bug的恶性循环。当我们创建一个对象的时候,一个对象的创建应当尽可能减少和其它对象间的耦合!一个对象的改变尽可能的不会引起代码库其它地方的修改。使用观察者模式能有效的解决此问题,一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知并被自动更新。

    观察者模式(有时又被称为模型-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

    问题

    假设一个负责处理用户登录的类:

    class Login
    {
        const LOGIN_USER_UNKNOWN = 1;
        const LOGIN_WRONG_PASS   = 2;
        const LOGIN_ACCESS       = 3;
        private $_status = array();
    
        public function handleLogin($user, $pass, $ip)
        {
            switch (rand(1,3))
            {
                case self::LOGIN_ACCESS:
                    $this->setStatus(self::LOGIN_ACCESS, $user, $ip);
                    $ret = true; break;
                case self::LOGIN_WRONG_PASS:
                    $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
                    $ret = false; break;
                case self::LOGIN_USER_UNKNOWN:
                default:
                    $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
                    $ret = false; break;
            }
            return $ret;
        }
    
        private function setStatus($status, $user, $ip)
        {
            $this->_status = array($status, $user, $ip);
        }
    
        public function getStatus()
        {
            return $this->_status;
        }
    }
    
    $login = new Login();
    $login->handleLogin('BNDong', '123456', '127.0.0.1');
    var_dump($login->getStatus());

    当然这个类并没有实际功能, handleLogin 方法会存储验证用户数据,该方法有3个潜在的结果。状态标签会被设置为 LOGIN_USER_UNKNOWN 、 LOGIN_WRONG_PASS 或 LOGIN_ACCESS 。

    现在看上去还可以,但是一个登录组件可不可能只有这面点东西,我们试着增加需求:(代码的腐败就是不断的迭代出来的)

    记录登录IP地址

    public function handleLogin($user, $pass, $ip)
    {
        ...
        Logger::logIp($user, $ip, $this->getStatus());
        ...
    }

    登录失败发送邮件通知管理员

    public function handleLogin($user, $pass, $ip)
    {
        ...
        !$ret && Notifier::mailWarning($user, $ip, $this->getStatus());
        ...
    }

    当然这些都是简单的功能,但是依这种方式来处理 Login 类,会发现该类和系统的依赖越来越深,代码的扩展和复用性越来越差! handleLogin 处理的东西越来越多。

    实现

    观察者模式的核心是把客户元素(观察者)从一个中心类(主体)中分离开来。当主体知道事件发生时,观察者需要被通知到。同时,我们并不希望将主体与观察者之间的关系进行硬编码。

    为了达到这个目的,我们允许观察者在主体上进行注册。

    interface Observable
    {
        public function attach(Observer $observer);
        public function detach(Observer $observer);
        public function notify();
    }
    
    class Login implements Observable
    {
        const LOGIN_USER_UNKNOWN = 1;
        const LOGIN_WRONG_PASS   = 2;
        const LOGIN_ACCESS       = 3;
        private $_status = array();
        private $_observers;
    
        public function __construct()
        {
            $this->_observers = array();
        }
    
        public function handleLogin($user, $pass, $ip)
        {
            switch (rand(1,3))
            {
                case self::LOGIN_ACCESS:
                    $this->setStatus(self::LOGIN_ACCESS, $user, $ip);
                    $ret = true; break;
                case self::LOGIN_WRONG_PASS:
                    $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
                    $ret = false; break;
                case self::LOGIN_USER_UNKNOWN:
                default:
                    $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
                    $ret = false; break;
            }
            $this->notify();
            return $ret;
        }
    
        private function setStatus($status, $user, $ip)
        {
            $this->_status = array($status, $user, $ip);
        }
    
        public function getStatus()
        {
            return $this->_status;
        }
    
        public function attach(Observer $observer)
        {
            $this->_observers[] = $observer;
        }
    
        public function detach(Observer $observer)
        {
            $newobservers = array();
            foreach ($this->_observers as $obs) {
                if ($obs !== $observer) {
                    $newobservers[] = $obs;
                }
            }
            $this->_observers = $newobservers;
        }
    
        public function notify()
        {
            foreach ($this->_observers as $obs) {
                $obs->update($this);
            }
        }
    }

    现在 Login 类管理着一系列观察者对象。这些观察者可以由第三方通过 attach 方法添加进 Login 类,也可以通过 detach 方法来移除。 notify 方法用来告诉观察者一些相关事情发生了。 notify 方法会遍历观察者列表,调用每个观察者的 update 方法。

     Login 类在它的 handleLogin 方法中调用 notify 方法。然后定义 Observer 接口,任何实现这个接口的对象都可以通过 attach 方法加入 Login 类中。

    interface Observer
    {
        public function update(Observable $observable);
    }
    
    class SecurityMonitor implements Observer
    {
        public function update(Observable $observable)
        {
            $status = $observable->getStatus();
            if ($status[0] == Login::LOGIN_WRONG_PASS) {
                // 发送邮件给系统管理员
                print __CLASS__.":发送邮件给系统给管理员<br>";
            }
        }
    }
    
    $login = new Login();
    $login->attach(new SecurityMonitor());
    $login->handleLogin('BNDong', '123456', '127.0.0.1');

    至此实现了一个观察者模式,减少了各个对象之间的耦合。

    优化

    这里还存在一个问题,获取主体类状态是通过 getStatus 方法来获取的,而并不能判断调用的  getStatus 方法是存在并且可用的,所以要解决这个问题。

    第一种方法:修改接口 Observer 中 update 方法参数 $observable 类型约束为 Login ,但是这样整个结构就被一个类限制了,多个登录类不能兼容,所以不推荐!!

    第二种方法:在接口 Observable 中添加 getStatus 方法,但是这样会失去接口的通用性!!

    第三种方法:继续保持 Observable 接口的通用性,将会添加 Observer 类型的对象来执行一些它们共有的任务。

    下面针对第三种方法来优化上面的代码:

    使用自建类优化

     创建一个抽象超类:

    abstract class LoginObserver implements Observer
    {
        private $_login;
    
        public function __construct(Login $login)
        {
            $this->_login = $login;
            $login->attach($this);
        }
    
        public function update(Observable $observable)
        {
            if ($observable == $this->_login) {
                $this->doUpdate($observable);
            }
        }
    
        abstract protected function doUpdate(Login $login);
    }

      LoginObserver 类的构造函数需要一个 Login 对象作为参数。 LoginObserver 保存对 Login 对象的引用,并且调用 Login::attach() 方法。当 update 方法被调用时, LoginObserver 会检查参数传入的 Observable 对象是否是正确的引用,然后 LoginObserver 会调用模板方法 doUpdate 。现在可以创建一批 LoginObserver 对象,它们能够判断使用的是 Login 对象,而不是任意 Observable 对象:

    class SecurityMonitor extends LoginObserver
    {
        public function doUpdate(Login $login)
        {
            $status = $login->getStatus();
            if ($status[0] == Login::LOGIN_WRONG_PASS) {
                // 发送邮件给系统管理员
                print __CLASS__.":发送邮件给系统给管理员<br>";
            }
        }
    }
    
    class GeneralLogger extends LoginObserver
    {
        public function doUpdate(Login $login)
        {
            $status = $login->getStatus();
            // 记录登录数据到日志
            print __CLASS__.":记录登录数据到日志<br>";
        }
    }
    
    $login = new Login();
    new SecurityMonitor($login);
    new GeneralLogger($login);
    $login->handleLogin('BNDong', '123456', '127.0.0.1');

     因此在主体类和观察者之间创建了一个很灵活的关系。

    使用PHP内置SPL优化

    PHP通过内置的SPL(Standard PHP Library,PHP标准类)扩展提供了对观察者模式的原生支持。其中的观察者(Observer)由3个元素组成:SplObserver、SplSubject 和 SplObjectStorage。SplObserver 和 SplSubject 都是接口,与之前示例中的 Observer 和 Observable 接口完全相同。SplObjectStorage 是一个工具类,用于更好的存储对象和删除对象。

    /**
     * The <b>SplSubject</b> interface is used alongside
     * <b>SplObserver</b> to implement the Observer Design Pattern.
     * @link http://php.net/manual/en/class.splsubject.php
     */
    interface SplSubject  {
    
            /**
             * Attach an SplObserver
             * @link http://php.net/manual/en/splsubject.attach.php
             * @param SplObserver $observer <p>
         * The <b>SplObserver</b> to attach.
             * </p>
             * @return void 
             * @since 5.1.0
             */
            public function attach (SplObserver $observer);
    
            /**
             * Detach an observer
             * @link http://php.net/manual/en/splsubject.detach.php
             * @param SplObserver $observer <p>
         * The <b>SplObserver</b> to detach.
             * </p>
             * @return void 
             * @since 5.1.0
             */
            public function detach (SplObserver $observer);
    
            /**
             * Notify an observer
             * @link http://php.net/manual/en/splsubject.notify.php
             * @return void 
             * @since 5.1.0
             */
            public function notify ();
    
    }
    SplSubject
    /**
     * The <b>SplObserver</b> interface is used alongside
     * <b>SplSubject</b> to implement the Observer Design Pattern.
     * @link http://php.net/manual/en/class.splobserver.php
     */
    interface SplObserver  {
    
            /**
             * Receive update from subject
             * @link http://php.net/manual/en/splobserver.update.php
             * @param SplSubject $subject <p>
         * The <b>SplSubject</b> notifying the observer of an update.
             * </p>
             * @return void 
             * @since 5.1.0
             */
            public function update (SplSubject $subject);
    
    }
    SplObserver
    /**
     * The SplObjectStorage class provides a map from objects to data or, by
     * ignoring data, an object set. This dual purpose can be useful in many
     * cases involving the need to uniquely identify objects.
     * @link http://php.net/manual/en/class.splobjectstorage.php
     */
    class SplObjectStorage implements Countable, Iterator, Traversable, Serializable, ArrayAccess {
    
            /**
             * Adds an object in the storage
             * @link http://php.net/manual/en/splobjectstorage.attach.php
             * @param object $object <p>
             * The object to add.
             * </p>
             * @param mixed $data [optional] <p>
             * The data to associate with the object.
             * </p>
             * @return void 
             * @since 5.1.0
             */
            public function attach ($object, $data = null) {}
    
            /**
         * Removes an object from the storage
             * @link http://php.net/manual/en/splobjectstorage.detach.php
             * @param object $object <p>
             * The object to remove.
             * </p>
             * @return void 
             * @since 5.1.0
             */
            public function detach ($object) {}
    
            /**
             * Checks if the storage contains a specific object
             * @link http://php.net/manual/en/splobjectstorage.contains.php
             * @param object $object <p>
             * The object to look for.
             * </p>
         * @return bool true if the object is in the storage, false otherwise.
             * @since 5.1.0
             */
            public function contains ($object) {}
    
            /**
             * Adds all objects from another storage
             * @link http://php.net/manual/en/splobjectstorage.addall.php
             * @param SplObjectStorage $storage <p>
             * The storage you want to import.
             * </p>
             * @return void 
             * @since 5.3.0
             */
        public function addAll ($storage) {}
    
            /**
             * Removes objects contained in another storage from the current storage
             * @link http://php.net/manual/en/splobjectstorage.removeall.php
             * @param SplObjectStorage $storage <p>
             * The storage containing the elements to remove.
             * </p>
             * @return void 
             * @since 5.3.0
             */
        public function removeAll ($storage) {}
    
            /**
         * Removes all objects except for those contained in another storage from the current storage
         * @link http://php.net/manual/en/splobjectstorage.removeallexcept.php
         * @param SplObjectStorage $storage <p>
         * The storage containing the elements to retain in the current storage.
         * </p>
         * @return void
         * @since 5.3.6
         */
        public function removeAllExcept ($storage) {}
    
        /**
             * Returns the data associated with the current iterator entry
             * @link http://php.net/manual/en/splobjectstorage.getinfo.php
             * @return mixed The data associated with the current iterator position.
             * @since 5.3.0
             */
            public function getInfo () {}
    
            /**
             * Sets the data associated with the current iterator entry
             * @link http://php.net/manual/en/splobjectstorage.setinfo.php
             * @param mixed $data <p>
             * The data to associate with the current iterator entry.
             * </p>
             * @return void 
             * @since 5.3.0
             */
            public function setInfo ($data) {}
    
            /**
             * Returns the number of objects in the storage
             * @link http://php.net/manual/en/splobjectstorage.count.php
             * @return int The number of objects in the storage.
             * @since 5.1.0
             */
            public function count () {}
    
            /**
             * Rewind the iterator to the first storage element
             * @link http://php.net/manual/en/splobjectstorage.rewind.php
             * @return void 
             * @since 5.1.0
             */
            public function rewind () {}
    
            /**
             * Returns if the current iterator entry is valid
             * @link http://php.net/manual/en/splobjectstorage.valid.php
         * @return bool true if the iterator entry is valid, false otherwise.
             * @since 5.1.0
             */
            public function valid () {}
    
            /**
             * Returns the index at which the iterator currently is
             * @link http://php.net/manual/en/splobjectstorage.key.php
             * @return int The index corresponding to the position of the iterator.
             * @since 5.1.0
             */
            public function key () {}
    
            /**
             * Returns the current storage entry
             * @link http://php.net/manual/en/splobjectstorage.current.php
             * @return object The object at the current iterator position.
             * @since 5.1.0
             */
            public function current () {}
    
            /**
             * Move to the next entry
             * @link http://php.net/manual/en/splobjectstorage.next.php
             * @return void 
             * @since 5.1.0
             */
            public function next () {}
    
            /**
             * Unserializes a storage from its string representation
             * @link http://php.net/manual/en/splobjectstorage.unserialize.php
             * @param string $serialized <p>
             * The serialized representation of a storage.
             * </p>
             * @return void 
             * @since 5.2.2
             */
            public function unserialize ($serialized) {}
    
            /**
             * Serializes the storage
             * @link http://php.net/manual/en/splobjectstorage.serialize.php
             * @return string A string representing the storage.
             * @since 5.2.2
             */
            public function serialize () {}
    
            /**
             * Checks whether an object exists in the storage
             * @link http://php.net/manual/en/splobjectstorage.offsetexists.php
             * @param object $object <p>
             * The object to look for.
             * </p>
         * @return bool true if the object exists in the storage,
             * and false otherwise.
             * @since 5.3.0
             */
            public function offsetExists ($object) {}
    
            /**
             * Associates data to an object in the storage
             * @link http://php.net/manual/en/splobjectstorage.offsetset.php
             * @param object $object <p>
             * The object to associate data with.
             * </p>
         * @param mixed $data [optional] <p>
             * The data to associate with the object.
             * </p>
             * @return void 
             * @since 5.3.0
             */
        public function offsetSet ($object, $data = null) {}
    
            /**
             * Removes an object from the storage
             * @link http://php.net/manual/en/splobjectstorage.offsetunset.php
             * @param object $object <p>
             * The object to remove.
             * </p>
             * @return void 
             * @since 5.3.0
             */
            public function offsetUnset ($object) {}
    
            /**
             * Returns the data associated with an <type>object</type>
             * @link http://php.net/manual/en/splobjectstorage.offsetget.php
             * @param object $object <p>
             * The object to look for.
             * </p>
             * @return mixed The data previously associated with the object in the storage.
             * @since 5.3.0
             */
            public function offsetGet ($object) {}
    
            /**
             * Calculate a unique identifier for the contained objects
             * @link http://php.net/manual/en/splobjectstorage.gethash.php
             * @param $object  <p>
             * object whose identifier is to be calculated.
             * @return string A string with the calculated identifier.
             * @since 5.4.0
            */
            public function getHash($object) {}
    
    }
    SplObjectStorage

    下面是改进过的示例代码:

    class Login implements SplSubject
    {
        const LOGIN_USER_UNKNOWN = 1;
        const LOGIN_WRONG_PASS   = 2;
        const LOGIN_ACCESS       = 3;
        private $_status = array();
        private $_storage;
    
        public function __construct()
        {
            $this->_storage = new SplObjectStorage();
        }
    
        public function handleLogin($user, $pass, $ip)
        {
            switch (rand(1,3))
            {
                case self::LOGIN_ACCESS:
                    $this->setStatus(self::LOGIN_ACCESS, $user, $ip);
                    $ret = true; break;
                case self::LOGIN_WRONG_PASS:
                    $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip);
                    $ret = false; break;
                case self::LOGIN_USER_UNKNOWN:
                default:
                    $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip);
                    $ret = false; break;
            }
            $this->notify();
            return $ret;
        }
    
        private function setStatus($status, $user, $ip)
        {
            $this->_status = array($status, $user, $ip);
        }
    
        public function getStatus()
        {
            return $this->_status;
        }
    
        public function attach(SplObserver $observer)
        {
            $this->_storage->attach($observer);
        }
    
        public function detach(SplObserver $observer)
        {
            $this->_storage->detach($observer);
    
        }
    
        public function notify()
        {
            foreach ($this->_storage as $obs) {
                $obs->update($this);
            }
        }
    }
    
    abstract class LoginObserver implements SplObserver
    {
        private $_login;
    
        public function __construct(Login $login)
        {
            $this->_login = $login;
            $login->attach($this);
        }
    
        public function update(SplSubject $subject)
        {
            if ($subject == $this->_login) {
                $this->doUpdate($subject);
            }
        }
    
        abstract protected function doUpdate(Login $login);
    }
    
    class SecurityMonitor extends LoginObserver
    {
        public function doUpdate(Login $login)
        {
            $status = $login->getStatus();
            if ($status[0] == Login::LOGIN_WRONG_PASS) {
                // 发送邮件给系统管理员
                print __CLASS__.":发送邮件给系统给管理员<br>";
            }
        }
    }
    
    class GeneralLogger extends LoginObserver
    {
        public function doUpdate(Login $login)
        {
            $status = $login->getStatus();
            // 记录登录数据到日志
            print __CLASS__.":记录登录数据到日志<br>";
        }
    }
    
    $login = new Login();
    new SecurityMonitor($login);
    new GeneralLogger($login);
    $login->handleLogin('BNDong', '123456', '127.0.0.1');

    参考资料

    《深入PHP面向对象、模式与实践》(第三版)

    https://baike.baidu.com/item/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F/5881786?fr=aladdin

    http://www.runoob.com/design-pattern/observer-pattern.html

  • 相关阅读:
    css注入获取网页中的数据
    跨路径读取cookie
    python 网络爬虫介绍
    ssh无法登录,提示Connection closing...Socket close.
    Tengine 添加第三方监控模块nginx-module-vts
    使用nginx很卡之strace命令
    MySQL清理慢查询日志slow_log的方法
    Python之json模块
    zabbix3调用接口发送短信告警
    RabbitMQ 安装 rabbitmq_delayed_message_exchange插件
  • 原文地址:https://www.cnblogs.com/bndong/p/9448051.html
Copyright © 2011-2022 走看看