之前有发表博文,简单的讲解一下观察者模式的大概内容(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图,就会发现我们可以用观察者模式来解决这个问题。

转载于:https://www.cnblogs.com/wenjiang/p/3149990.html

一步一步将自己的代码转换为观察者模式相关推荐

  1. [教程]JS从糊涂到明白:一步一步编写计算器2 – 简化代码

     [文章原始发表:This Is WWW : http://www.plrsoft.cn/blog/?p=69  转载请注明出处]  我在上一篇文章"一步一步编写计算器 – 构建和兼容&qu ...

  2. SpringBoot+MyBatisPlus+ElementUI一步一步搭建前后端分离的项目(附代码下载)

    场景 一步一步教你在IEDA中快速搭建SpringBoot项目: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/87688277 ...

  3. 一步一步学Silverlight 2系列(22):在Silverlight中如何用JavaScript调用.NET代码_转载...

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  4. 一步一步学Silverlight 2系列(22):在Silverlight中如何用JavaScript调用.NET代码

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  5. [翻译Joel On Software]Joel测试:12步写出更高质量代码/The Joel Test: 12 Steps to Better Code

    Joel on Software The Joel Test: 12 Steps to Better Code Joel测试:12步写出更高质量代码 byJoel Spolsky Wednesday, ...

  6. Typora快捷输入,三步让你打出带类型代码块(前提:需搜狗输入法)

    三步让你打出带类型代码块 前言:我一向是个爱偷懒的人,自从爱上用Typora写笔记后,觉得这个软件真的是什么都好,就是我写代码的时候每次都要先shift+alt+k,然后再选代码类型 这种代码少的时候 ...

  7. C#WPF 语音开发教程 TTS中英文语音(男女声音)朗读 源代码下载 csdn tts(text to sound) 一步一步 教你制作语音软件 附图和源代码

    C#WPF  语音开发教程  TTS中文语音朗读 一步一步 教你制作语音软件 附图和源代码 使用时,请确认电脑喇叭打开,并且不是静音额. 效果展示 一 项目准备 1.vs2012开发平台 2.微软的语 ...

  8. TensorFlow手写数字识别与一步一步实现卷积神经网络(附代码实战)

    编译 | fendouai 编辑 | 安可 [导读]:本篇文章将说明 TensorFlow 手写数字识别与一步一步实现卷积神经网络.欢迎大家点击上方蓝字关注我们的公众号:深度学习与计算机视觉. 手写数 ...

  9. 一步一步推导S-MSCKF系列(和代码一致)

    S-MSCKF学习系列 一步一步推导S-MSCKF(一)姿态表示方法 一步一步推导S-MSCKF(二)连续时间IMU运动模型 一步一步推导S-MSCKF(三)离散时间系统运动模型 一步一步学习S-MS ...

最新文章

  1. Linux下程序崩溃dump时的 core文件的使用方法
  2. Windows下搭建PHP开发环境
  3. 苹果6怎么截屏_蓝苹果多肉怎么养,掌握这6种养殖方法
  4. 不使用注解和使用注解的web-service-dao结构
  5. 《Netkiller Spring Cloud 手札》Spring boot 2.0 mongoTemplate 操作范例
  6. Mac Nginx 配置 Tomcat 配置 jdk环境变量 Nginx部署服务遇到的坑(2)
  7. 2019.7.17东湖大数据页面三
  8. VB.NET写的简单图片缩放处理组件源代码,支持添加半透明效果小图标(转)
  9. Keil5最新注册机到2032
  10. 电子设计推荐看的好书
  11. Excel-工作周报(月报)【改良版】
  12. Javassist学习总结1
  13. Linux系统安装迷你世界,迷你世界国际服下载安装
  14. animate.css的使用
  15. C语言程序设计-跳马问题
  16. Halcon——颜色检测
  17. Excel 甘特图 一行公式 极简版
  18. Jupyter 福音: 官方可视化 Debug 工具!
  19. Java中的时间、时区和夏令时
  20. jupyter notebook文件保存路径

热门文章

  1. OpenCV学习--saturate_cast防止数据溢出
  2. 问题解决:vue dev模式没问题,dist之后页面not found
  3. 3.2.5 端到端的学习
  4. Ubuntu 18.04 安装 redis入门使用
  5. react gps坐标转换_手持GPS的三参数计算方法
  6. iPhone音频播放后台控制
  7. slimphp中间件调用流程的理解
  8. 【DP】[ZJOI2008][HYSBZ/BZOJ1037]生日聚会Party
  9. 用场景来规划测试工作
  10. [排错]运行cocos2d自带的cocos2d-test-ios工程出现错误:找不到libcocos2d.a