2007年7月20日
2007年7月10日
常听说这么一句话(大意是这样):不必可以去套用设计模式,如果按照面向对象的基本原则编程,自然是优雅的设计,即使没有刻意使用模式,设计也会近乎于模式。开始感觉有一点玄,但在看了《C#设计模式纵横谈》视频后,觉得有所收获。下面,就参考视频的内容,尝试着写这么一个过程:根据面向对象的一般原则对设计进行重构,逐渐演化出观察者模式。
涉及的面向对象设计原则:单一职责原则、封装变化、面向接口编程、依赖倒置原则、开闭原则。
1.发布订阅模型:
假如有需求如下:
银行需要把帐户的如汇款、转账或取款等操作通知用户,途径包括手机短信、 email等。如图所式。
自然地,我们可以这样做:
public class ATM
{
BankAccount bankAccount;
public void process()
{
//bankAccount...
this.sendEmail(userEmail);
this.sendPhone(phoneNumber);
}
private void sendEmail(String userEmail)
{
//
}
private void sendMobile(String phoneNumber)
{
//
}
}
ATM机的 process()方法在处理完业务逻辑后,由email和phone通知用户。
2.初步重构
好像有bad smells,恩,根据单一职责原则。新增Email类和Phone类,并把相关业务逻辑改到BankAccount类完成。于是我们的代码可以这样:
public class ATM
{
BankAccount bankAccount;
public void process()
{
//
bankAccount.withDraw();
}
}
public class BankAccount
{
Email email;
Mobile mobile;
public void withDraw()
{
//
email.sendEmail(userEmail);
mobile.sendMobile(phoneNumber);
}
}
public class Email
{
public void sendEmail(String userEmail)
{
}
}
public class Mobile
{
public void sendMobile(String phoneNumber)
{
}
}
下面是代码的UML图:
3.拥抱变化
这个解决方案有问题吗?可能没有问题。它实现了我们的需求:在帐户有操作变动的时候,通知Email和Mobile去发送信息给用户。但这样设计就足够了吗?可能足够了,可能还不够。
考虑如下两种情况:
1.在很长一段时间里,订阅方式很稳定,比如系统只通过邮件和手机短信进行信息订阅,那么这个实现没有太大问题;
2.在近一两年或更短的时间,更多的订阅方式将会源源不断地被加进来:比如可以登录官方网站等等,那这个实现就有问题:再看一下我们的UML图,类BankAccount依赖于Email和Mobile类!就是说,如果需要添加新的订阅方式ATM类的process()方法势必要重新设计!
于是我们的BankAccount类不得不变成:
public class BankAccount
{
Email email;
Mobile mobile;
Web web;
public void withDraw()
{
//
email.sendEmail(userEmail);
mobile.sendMobile(phoneNumber);
web.sendWeb(webSite);
}
}
如果还有另一种方式,那么process()方法就又会需要加入:otherSubscribe.send...();等方法,另外如果订阅类的接口(这里指sendEmail等方法)发生变化,BankAccount的withDraw()方法也必须有相应的变化!这当然是种灾难。我们必须改变这种情况。
先解决遗留问题:第一种情况:订阅方式相对稳定的情况下呢?不改动会产生灾难吗?
个人认为:不会。比如某个系统信息只通过手机短信订阅,那就没有必要太在意这个问题。考虑周全一点不好吗,如果将来有类似需求呢?小心过度设计!为了将来可能出现需求而进行的预先设计并不太好。有需求,才有设计。
现在来看解决之道:
运用面向对象的思想,抽象出问题所在。BankAccount类依赖于 Email类和Mobile类,而Email和Mobile是具体的类,ATM依赖于具体的类了,而且还不止一个!回忆一下依赖倒置原则:具体应该依赖于抽象,底层模式应该依赖于高层模式。那怎么实现依赖倒置原则呢?面向对象编程中有一条总的原则:封装变化。如何实现封装变化?需要我们这样:面向接口编程。
回顾一下:我们在设计中实现类依赖了具体的类,违反了依赖倒置原则。为了遵循依赖倒置原则,我们采用面向接口编程的方法,从而实现了面向对象的一条总的原则:封装变化。
看代码:
public interface AccountObserver
{
public void upDate(UserAccount userAccount);
}
public class Email implements AccountObserver
{
public void upDate(UserAccount userAccount)
{
}
}
public class Mobile
{
public void upDate(UserAccount userAccount)
{
}
}
public class BankAccount
{
List <AccountObserver> observer = new ArrayList<AccountObserver>;
public void withDraw()
{
//
for (AccountObserver ao : observer)
{
ao.upDate(userAccount)
}
}
public void addOberver(AccountObserver accountObserver)
{
observer.add(accountObserver);
}
}
UML图:
现在,BankAccount依赖于interface AccountObserver。Email和Mobile实现AccountObserver接口。通过遵循面向接口编程遵循了依赖倒置原则。
4.开闭原则
终于修改好了,我们解决了订阅者变化的问题。但如果发布者也倾向于变化呢?这就牵涉到面向对象里的另一个原则:开闭原则。即:对扩展开放,对修改关闭。具体怎么做呢?通过抽象类,从抽象类继承具体类。
看最终的代码(只写几个关键的方法,全貌可看最后的UML图):
订阅:
public interface AccountObserver
{
public void upDate(UserAccount userAccount);
}
public class Email implements AccountObserver
{
public void upDate(UserAccount userAccount)
{
}
}
public class Mobile implements AccountObserver
{
public void upDate(UserAccount userAccount)
{
}
}
发布:
public abstract class Subject
{
List <AccountObserver> observer = new ArrayList<AccountObserver>;
protected void withDraw()
{
//
notify();
}
protected void notify(UserAccount userAccount)
{
for (AccountObserver ao : observer)
{
ao.upDate(userAccount)
}
}
protected void addOberver(AccountObserver accountObserver)
{
observer.add(accountObserver);
}
protected void deleteOberver(AccountObserver accountObserver)
{
observer.remove(accountObserver);
}
}
public class BankAccount extends Subject
{
public void withDraw()
{
//
for (AccountObserver ao : observer)
{
ao.upDate(userAccount)
}
}
}
看UML图:
5.观察者模式概况
这就是观察者模式了,对比一下官方的UML图,是不是一目了然了呢?
稍作说明(这里的依赖都是指广义的依赖):
1.被观察者ConcreteSubject继承自Subject抽象类;
2.Subject抽象类依赖于观察者Observer抽象接口;
3.观察者ConcreteObserver实现Observer 接口;
4.观察者ConcreteObserver间接依赖于ConcreteSubject类。
如果要增加具体的观察者,只要再实现Obsever接口即可,而被观察方不需要做任何修改。而如果需要修改被观察者,只要从Subject抽象类继承即可。