zoukankan      html  css  js  c++  java
  • 设计模式-观察者模式上

    观察者模式可以说是非常贴近我们生活的一个设计模式,为什么这么说呢?哲学上有这么一种说法,叫做“万事万物皆有联系”,原意是说世上没有孤立存在的事物,但其实也可以理解为任何一个事件的发生必然由某个前置事件引起,也必然会导致另一个后置事件。我们的生活中,充斥着各种各样的相互联系的事件,而观察者模式,主要就是用于处理这种事件的一套解决方案。

    示例

    观察者模式在不同需求下,实现方式也不尽相同,我们还是举一个例子,然后通过逐步的改进来深刻感受一下它是如何工作的。

    在中学阶段有一篇课文《口技》,其中有一句“遥闻深巷中犬吠,便有妇人惊觉欠伸,其夫呓语。既而儿醒,大啼。”应该不用翻译吧?我们接下来就是要通过程序模拟一下这个场景。

    先看看他们之间的关系,如下图所示:

    初版实现

    一声狗叫引发了一系列的事件,需求很清晰,也很简单。于是,我们可以很容易的得到如下实现:

    public class Wife
    {
        public void Wakeup()
        {
            Console.WriteLine("便有妇人惊觉欠伸");
        }
    }
    
    public class Husband
    {
        public void DreamTalk()
        {
            Console.WriteLine("其夫呓语");
        }
    }
    
    public class Son
    {
        public void Wakeup()
        {
            Console.WriteLine("既而儿醒,大啼");
        }
    }
    
    public class Dog
    {
        private readonly Wife _wife = new Wife();
        private readonly Husband _husband = new Husband();
        private readonly Son _son = new Son();
        public void Bark()
        {
            Console.WriteLine("遥闻深巷中犬吠");
    
            _wife.Wakeup();
            _husband.DreamTalk();
            _son.Wakeup();
        }
    }
    

    功能实现了,调用很简单,就不上代码了,从Dog类中可以看出,确实是狗叫触发了后续的一系列事件。但是,有一定经验的人一定很快就会发现,这里至少违反了开闭原则和迪米特原则,最终会导致扩展维护起来比较麻烦。因此,需要改进,而改进的方法也不难想到,无非就是抽象出一个基类或接口,让面向实现编程的部分变成面向抽象编程,而真正关键的是抽象什么的问题。难道是抽象一个基类,然后让Wife,Husband,Son继承自该基类吗?他们都是家庭成员,看似好像可行,但它们并没有公共的实现,而且如果后续再加入猫,老鼠或者其它什么的呢?就会变得更加风马牛不相及。面对这种未知的变化,显然很难抽象出一个公共的基类,而针对“观察事件发生”这个行为抽象出接口或许更合适。

    演进一-简易观察者模式

    根据这个思路,下面看看改进后的实现,先定义一个公共的接口:

    public interface IObserver
    {
        void Update();
    }
    

    这里定义了一个跟任何子类都无关的void Update()方法,这也是没办法的办法,因为我们不可能直接对Wakeup()或者DreamTalk()方法进行抽象,只能通过这种方式规范一个公共的行为接口,意思是当被观察的事件发生时,更新具体实例的某些状态。而具体实现类就简单了:

    public class Wife : IObserver
    {
        public void Update()
        {
            Wakeup();
        }
    
        public void Wakeup()
        {
            Console.WriteLine("便有妇人惊觉欠伸");
        }
    }
    
    public class Husband: IObserver
    {
        public void DreamTalk()
        {
            Console.WriteLine("其夫呓语");
        }
    
        public void Update()
        {
            DreamTalk();
        }
    }
    
    public class Son : IObserver
    {
        public void Update()
        {
            Wakeup();
        }
    
        public void Wakeup()
        {
            Console.WriteLine("既而儿醒,大啼");
        }
    }
    

    这里Update()仅仅相当于做了一次转发,当然,也可以加入自己的逻辑。改变较大的是Dog类,不过也都是前面组合模式,享元模式等中用过的常用手法,如下所示:

    public class Dog
    {
        private readonly IList<IObserver> _observers = new List<IObserver>();
    
        public void AddObserver(IObserver observer)
        {
            _observers.Add(observer);
        }
    
        public void RemoveObserver(IObserver observer)
        {
            _observers.Remove(observer);
        }
    
        public void Bark()
        {
            Console.WriteLine("遥闻深巷中犬吠");
            foreach (var observer in _observers)
            {
                observer.Update();
            }
        }
    }
    

    不难理解,由于Wife,Husband,Son都实现了IObserver接口,因此可以通过IList<IObserver>集合进行存储,同时通过AddObserver(IObserver observer)RemoveObserver(IObserver observer)对具体实例进行添加和删除管理。

    再看看调用的代码:

    static void Main(string[] args)
    {
        Dog dog = new Dog();
        Wife wife = new Wife();
        Husband husband = new Husband();
        Son son = new Son();
        dog.AddObserver(wife);
        dog.AddObserver(husband);
        dog.AddObserver(son);
        dog.Bark();
        Console.WriteLine("----------------------");
        dog.RemoveObserver(son);
        dog.Bark();
    }
    

    其实,这就是需求最简单的观察者模式了,其中Dog是被观察者,也就是被观察的主题,而Wife,Husband,Son都是观察者,下面看看它的类图:

    从这个类图上,我们可能会发现一个问题,既然观察者实现了一个抽象的接口,那么被观察者理所应当也应该实现一个抽象的接口啊,毕竟面向接口编程嘛!是的,但是该实现接口还是继承抽象类呢?我们暂且搁置,先叠加一个需求看看。

    演进二

    翻翻课本可以看到,“遥闻深巷中犬吠,便有妇人惊觉欠伸,其夫呓语。既而儿醒,大啼。”,后面还有三个字“夫亦醒。”(后面还有很多,为防止过于复杂,我们就不考虑了),我们再来看看他们之间的关系:

    结合上下文可以知道,丈夫是被儿子哭声吵醒的,而不是狗叫。依据这些,我们可以分析出以下三点:

    1. 被观察者有两个,一个是狗,一个是儿子;
    2. 丈夫观察了两件事,一个是狗叫,一个是儿子哭;
    3. 儿子既是观察者,又是被观察者。

    感觉一下子复杂了好多,不过好在有了前面的铺垫,实现起来,好像也并不是特别困难,WifeDog没有任何变化,主要需要修改的是HusbandSon,代码如下:

    public class Husband : IObserver
    {
        public void DreamTalk()
        {
            Console.WriteLine("其夫呓语");
        }
    
        public void Update()
        {
            DreamTalk();
        }
    
        public void Wakeup()
        {
            Console.WriteLine("夫亦醒");
        }
    }
    
    public class Son : IObserver
    {
        private readonly IList<IObserver> _observers = new List<IObserver>();
    
        public void AddObserver(IObserver observer)
        {
            _observers.Add(observer);
        }
    
        public void RemoveObserver(IObserver observer)
        {
            _observers.Remove(observer);
        }
    
        public void Update()
        {
            Wakeup();
        }
    
        public void Wakeup()
        {
            Console.WriteLine("既而儿醒,大啼");
            foreach (var observer in _observers)
            {
                observer.Update();
            }
        }
    }
    

    可以看到,Husband多了一个Wakeup()方法,Son同时实现了观察者和被观察者的逻辑。

    当然,调用的地方也有了一些变化,毕竟Son的地位不同了,代码如下:

    static void Main(string[] args)
    {
        Dog dog = new Dog();
        Wife wife = new Wife();
        Husband husband = new Husband();
        Son son = new Son();
        dog.AddObserver(wife);
        dog.AddObserver(husband);
        dog.AddObserver(son);
        son.AddObserver(husband);
        dog.Bark();
    }
    

    看到这里,细心的人会发现这段代码存在着很多问题,至少有以下两点:

    1. DogSon中存在着大量重复的代码;
    2. 运行一下会发现Husband的功能没有实现,因为Husband中没有标识事件的类型或来源,因此也就不知道是该说梦话还是该醒过来。

    演进三-标准观察者模式

    为了解决上述两个问题,我们需要再做一次改进,首先第一个代码重复的问题,很明显提取一个共同的基类就可以解决,而第二个问题必须通过传参来加以区分了,我们可以先定义一个携带事件参数的类,事件参数通常至少包含事件来源以及事件类型(当然也可以包含其它的属性),代码如下:

    public class EventData
    {
        public object Source { get; set; }
    
        public string EventType { get; set; }
    }
    

    改造的观察者接口和提取的被观察者基类如下:

    public interface IObserver
    {
        void Update(EventData eventData);
    }
    
    public class Subject
    {
        private readonly IList<IObserver> _observers = new List<IObserver>();
    
        public void AddObserver(IObserver observer)
        {
            _observers.Add(observer);
        }
    
        public void RemoveObserver(IObserver observer)
        {
            _observers.Remove(observer);
        }
    
        public void Publish(EventData eventData)
        {
            foreach (var observer in _observers)
            {
                observer.Update(eventData);
            }
        }
    }
    

    可以看到,观察者IObserver中加入了事件参数,被观察者Subject既没有使用接口,也没有使用抽象类,原则上,这样是不合适的,但是,这个类中实在是没有抽象方法,也不适合用抽象类,所有只能勉强使用普通类了。

    其它代码如下:

    public class Wife : IObserver
    {
        public void Update(EventData eventData)
        {
            if (eventData.EventType == "DogBark")
            {
                Wakeup();
            }
        }
    
        public void Wakeup()
        {
            Console.WriteLine("便有妇人惊觉欠伸");
        }
    }
    
    public class Husband : IObserver
    {
        public void DreamTalk()
        {
            Console.WriteLine("其夫呓语");
        }
    
        public void Update(EventData eventData)
        {
            if (eventData.EventType == "DogBark")
            {
                DreamTalk();
            }
            else if (eventData.EventType == "SonCry")
            {
                Wakeup();
            }
        }
    
        public void Wakeup()
        {
            Console.WriteLine("夫亦醒");
        }
    }
    
    public class Son : Subject, IObserver
    {
        public void Update(EventData eventData)
        {
            if (eventData.EventType == "DogBark")
            {
                Wakeup();
            }
        }
    
        public void Wakeup()
        {
            Console.WriteLine("既而儿醒,大啼");
            Publish(new EventData { Source = this, EventType = "SonCry" });
        }
    }
    
    public class Dog : Subject
    {
        public void Bark()
        {
            Console.WriteLine("遥闻深巷中犬吠");
    
            Publish(new EventData { Source = this, EventType = "DogBark" });
        }
    }
    

    可以看到,被观察者通过Publish(EventData eventData)方法将事件发出,而观察者通过参数中的事件类型来决定接下来该执行什么动作,下面是它的类图:

    这其实就是GOF定义的观察者模式了。

    定义

    多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

    UML类图

    将上述实例的类图简化一下,就可以得到如下观察者模式的类图了:

    • Subject:抽象主题角色,它是一个抽象类(而实际上我用的是普通类),提供了一个用于保存观察者对象的集合和增加、删除以及通知所有观察者的方法。
    • ConcreteSubject:具体主题角色。
    • IObserver:抽象观察者角色,它是一个接口,提供了一个更新自己的方法,当接到具体主题的更改通知时被调用。
    • Concrete Observer:具体观察者角色,实现抽象观察者中定义的接口,以便在得到主题的更改通知时更新自身的状态。

    优缺点

    优点

    1. 降低了主题与观察者之间的耦合关系;
    2. 主题与观察者之间建立了一套触发机制。

    缺点

    1. 主题与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用;
    2. 当观察者对象很多时,事件通知会花费很多时间,影响程序的效率。

    当然,这里的缺点指的是观察者模式的缺点,上述实例的缺点其实会更多,我们后续再想办法解决。

    通知模式

    其实观察者模式中,事件的通知无外乎两种模式-推模式拉模式,这里简单的解释一下。我们上述的实现使用的都是推模式,也就是由主题主动将事件消息推送给观察者,好处就是实时高效,这也是较为推荐的一种方式。

    但是并非所有场景都适合使用推模式,例如,某主题有非常多的观察者,但是每个观察者都只关注主题的某个或某些状态,这时使用推模式就不太合适了,因为推模式会将主题的所有状态不加区分的推送给所有观察者,对观察者而言,得到的消息就过于臃肿驳杂了。这时就可以采用拉模式了,主题公开所有可以被观察的状态,由观察者主动拉取自己关注的部分。

    而拉模式根据不同情况又可以有两种实现。一种方式是由观察者定时检查,并拉取数据,这种操作简单粗暴,但是,会给主题造成较大的性能负担,同时,也会因为检查频率的不同而带来不同程度的延时。而另一种方式还是由主题主动发出通知,不过通知不带任何参数,仅仅是告诉观察者主题有变化了,然后由观察者去拉取自己关注的部分,这正是拉模式中最常采用的一种手段。

    总结

    好了,GOF定义的观察者模式分析完了,但实际上,观察者模式还远远没有结束,限于篇幅,我们在下一篇中接着分析。不过在这之前,可以提前思考一下下面两个问题:

    1. dog.AddObserver(...)真的合适吗?实际生活中,狗真的有这种能力吗?
    2. 我们知道C#中不支持多继承,如果Dog本身继承自Animal的基类,如果同时作为被观察者,除了用上述演进一的实现,还能如何实现?因为这种场景太常见了。

    想清楚这两个问题,观察者模式才可能真正的展现出它的威力。

    源码链接

  • 相关阅读:
    设置标题自适应宽度,动态调整大小
    终止延迟函数
    iOS 关于音频开发
    阻止iOS设备锁屏
    苹果开发——设置iTunes Connect中的Contracts, Tax, and Banking
    【iOS开发必收藏】详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!【2012-12-11日更新获取"产品付费数量等于0的问题"】
    uibutton 设置title 居左显示
    通过view 获取viewController
    tableview 自动滑动到某一行
    uibutton 设置圆角边框
  • 原文地址:https://www.cnblogs.com/FindTheWay/p/14773128.html
Copyright © 2011-2022 走看看