关于设计模式:18-设计模式观察者模式上

3次阅读

共计 6554 个字符,预计需要花费 17 分钟才能阅读完成。

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

示例

观察者模式在不同需要下,实现形式也不尽相同,咱们还是举一个例子,而后通过逐渐的改良来粗浅感受一下它是如何工作的。

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

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

初版实现

一声狗叫引发了一系列的事件,需要很清晰,也很简略。于是,咱们能够很容易的失去如下实现:

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

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

源码链接

正文完
 0