`

一步一步将自己的代码转换为观察者模式 - 文酱

阅读更多
原帖地址:http://www.cnblogs.com/wenjiang/p/3149990.html

     之前有发表博文,简单的讲解一下观察者模式的大概内容(http://www.cnblogs.com/wenjiang/archive/2013/05/07/3065040.html),主要是利用java对观察者模式的内置支持来实现观察者模式,现在想要换个思路,自定义观察者模式。

     这次使用Eclipse的单元测试框架,前面那个例子就不适合了,所以特意挑一个有关时钟报时的例子,方便测试。

     敏捷开发的原则就是测试先于代码,这里就采用这个原则,先从测试代码开始:

public class ClockTest extends TestCase {
private TimeScreen screen;
private TimeSource source;

 public ClockTest(String name) {
super(name);
}

public void testTimeChange() {
TimeSource source = new TimeSource();
TimeScreen screen = new TimeScreen();
Clock clock = new Clock(source, screen);
source.setTime(3, 4, 5);
assertEquals(3, screen.getHours());
assertEquals(4, screen,getMinutes());
assertEquals(5,screen.getSeconds());
}
}

      该测试主要测试:当时钟时间改变时,屏幕能否跟着改变。
      因为时钟可能是电子时钟或者其他时钟,所以,我们定义一个时间来源的抽象:Source:

public interface Source{
public void setSource(Clock clock);
}

      同样屏幕也要有一个抽象:

public interface Screen{
public void setTime(itn hours, int minutes, int seconds);
}

      接着是Clock的代码:

public class Clock{
priate Screen screen;

public Clock(Source source, Screen screen){
source.setClock(
this);
this.screen = screen;
}

public void update(int hours, int minutes, int seconds){
screen.setTime(hours, minutes, seconds);
}
}

      Clock通知屏幕更新时间,所以它必须拥有Screen的运用,又因为它是从Source获取时间,所以它必须将自己传给Source,也就是说,它是Screen和Source之间的邮差。

      我们来实现具体的Screen和Source:

public class TimeSource implements Source{
private Clock clock;

public void setTime(int hours, int minutes, int seconds){
clock.update(hours, minutes, seconds);
}

public void setClock(Clock clock){
this.clock = clock;
}
}

 

public class TimeScreen implements Screen{
private int hours;
private int minutes;
private int seconds;

public int getHours(){
return this.hours;
}

public int getMinutes(){
return this.minutes;
}

public int getSeconds(){
return this.seconds;
}

public void setTime(int hours, int minutes, int seconds){
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
}
}

       UML图如:

      

      上面的代码能够通过测试,但并不是一个好方案,最主要的问题就是TimeSource持有Clock的引用。Clock确实是Source和Screen的邮差,但我们并不依赖于具体的邮差帮我们传送数据,邮差本身也可以是一个抽象:

public interface TimeObserver{
public void update(int hours, int minutes, int seconds);
}

      为了贴近今天的主题,特意将这个抽象命名为TimeObserver,因为它就是一个观察者,观察数据什么时候改变,然后通知相应的屏幕。

     然后就是实现这个抽象:

public class Clock implements TimeObserver{
private Screen screen;

public Clock(Source source, Screen screen){
source.setObserver(
this);
this.screen = screen;
}

public void update(int hours, int minutes, int seconds){
screen.setTime(hours, minutes, seconds);
}
}

      接着就是将原本引用Clock的地方都改为TimeObserver就行,像是下面这样:

public interface Source{
public void setObserver(TimeObserver observer);
}

      加入这样的抽象的好处非常明显,就是消除我们之前的依赖,也就是通过提供间接层的方式消除依赖的做法,这也是接口的作用。

     

      通过查看代码,我们发现TimeObserver的update()其实就是调用Screen的setTime(),这是因为它必须通知Screen修改显示的时间,那么我们是否可以直接将Screen传递给Source的方法,而不是像之前那样需要TimeObserver?显然我们的测试代码需要进一步修改,修改的地方只有一处:

source.setObserver(screen);

       然后是我们的Source:

public class TimeScreen implements TimeObserver{
private int hours;
private int minutes;
private int seconds;

public int getHours(){
return this.hours;
}

public int getMinutes(){
return this.minutes;
}

public int getSeconds(){
return this.seconds;
}

public void update(int hours, int minutes, int seconds){
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
}
}

      

      为什么我们一开始会有一个Clock呢?因为我们需要一个邮差,但为什么不让我们的Source直接通知Screen呢?我们经常会犯这样的错误,尤其是在使用接口的时候,认为凡事有个间接层都是好的,都是动态的,其实不然,接口的确是个好东西,但是,该让谁实现这个接口就是一个问题。我们很容易像是上面一样,引入了一个具体类型Clock,而且代码的运行也没有错,它依然能够工作得很好,我们还可以和别人炫耀:看看我的邮差工作得多努力!
     如何设计好接口,是面向对象编程中一个很重要的努力方向。

     我的个人经验,当然,这经验是微不足道的(面向对象编程经验只有一年OTZ),如果两个类型之间需要进行通信,应该是让它们的抽象之间进行通信,也就是它们各自实现的接口,这样就能消除具体类型的耦合,而且这种通信的方式是以传参的方式进行。这样,我们的对象层次上既保证一定的解耦,又能保证逻辑上的耦合关系的完整。

     之前的测试实在是太简单了,只是单独测试一个Screen,现实生活中的情况是非常复杂的,我们很可能需要多个屏幕,而且它们是不同材质,不同地方,这需要增加一个测试:

public void testMultipleScreens(){
TimeSource source
= new TimeSource();
TimeScreen screen
= new TimeScreen();
source.registerObserver(screen);

TimeScreen screen2
= new TimeScreen();
source.registerObserver(screen2);

source.setTime(
3, 4, 5);
assertScreenEquals(screen,
3, 4, 5);
assertScreenEquals(screen2,
3, 4, 5);
}

private void assertScreenEquals(TimeSource source, int hours, int minutes, int seconds){
assertEquals(hours, screen.getHours());
assertEquals(minutes, screen.getMinutes());
assertEquals(seconds, screen.getSeconds());
}

      我们在Source里增加了一个方法:registerObserver(),正如其名,就是将相关的Screen注册进Source需要通知的名单中,新的Source如:

public interface Source{
public void registerObserver(TimeObserver observer);
}

      接着我们的TimeSource如:

public class TimeSource implements Source{
private List<TimeObserver> observers = new ArrayList<TimeObserver>();

public void registerObserver(TimeObserver observer){
list.add(observer);
}

public void setTime(int hours, int minutes, int seconds){
for(TimeObserver observer : observers){
observer.update(hours, minutes, seconds);
}
}
}

      我们用ArrayList来作为存储需要通知的Screen的名单,然后在时间更新的时候,逐个通知它们更新自己的时间。
      但问题也来了,任何一个Source的实现类都必须实现注册和更新的代码,哪怕它们都是一样的。这样代码的重复性太高了,我们得想办法解决这个问题。

      将Source从接口变为类型就可以解决了:

public class Source{
private List<TimeObserver> observers = new ArrayList<TimeObserver>();

protected void notify(int hours, int minutes, int seconds){
for(TimeObserver observer : observers){
observer.update(hours, minutes, seconds);
}
}

public void registerObserver(TimeObserver observer){
observers.add(observer);
}
}

       然后我们的TimeSource只需要这样:

public class TimeSource extends Source{
public void setTime(int hours, int minutes, int seconds){
notify(hours, minutes, seconds);
}
}

      我们的派生类型的确是不需要重新写注册和更新的代码,只要调用基类的相关方法就行。

     

      从上面我们可以知道,接口可以为我们提供间接层,减少具体类型的依赖,使得我们的代码更具动态,但是,它会使我们面临代码重复性较高的危险,更可怕的是,它会让我们陷入这样的怪论:"只要能呱呱叫,就是鸭子"。这是面向对象编程的一个经典现象,因为所有实现类都要实现接口规定的方法,而且我们不能阻止非目标类型对该接口的实现。

     使用继承可以解决上面的怪论:"只有鸭子才能呱呱叫"。这是继承的本质,它规定的是一种类型,而不是一组行为协定。当然,接口也有自己的对策:将行为协议划分得更细,最好就是一组相关的行为放到一个接口里。前面之所以会出现这样的怪论,是因为程序员可能会这样设计接口:

public interface Duck{
public void fly();
public void shout();
}

     这样的接口就会让人产生误解,正确的接口应该是这样的:

public interface FlyAble{
public void fly();
}

public interface ShoutAble{
public void shout();
}

     接口的命名应该是动词,而不是名词,因为它规定的是一组行为协议。

     但继承也存在自己的问题:"不是所有的鸭子都会呱呱叫",有些鸭子可能不会叫,但是它们是有方法可以呱呱叫的,这就会出现错误。

     所以,使用继承解决问题的时候,我们必须明确一点:派生类能从基类中继承的职责到底是什么?

     在这里,很明确的就是,我们的Source根本就没有必要理会注册和更新的行为,它本来应该只知道时间而已。于是,我们需要将这部分的职责从Source中移除。

     使用委托是一个不错的选择:

public class TimeNotify{
private List<TimeObserver> observers = new ArrayList<TimeObserver>();

public void registerObserver(TimeObserver observer){
list.add(observer);
}

public void setTime(int hours, int minutes, int seconds){
for(TimeObserver observer : observers){
observer.update(hours, minutes, seconds);
}
}
}

public class TimeSource implements Source{
private TimeNotify notify = new TimeNotify();

public void registerObserver(TimeObserver observer){
notify.registerObserver(observer);
}

public void setTime(int hours, int minutes, int seconds){
notify.notify(hours, minutes, seconds);
}
}
public class TimeNotify{
private List<TimeObserver> observers = new ArrayList<TimeObserver>();

public void registerObserver(TimeObserver observer){
list.add(observer);
}

public void setTime(int hours, int minutes, int seconds){
for(TimeObserver observer : observers){
observer.update(hours, minutes, seconds);
}
}
}

public class TimeSource implements Source{
private TimeNotify notify = new TimeNotify();

public void registerObserver(TimeObserver observer){
notify.registerObserver(observer);
}

public void setTime(int hours, int minutes, int seconds){
notify.notify(hours, minutes, seconds);
}
}
public class TimeNotify{
private List<TimeObserver> observers = new ArrayList<TimeObserver>();

public void registerObserver(TimeObserver observer){
list.add(observer);
}

public void setTime(int hours, int minutes, int seconds){
for(TimeObserver observer : observers){
observer.update(hours, minutes, seconds);
}
}
}

public class TimeSource implements Source{
private TimeNotify notify = new TimeNotify();

public void registerObserver(TimeObserver observer){
notify.registerObserver(observer);
}

public void setTime(int hours, int minutes, int seconds){
notify.notify(hours, minutes, seconds);
}
}

       使用委托是增加了一个间接层,专门用于负责注册和更新的具体实现,而我们的Source只要调用它的相应方法就行。

      
       哦,间接层怎么又来了!明明开头我们消除了一个邮差,现在又来了个新的邮差!!此邮差非彼邮差。之前的邮差是因为我们的Source的具体类型要持有一个邮差的引用才能通知Screen的具体类型,但是事实就是Source的具体类型应该可以直接通知Screen的具体类型,这是职责的分离。但这里我们是职责过分集中在一个类型中,所以需要通过间接层将职责分离出去。

      我们知道,这样的解释实在是太模糊了!同样是邮差,为什么一个邮差要被赶走,另一个邮差却要被雇佣,而且评价甚高!!这不公平!!!仔细想想它们的工作就知道了,之前的邮差它负责的工作是更新数据,而且还是命令Screen更新!!这就是冗余,所以它才会被赶走,但是现在这个邮差却负责了新的工作:通知Screen更新数据和注册新的Screen,Source的工作仅仅是命令它做事而已。这样辛苦工作的邮差怎么可能被炒呢!!

      现在的我们已经将整个观察者实现出来了,只要将Source改为TimeSubject就行,因为在观察者模式中,被观察的就是Subject,而java中习惯的命名方式是TimeObservable。我们这里采用的是"推模型",也就是通过把数据传给notify和update方法从而把数据从Subject推给观察者Observer,而另一种方式"拉模型"是Observer在收到更新消息后,查询Subject得到。该使用哪种方式,就在于Observer是否知道是哪个Subject发生变化(Subject可以是多个),如果确定的话,可以使用"拉模型",否则使用"推模型"比较方便。

      下面就是观察者模式的大概UML图:

     

      观察者模式是一个非常好用的设计模式,它应用的范围非常广泛,解决了很多设计问题,而且存在各种变形,但万变不离其宗,只要我们谨记模式的意图,就能在我们毫无头绪的时候指点迷津,尤其是在一开始设计类的时候,如果画一下UML图,就会发现我们可以用观察者模式来解决这个问题。

     


本文链接:http://www.cnblogs.com/wenjiang/p/3149990.html,转载请注明。

分享到:
评论

相关推荐

    RedUTF8将GBK代码快速批量转换为UTF-8的工具

    Red UTF-8 将GBK代码快速批量转换为UTF-8的工具使用本软件可一次性将整站默认代码(GBK及所有默认代码)转换为UTF-8 目前有很多网友需要UTF-8的程序,很多网友想把GBK代码或默认的任何代码想转为UTF-8大多都是手工...

    VB代码转换为C#代码-转换工具-转换器

    标题中的“VB代码转换为C#代码-转换工具-转换器”表明了这是一个关于编程语言转换的工具,主要功能是将Visual Basic(VB)代码,包括VBA和VB.NET,转化为C#语言。C#是一种现代化、面向对象的编程语言,广泛应用于...

    C#经典设计模式及代码示例

    适配器模式可以将不兼容的接口转换为可用的接口,而装饰器模式允许动态地给对象添加新的行为或责任。 3. 行为型模式:这类模式关注对象之间的交互和责任分配,包括策略模式(Strategy)、观察者模式(Observer)、...

    转换FBX-c3b-c3t

    "转换FBX-c3b-c3t"这个主题涉及到的是将FBX格式的3D模型转换为c3b和c3t两种特定的格式。让我们深入探讨这些格式以及转换过程中的技术细节。 FBX(Filmbox)是Autodesk公司推出的一种3D模型交换格式,广泛应用于3D...

    Javascript设计模式之观察者模式(推荐)

    为了适应多个村子的需求,可以将`observer`对象转换为一个构造函数,这样可以创建多个独立的观察者实例,每个实例都有自己的村民列表。在改进后的代码中,`Observer`函数是一个构造函数,通过`new`关键字可以创建新...

    vb6代码转换为c#步骤

    本篇将详细介绍如何将VB6(Visual Basic 6)代码转换为C#,这是一个在20181128实测有效的方法,以下将分为几个步骤进行讲解。 1. **理解VB6与C#的基本差异** VB6是基于事件驱动的编程语言,语法简洁,而C#是面向...

    易语言按键精灵代码转换器1.0.5

    转换器通过识别这些不同的语法特征,将易语言代码转化为按键精灵可以理解的形式,反之亦然,实现了代码的双向转换。 "易语言按键精灵代码转换器1.0.5.exe"是这个工具的可执行文件,用户只需运行此程序,按照界面...

    易语言按键精灵代码转换器

    易语言按键精灵代码转换器1.0.5 易语言和按键精灵的代码互相转换器,我做的,很好用,请大家多多下载 通过卡巴、小红伞、NOD、360、金山、瑞星等权威杀毒软件检测。安全无插件。 软件只有712 KB 超好用

    代码格式转换工具 转换代码风格

    - 个人习惯调整:对于个人而言,可以将代码格式化成自己习惯的风格。 5. **最佳实践** - 在团队中,应建立统一的代码风格指南,并要求所有成员使用代码格式转换工具来遵循这些规范。 - 定期运行格式化工具,以...

    VTK User's Guide(中文完整版)

    3.3 在两种语言间转换 第二部分 通过例子学习VTK 第4章 基础 4.1 创建1个简单的模型-------------------------------------------------------------------------24 程序化源对象----------------------------...

    易语言源码火星文转换源码

    总的来说,这个压缩包提供的易语言源码为学习者提供了一个实践汉字与火星文转换的实例,有助于提升在易语言环境下的编程技能,同时加深对字符编码和字符串处理的理解。通过阅读和研究源码,可以学习到如何用易语言...

    中文短信编码转换工具

    对于需要发送的中文短信,工具则会将其转换为PDU模式的16进制编码,以便于通过GSM网络发送。 在实际应用中,这样的工具极大地简化了开发者和用户处理中文短信的工作。通过理解这些编码原理和技术,可以更好地理解和...

    UTF-8转ANSI文本文件转换器

    "UTF-8转ANSI文本文件转换器"就是这样一个工具,它能够帮助用户批量将UTF-8编码的文本文件转换为ANSI编码的文本文件。在此,我们将深入探讨UTF-8和ANSI编码的原理以及转换过程中涉及的关键知识点。 **一、UTF-8编码...

    Tesseract-OCR中文训练库

    Tesseract OCR(Optical Character Recognition)是由Google维护的一款开源OCR引擎,它能够识别图像中的文本并将其转换为可编辑的格式。在处理中文文本时,Tesseract需要特定的训练数据来提高识别准确率,这就是...

    ATP-EMTP中文教程

    通过ATPDraw,用户可以轻松地构建复杂的电力系统模型,并将其转换为ATP能够识别的电路文件(.cir文件),从而实现对电力系统的仿真分析。 ##### 1.2 ATP简介 ATP是ATP-EMTP的核心仿真引擎,负责执行由ATPDraw或其他...

    tif 转换成 jpg等格式-C#原代码

    在IT行业中,图像处理是一项非常重要的任务,尤其是在软件开发中。`TIF`(Tagged Image File Format)是一种常见的图像文件格式,常用于保存高分辨率的...学习并理解这些代码将有助于你深入掌握C#中的图像处理技术。

    linux C 汉字串与utf-8串相互转化代码

    linux C/c++ 源代码,将中文字串与UTF-8格式字串相互转化,我在项目中使用的代码,完全可用

    模型格式转换工具(osgb-obj-ive)

    "模型格式转换工具(osgb-obj-ive)"就是一款专门用于处理这种问题的实用工具,它能帮助用户在osgb、obj、ive等格式之间进行转换。以下是对这些格式以及转换工具的详细介绍: 1. osgb格式: OpenSceneGraph Binary ...

    颜色转换器 - 反射光谱到 CIE1964 空间:使用 10° 补充标准观察者在六个 CIE 光源下计算颜色空间 CIE 1964 内坐标的代码-matlab开发

    该代码允许将反射率转换为颜色空间 CIE 1964(10° 补充标准观察者)内的坐标,在 5 nm 测量采样下,六个 CIE 光源:A、C 和 D(日光)系列的四个光源:D50、D55 、D65、D75。 该功能自动对 380-780 nm 波长范围执行...

    如何使用Java代码将GBK编码格式的工程转换为UTF-8编码格式的工程.zip

    在处理包含中文字符的Java工程时,有时需要将GBK编码的工程转换为UTF-8编码,以确保在不同系统或工具中的正常显示和处理。本教程将详细讲解如何使用Java代码来完成这个转换过程。 首先,我们需要了解GBK和UTF-8编码...

Global site tag (gtag.js) - Google Analytics