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的基类,如果同时作为被观察者,除了用上述演进一的实现,还能如何实现?因为这种场景太常见了。

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

    源码链接

  • 相关阅读:
    Ftp、Ftps与Sftp之间的区别
    Previous Workflow Versions in Nintex Workflow
    Span<T>
    .NET Core 2.0及.NET Standard 2.0 Description
    Announcing Windows Template Studio in UWP
    安装.Net Standard 2.0, Impressive
    SQL 给视图赋权限
    Visual Studio for Mac中的ASP.NET Core
    How the Microsoft Bot Framework Changed Where My Friends and I Eat: Part 1
    用于Azure功能的Visual Studio 2017工具
  • 原文地址:https://www.cnblogs.com/FindTheWay/p/14773128.html
Copyright © 2011-2022 走看看