Good cooking takes time. If you are made to wait, it is to serve you better, and to please you.
(美酒的酿造需要年头,美食的烹调需要时间;片刻等待,更多美味,更多享受。)
—— 《人月神话》
写在前面
本文的内容主要来源于前段时间的部门分享,经过整理、修订与完善后,现分享给各位,祝开卷有益。
适配器模式
While in rome , do as rome dose
假设您所维护的应用暴露出来的接口与现有服务商提供的接口相冲突……
您可以通过实现一个适配器来使两者“相匹配”
现在,由于适配器的引入,您的应用又可以正常工作了,而不用去修改现有系统或厂商类的代码了
还记得全聚德的鸭子们吗?它们现在又碰到了麻烦。由于现在鸭肉紧缺,公司准备用火鸡来代替,但却发觉之前的系统似乎失灵了……
恩,这似乎是显而易见的,之前的系统是针对鸭子接口的,当然不能用于火鸡的实例,要搞清楚的事,火鸡从不呱呱叫(quack),只会咯咯叫(gobble);火鸡由于体型的缘故,飞行距离很短;另外,您见过会游泳的火鸡吗?!
鸭子和火鸡的抽象超类和具体实现类如下所示:
public interface Duck { //鸭子的接口……
public void quack();
public void fly();
public void swim();
}
public class MallardDuck implements Duck { //绿头鸭的具体实现……
public void quack() {
System.out.println("Quack");
}
public void fly() {
System.out.println("fly");
}
public void swim() {
System.out.println("swim");
}
}
public interface Turkey { //火鸡的接口……
public void gobble();
public void fly();
}
public class WildTurkey implements Turkey { //野生火鸡的具体实现……
public void gobble() {
System.out.println("Gobble");
}
public void fly() {
System.out.println("Flying a short distance");
}
}
由于之前的系统只能识别Duck接口,因此我们实现了一个适配器:
public class TurkeyAdapter implements Duck { //首先,你必须实现你想要转换成的、客户 所期望看到的类型接口(Duck)
Turkey turkey;
public TurkeyAdapter(Turkey turkey){ //必须采用某种方式,获得需要适配的对象的引用, 比如构造器注入…
this.turkey = turkey;
}
public void quack() { //有些方法需要简单转换,比如用火鸡的咯咯叫(gobble)来代替鸭子的呱呱叫(quack)
turkey.gobble();
}
public void fly() { //有些方法需要复杂抓换,比如,由于火鸡的飞行距离较短,要达到鸭子的飞行效果,火鸡需要加倍的努力哦(调用fly五次)
for (int i=0; i<5; i++)
{
turkey.fly();
}
}
public void swim() { //还有些方法无法适配,这是可以覆盖为空、或者抛出异常、或者干脆像火鸡一样说:“这个我干不了”
System.out.println("No Turkey can SWIM!!!");
}
}
虽然很简单,但这里其实就用到了一个设计模式:
适配器模式将一个类的接口,转换成客户期望的另一个接口。适配器让原来不兼容的类可以合作无间。
—— GOF
当然,设计模式本身就是语言无关的,您正使用支持多重继承(C++)的语言,可能还会看到所谓的“类适配器”,它采用同时继承目标和被适配类、而不是组合被适配类的方法来适配被适配者
下面举一个现实生活中的例子:我们常常碰到遗留代码,它们往往暴露出Enumeration接口,但我们有希望在新的代码中使用Iterator……
这个时候,您可以考虑使用一个适配器,如下所示:
我们要使旧代码中的枚举变得像是新代码中的迭代器,新代码依然使用迭代器,虽然实际上背后隐藏的事枚举器。下面看一下这个适配器的具体实现:
public class Enumeration implements Iterator { //因为我们要将枚举适配成迭代器,因此必须要实现迭代器(被适配者)接口,让它看起来就像是一个迭代器
Enumeration enum;
public EnumerationIterator(Enumeration enum) { //利用组合的方式,保存一个枚举的引用
this.enum = enum;
}
public boolean hasNext(){ //迭代器的hasNext方法其实是委托给枚举的hasMoreElements方法
return enum.hasMoreElements();
}
public object next(){ //迭代器的next方法其实是委托给枚举的nextElement方法
return enmu.nextElement();
}
public void remove(){ //很不幸,枚举不支持remove方法,所以必须放弃,在这里,我们抛出一个UnsupportedOperationException
throw new UnsupportedOperationException();
}
}
适配器模式的小结如下:
- 客户通过目标接口调用适配器的方法对适配器发出请求
- 适配器使用被适配者接口吧请求转换为被适配者的一个或多个调用接口
- 客户接收到调用的结果,但并未察觉到这一切是适配器在起转换作用
- 当客户没有指定所需要的接口时,我们仍然可以应用Adapter模式,并使用现有类的实例来创建一个新的客户子类。这种方法会创建一个对象适配器,将客户调用转发给现有类的实例。但是这种方法也很危险,特别是当我们没有(或者不可以)重写客户可能调用的所有方法时。
观察者模式
观察,观察,再观察 —— 巴甫洛夫
假设你接到《观察者日报》的这样一个需求:
- 总部设在纽约的《观察者日报》急需新增一个电子版报纸系统,要求订阅电子版报纸的用户可以实时的获取最新报纸。
- 系统需要有注册用户和注销用户功能。当有最新的报纸出版时,所有订阅了报纸的用户可以自动获得最新版报纸。
- 系统不需要了解究竟是哪些用户订阅了报纸,它负责的只是发布、发布、再发布!
- 用户可以随时退订报纸,之后也可以重新再订阅,考虑到每天都有数以千计的用户订阅/退订,你不会想让我们每次都修改发报的代码吧?
先来看一个错误的设计方案:
public void publishNewsPaper() {
NewsPaperOffice newPaperOffice = new NewsPaperOffice("观察者日报社");
Paper = newsPaperOffice.getPaper("《观察者日报》");
Google.send(paper);
Yahoo.send(paper);
Sina.send(paper);
}
这样的设计会有什么问题?
- 如果Alibaba也想订阅报纸,怎么办?
- 如果Yahoo想订阅两份报纸(Yahoo与Yahoo中国各一份),怎么办?
- 如果Google想退订《观察者日报》,怎么办?
- 如果Google退订之后又想再订阅《观察者日报》,怎么办?
按照目前的设计,只有一个方法,就是修改publishNewsPaper的代码!
恩,让我们先来深入研究一下“订阅报纸”的业务:
- 报社的业务就是出版报纸
- 向某家报社订阅报纸,只要它们有新报纸出版,就会给你送来
- 当你不想再看报纸的时候,取消订阅,就不会再有新报纸送来
- 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸
其实,出版商 + 订阅者 = 观察者模式……
其实,主题和观察者都以为对方是俊男美女,因为他们都是高度近视,看不清对方的细节。事实上,有一个设计原则可以支持上面这种设计:
耦合-内聚原则: 为了交互对象之间的松耦合设计而努力
一个松耦合的设计具有如下特点 :
- 主题和观察者之间松耦合,意味着它们虽然可以互相交互,但是不太清楚彼此的细节
- 任何时候都可以增加新的观察者,因为主题唯一依赖的是一个Observer接口的列表
- 有新类型的观察者出现时,主题的代码不需要因为兼容性的问题而修改
- 改变主题或是观察者中的其中一方,并不会影响另一方,因为两者是松耦合的
- 可以独立地复用主题或观察者
一个松耦合的观察者日报的设计大概如下:
观察者日报社的核心代码的实现如下:
List<Observer> observers;
Newspaper newspaper;
public NewspaperOffice()
{
observers = new List<Observer>();
}
public void changeNewspaper(Newspaper n)
{
newspaper = n;
notifyObservers();
}
public void registerObserver(Observer o)
{
observers.Add(o);
}
public void removeObserver(Observer o)
{
observers.Remove(o);
}
public void notifyObservers()
{
foreach (Observer var in observers)
{
var.update(newspaper);
}
}
让我们来测试一下这个“松耦合”设计方案的扩展性。《观察者日报》最初是面向中国地区的,有许多中国订阅者(观察者),它们的具体实现如下:
class Chinese extends Observer
{
Newspaper newspaper;
Observer Members;
public void update(Newspaper n)
{
this.newspaper = n;
display();
}
public void display()
{
System.out.println("I am a chinese and I have got the newspaper");
this.newspaper.display();
}
}
《观察者日报》采用新的系统后大获成功,威震天感觉应该让所有Cybertron人人手一份,所以又来了许多Cybertron订阅者。这时问题出现了,日报没有Cybertron语言版的,怎么办呢?为了让所有人都能读懂报纸,必须在Cybertron订阅者中添加一个翻译方法,将中文版的《观察者日报》翻译成Cybertron版日报。
class Cybertron extends Observer
{
Newspaper newspaper;
public Newspaper translate(Newspaper n)
{
Newspaper tmp = n;
//translate the newpaper.
tmp.setContent("@#$$(@*&@&^@**@(!)!@))#(@&@$
@*#*$^*@(");
return tmp;
}
public void update(Newspaper n)
{
this.newspaper = n;
display();
}
public void display()
{
System.out.println("I am a Cybertron and I have got the newspaper");
translate(newspaper).display();
}
}
《观察者日报》最近新推出了一个版面——“身边的特异功能者”,Sylar发现了这个版面后兴奋的三天没合眼,立即订阅了报纸,只要每天订阅这个报纸就可以轻而易举的找到那些超能者,挖开他们的大脑,获得他们的能力。Sylar立即订阅了一份《观察者日报》。现在Sylar也能不断收到最新的报纸了,其他Hero们要小心了……
class Sylar extends Observer
{
Newspaper newspaper;
Observer Members;
public void update(Newspaper n)
{
this.newspaper = n;
display();
}
public void display()
{
System.out.println("I am Sylar and I have got the newspaper");
this.newspaper.display();
}
}
看见了吗?无论是谁想成为观察者,都不会影响既有的代码,不需要重新编译、部署(新添代码除外),这就是松耦合的好处。当然,观察者模式只是松耦合实现的一种方法,现定义如下:
观察者模式定义了对象之间的一对多关系,这样一来,当一个对象改变状态时,它的所有依赖者都会接收到通知,并自动更新。
—— GOF
恩,刚才还保留了一点没说,其实Java具备内置的观察者模式解决方案,请看:
这里要注意的是:
- 你可以使用推(push)或拉(pull)的方式传送数据,但这意味者你必须公开 你的get方法, 至于安全性如何,你自己决定吧J
- 不要依赖于观察者被通知的次序
- Observable是一个抽象类而不是接口,你明白我是什么意思了吧,如果你的应用程序已经继承了一个抽象超类,你就不能使用它了,这也是为什么一开始不介绍它的原因……
- setChanged方法是被保护的(proteced),这意味者你不能采用组合的方式,违反了“多用组合,少用继承”
- 在GUI中,比如Swing,或MVC中,你能看到更多的观察者模式
工厂模式
任何一个民族,如果被剥夺了工业,那是不能和其他民族在文明上并驾齐驱的。 —— 恩格斯
假设在某系统中要用到一个DataSource对象。而且用的很广泛。在盘古开辟天地的时期,要获取一个DataSource对象,是这么干的:
在很多地方写了这样的代码之后,你的团队里面的一个测试MM开始与你合作做这个项目。这位美女拿到代码之后发现,你写在代码中一共999个地方的new DataSourceImpl(“dburl”,“user”,“password”)里面的数据库url,username和password和他的机器上的情况不符合,于是你发现,应该这样写:
public class DataSourceManager{
public DataSource getDataSource(){
return new DataSourceImpl("dburl","user","password");
}
}
经过上面的改造,你的同事拿到代码之后高兴了,发现自己只要把这个DataSourceManager里面的代码改一改,在自己机器上就能跑起来了。于是她请你吃了哈根达斯——当然,不排除这是因为她对你有好感。
又过了两天,团队又加入了2个人。当大家都开始从svn拿最新的代码的时候,你发现每天都会拿到新的DataSourceManager,里面变化的是各个团队成员自己机器上的数据库url,用户名,密码。这意味着要重新编译,你知道程序员最讨厌的是什么吗?于是你发挥聪明才智,把数据库用户名密码放在了一个properties配置文件中。暂且叫做jdbc.properties。然后经过改造,DataSourceManager变成了下面的样子:
public class DataSourceManager {
private String dbUrl;
private String user;
private String password;
static {
//读取jdbc.properties,初始化dburl,user,password
}
public DataSource getDataSource(){
return new DataSourceImpl(dbUrl,user,password);
}
}
恭喜你,经过这样的改造,DataSourceManager封装了更多的创建逻辑,而你在不同的机器上部署应用的时候也不需要总是去编译了。
过了几天,领导对你说,现在用jdbc直接连数据库可能会有性能问题,我们希望使用jndi,使用容器管理的数据源,但是同样要给开发人员保持使用jdbc连接的余地。于是你再一次的修改DataSourceManager,根据一个配置,决定是去jndi中取dataSource直接返回,还是根据用户密码url来创建dataSource返回。
回首往事,你将会心潮澎湃。试想一下当初如果没有用DataSourceManager这么个东西来封装DataSource的创建过程,而是坚持在系统的各个角落创建DataSource的话,现在要改成jndi方式得费多大劲。于是一高兴,你请当初请你吃哈根达斯的那个MM吃了顿饭,顺道表露了一下你对她有某方面的好感。
再后来的某一天,你有幸读到了和banq吵架很火爆的阎宏博士写的《Java与模式》,发现你所写的DataSourceManager就是他书中阐述的工厂模式——因为其职责是封装对象的创建过程。
至此,你终于理解了工厂模式是干什么的,以及它为什么这么重要。
后来的若干岁月,你开始研究开源世界里面大大小小的轮子,某一天发现了Spring这么个好东西,之后又惊奇的发现,这个东西的核心内容,竟然就是一个BeanFactory——也就一个工厂。
于是你融会贯通,高兴之余,拒绝了那个测试MM对你的求爱,告诉他说你现在只对工厂感兴趣,对女人没兴趣
上面那个DataSource的例子所说的工厂,其实只是工厂模式的一种,称为简单工厂模式;其实,总共有三种工厂模式:
- 简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类或接口,通常根据一个条件(参数)来返回不同的类的实例。
- 工厂方法模式定义了一个创建对象的接口,但由子类 决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
- 抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
下面我们以社会分工理论来具体说明它们的区别:
假设我们要分别创建宝马和奔驰,在建国初期,只有一个汽车工厂,你想要宝马,它就造一辆宝马;你想要奥迪,它就给你一辆奔驰;改革开放后,允许私有经济了,因此出现了宝马工厂和奔驰工厂,分别独立负责制造它们自己的汽车,用户想要买什么汽车就可以直接去相应的工厂去买;加入世贸组织后,社会分工进一步扩大了,汽车的不同部件(轮胎、车身)由不同的工厂加工,因此出现了宝马、奔驰两个平行的产品族(宝马轮胎、宝马车身、奔驰轮胎、奔驰车身)及相应的工厂,可以根据用户的需要自动地从工厂采购元器件拼装成一辆汽车。
关于模式,你还应该知道的……
1、三类设计模式(懒得翻译了)
① Creational design patterns are all about class instantiation. This pattern can be further divided into class-creation patterns and object-creational patterns. While class-creation patterns use inheritance effectively in the instantiation process, object-creation patterns use delegation effectively to get the job done.
② Structural design patterns are all about Class and Object composition. Structural class-creation patterns use inheritance to compose interfaces. Structural object-patterns define ways to compose objects to obtain new functionality.
③ Behavioral design patterns are all about Class's objects communication. Behavioral patterns are those patterns that are most specifically concerned with communication between objects.
2、并发设计模式
http://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_%28%E8%AE%A1%E7%AE%97%E6%9C%BA%29
或参考《Java多线程设计模式》
3、 反模式
在软件工程中,一个反面模式(anti-pattern或antipattern)指的是在实践中明显出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。它们已经经过研究并分类,以防止日后重蹈覆辙,并能在研发尚未投产的系统时辨认出来。例如:
- 抽象倒置(Abstraction inversion):不把用户需要的功能直接提供出来,导致他们要用更上层的函数来重复实现
- 魔力按键(Magic pushbutton):直接在接口的代码里编写实现,而不使用抽象
- 贫血的域模型(Anemic Domain Model):仅因为每个对象都要有属性和方法,而在使用域模型的时候没有加入非OOP的业务逻辑
- 上帝对象(God object):在设计的单一部分(某个类)集中了过多的功能
- 对象粪池(Object cesspool):复用那些不满足复用条件的对象
- 单例爱好者(Singletonitis):滥用单例(singleton)模式
- 又TMD来一层(Yet Another Fucking Layer):向程序中添加不必要的层次结构、库或是框架。自从第一本关于编程的模式的书出来之后这就变得很普遍。
- 幽灵(Poltergeists):指这样一些对象,它们唯一的作用就是把信息传给其它对象
- 魔幻数字(Magic numbers):在算法里直接使用数字,而不解释含义
- 自身测试(Meta-testing):过度设计测试过程以至于它本身都需要测试,也被称为“看门人的看门人”
参考文献
http://www.jdon.com/
http://www.cnblogs.com/Terrylee/archive/2006/07/17/334911.html
http://sourcemaking.com/design_patterns
http://www.iteye.com/forums/tag/Design-Pattern
http://www.chinajavaworld.com/forum.jspa?forumID=44
http://www.ibm.com/developerworks/cn/java/design/#N100C1
http://www.blogjava.net/AllanZ/archive/2008/08/23/223890.html
……