通过自己举出的一些设计例子,配合代码,学习观察者模式和装饰者模式。这篇文章包含了两个不相关的内容,分别对应书上两个不同的章节。
观察者模式
假设目前我们有一位教务处老师,这位老师希望同学们帮他制作一个电子公告栏。亲切的教务处老师送给我们一段源码。
1 | public class ManagementSys { |
教务处老师给出了获得课程信息,课程时间,授课老师等等一系列的getter
方法。而且,每次老师获得新信息之后,都会调用一个infoChanged()
方法。我们不需要在意getter
的具体实现,只需要根据信息写电子版程序即可。我们很容易得到思路,首先,我定义一个可以显示内容的公告栏类,比如说开课栏目公告栏、开课时间公告栏等等,再为公告栏类增加update()
方法来更新内容,因此…
1 | public void infoChanged() { |
这应该是大部分人直觉中的方法,至少是我的。但是,这违反了我们曾经学习的设计原则。首先,我们没有封装变化的部分,大家的update()
至少在形式上是一致的,应该被封装起来;其次,我们现在面向实现编程,也就是说,我们把infoChanged()
将会更新哪些公告板写死在代码中。如果我们未来想要添加新的展板,我们不得不也修改这里的代码,并且我们将无法做到在运行时动态的改变是否更新某个告示板。不太好!
于是,我自己提出了一个解决方案,我打算让这些公告栏都继承自主公告栏类,这样它们就拥有相同的update()
方法,当然我也可以用继承加多态的方式直接遍历所有Display
对象,就如同下面这样:
1 | //... |
将对象添加到数组的过程就相当于同意接受更新,也可以动态去除。我们来看看我们提出的土方法和观察者模式之间有何异同。
定义
观察者模式定义了对象之间的一对多依赖,这样一来当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。观察者模式将会设计一个主题(Subject)接口,其方法有添加观察者、去除观察者和通知观察者;以及一个观察者(Observer)接口,拥有更新的方法。具体的主题总是会实现主题接口,而具体的观察者将会实现观察者接口。
观察者模式提供的设计允许主题和观察者之间松耦合。这是什么意思?松耦合意味着两个对象之间可以交互,但是彼此并不明白各自的细节。主题只知道有某个类实现了观察者接口,但对于这个类是什么,将会做什么,主题不知道也不关心。如果我需要在其他地方使用主题或者观察者,那么可以轻易复用。这就带来了我们的第四个设计原则:为了交互对象之间的松耦合设计而努力。
重新设计
根据我们已有的信息,我们来重新设计一下我们的系统,首先当然实现我们的主题-观察者接口。
1 | public interface Subject { |
有了接口之后,我们可以让ManagementSys
实现接口…
1 | public class ManagementSys implements Subject { |
不得不说,我们的土方法可以算是和观察者模式非常接近了,但是我们并没有把观察者和主题抽象成接口,也没有把存储观察者的ArrayList封装起来。对于单个项目来说,我想我们已经做的足够好了。不过,如果要讨论到可复用性,还是书上提出的方法更加完善。任何类只要实现了写好的接口就可以达到效果。这样,我们也可以针对性地修改我们的告示牌,只需要实现Observer
接口,并在update()
方法中选择自己需要的参数进行更新就可以了。
内置的观察者模式
Java API中有内置的观察者模式。java.util
中包含基本的Observable类和Observer接口。与我们自己实现的观察者模式不同的是,它支持使用“推”和“拉”两种方式传递数据,同时在做之前需要调用setChanged()
方法告知程序已经改变。但是,Java提供的“主题”是一个类,意味着你没有办法进行多继承,而且Observable具有Protected方法,这也会阻止你将Observable实例组合到你自己的类中。简直是我们之前所学的设计原则的灾难啊!
除了java.util.Observable
,在其他各种地方也有类似的观察者模式的设计,比如Swing API中的JButton,其超类AbstractButton具有AddListener()
的方法,允许你做到:当按钮被按下时,传递消息到所有的ActionListener中。ActionListener接口则有actionPerformed()
方法,相当于本例子中的update()
。这些会令人想到在 jQuery,MATLAB App Designer 等工具中编写回调函数。许多GUI框架大量使用了这种模式。
思考
上文提到的jQuery令我想起了一些事情。想象一下,notifyObservers()
方法在循环中对所有观察者调用update()
方法时,有一个观察者出现了问题,比如:过长甚至是死循环,抛出异常等等。这样将会打断notifyObservers()
方法的整个过程。真不好!所以如果可以,我们应该用异步的办法解决这类问题。当然,成熟的GUI框架也都是如此做的。
装饰者模式
还记得上次的游戏公司吗?现在有新的工作了:你需要设计游戏里在NPC处售卖的武器。现在游戏里已经有一个原始的武器实现了。目前为止,每个武器都会有名字和返回价格的方法,其他的部分我们暂时不关心。
1 | // Weapon.java |
即所有的武器都继承自一个超类,各自实现自己的cost()
方法。但是现在问题来了,如果只有这些单一的武器多没有意思啊,不会有人喜欢玩我们的游戏的。项目经理告诉你,我们打算做一个:
…怎么办?可不要说我们要实现下面的这个类:
1 | public class RareFlameAspectUndeadSlayerVampireLongSwordPlusThree extends Weapon { |
很明显,原始的武器实现严重违反了我们的设计原则:独立变化之处,少用继承。当然你可能想到了另一种思路,毕竟上面的那把炫酷剑,到头来说也是剑嘛。我们可以让剑HAS-A炫酷属性,不就解决问题了吗?
1 | public abstract class Weapon { |
这是书上给出的方案,但我觉得不够好,或者说完全不好,我甚至想不到如何计算价格,难道要遍历这些方法?书上的例子是否有些为了否定而否定呢?
HAS-A并不是无法解决这个问题。我想到,此时我们也许应该用到我们之前学习的策略模式,将炫酷属性封装起来,成为一个接口,具体的炫酷属性将会实现这个接口,并且再用一个ArrayList存储它们,现在我们就将变化之处独立出来了。如果游戏多了新的炫酷效果,那么我们只需要实现炫酷效果本身即可。
1 | // Weapon.java |
看来其他的设计模式并不是不可行,我们也做到了使用组合不使用继承。但是毕竟我们这一节是讲装饰者模式嘛!装饰者模式究竟比我们以上的代码优越在何处?这需要我们理解我们的第五个设计原则:类应该对扩展开放,对修改关闭。可以用任何想要的行为扩展类,但是尽量不要修改类的代码。可能现在代码已经被写好,如果这个时候去修改Weapon类的属性,构造函数等等,就会出一些问题。装饰者模式严格遵守了开放-关闭原则,让我们看看这是怎么做到的。
定义
装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的方案。装饰者超类本身将会继承现有的超类。也就是说,在本例子中,装饰者超类和Sword
类是同级别的。同时,装饰者的具体实现是继承了装饰者超类的。
等一下?不是不要使用继承要用组合吗?但其实,本处继承的重点是,要求装饰者和被装饰者必须是一样的类型。我们所做的事情是“类型匹配”,而非获得任何行为。当我们把装饰者和组件结合时,才是加入了新的行为。获得行为的方式是组合而来的。
重新设计
我们现在可以重新设计我们的武器属性了,首先,Weapon类不需要做任何变化,我们之前写好的Sword类也不需要变化,来看看装饰者的超类如何实现。
1 | public abstract class WeaponDecorator extends Weapon { |
先别急着疑惑,我们再具体实现一个装饰者,这样就能具体看到它的作用了。
1 | public class Vampire extends WeaponDecorator { |
也就是说,装饰者超类必须实现其父类需要改变的方法,而具体的装饰者将会override这些方法,并进行具体的改动。这样的行为可以让我们得到神奇的效果(我已经在兴奋了):
1 | Weapon sellWeapon = new Vampire(new UndeadSlayer(new FlameAspect(new Rare(new Sword())))); |
真是美丽又可怕!这立刻让我们想到了别的东西。
真实世界的装饰者
装饰者竟在我身边,想想你曾经要做文件流读写的时候写过的代码:
1 | InputStreamReader iptStrm = new InputStreamReader(new FIleInputStream(arg)); |
这样看来,装饰者模式的缺点也非常明显了,因为这样嵌套的小类实在是太多了(比如我的一位朋友对于Java的印象就是:有很多new)。
总结
来看看我们新学到的东西:
- 设计原则四:为了交互对象之间的松耦合设计而努力。
- 设计原则五:类要对扩展开放,对修改关闭。
- 观察者模式:定义对象之间的一对多依赖,这样一来当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
- 装饰者模式:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的方案。