开始了解观察者模式之前,我们先了解一下什么是设计模式、为什么我们要使用设计模式
- 什么是设计模式?
一种针对特定问题的解决方案,这种解决方案被抽象化、模板化后就是设计模式。 - 为什么要使用设计模式?
当我们使用第三方的库、框架的是很好,利用他们的API或者组件,放到合适的地方编译成我们自己的程序。但是库与框架并无法帮助我们将应用组织成易了解、易维护、具有弹性的架构,这个时候就需要设计模式。设计是一门艺术,通常有许多可以取舍的地方,如果我们利用这些经过深思熟虑,且经过时间考研的设计模式就可以稍微领先别人了。
观察者模式
该模式定义了对象之间的一对多依赖,当一个对象改变了状态,它的所有依赖者都会收到通知并自动更新。
观察者模式中,通常有一个主题和N个观察这,主题和观察者之前定义了一对多的关系。观察者依赖主题,只要主题的状态一旦发生了变化,观察者就会收到通知。观察者根据对应的通知做出响应的新值或更新。
在一些安全性要求比较高的系统中。通常我们修改了账户的密码,就会收到短信、邮件、公众号、APP推送的通知,“亲爱的用户 XXX:您好! 您于2021-03-08 11:29:20修改了您的密码,请谨记您的新密码。如非本人操作,则密码可能已经泄露,建议修改密码”,相信您一定收到过这种短信或者邮件。
我们此时用以下的类来模拟对用的通知发送类,分别对应了短信、邮件、公众号、APP推送,控制台打印了对应的文字,就表示通知发送出去了。实际情况中代码并没有这么简单。
package toolkit.net.cn;
/**
* 短信通知
* @author zhaox
*/
public class SMSDisplay {
public void send(String username, String password) {
System.out.println("短信-尊敬的用户" + username + ", 您的密码已经修改为" + password);
}
}
package toolkit.net.cn;
/**
* 邮箱通知
* @author zhaox
*/
public class EMailDisplay {
public void send(String username, String password) {
System.out.println("邮件-尊敬的用户" + username + ", 您的密码已经修改为" + password);
}
}
package toolkit.net.cn;
/**
* 微信公众号通知
* @author zhaox
*/
public class MPDisaplay {
public void send(String username, String password) {
System.out.println("公众号-尊敬的用户" + username + ", 您的密码已经修改为" + password);
}
}
package toolkit.net.cn;
/**
* APP端通知用户
* @author zhaox
*/
public class APPDisplay {
public void send(String username, String password) {
System.out.println("APP-尊敬的用户" + username + ", 您的密码已经修改为" + password);
}
}
新建一个AccountData类,来模拟修改账户密码后分别使用短信、邮件、公众号、APP通知用户的情况。通常没有使用任何模式的情况下,我们账户类的代码基本如下:
package toolkit.net.cn;
/**
* @author zhaox
*/
public class AccountData {
private SMSDisplay smsDisplay;
private EMailDisplay eMailDisplay;
private MPDisaplay mpDisaplay;
private APPDisplay appDisplay;
public AccountData(SMSDisplay smsDisplay, EMailDisplay eMailDisplay, MPDisaplay mpDisaplay,
APPDisplay appDisplay, String username, String password) {
this.smsDisplay = smsDisplay;
this.eMailDisplay = eMailDisplay;
this.mpDisaplay = mpDisaplay;
this.appDisplay = appDisplay;
this.username = username;
this.password = password;
}
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
/**
* 密码修改后,会调用该方法
*/
private void passwordChanged() {
smsDisplay.send(this.getUsername(), this.getPassword());
eMailDisplay.send(this.getUsername(), this.getPassword());
mpDisaplay.send(this.getUsername(), this.getPassword());
appDisplay.send(this.getUsername(), this.getPassword());
}
/**
* 模拟修改密码
*/
public void changePassword(String newPassword) {
this.password = newPassword;
this.passwordChanged();
}
}
初始化对应的推送服务,修改密码后,调用对应的推送服务来通知用户。我们通过main方法做一个模拟测试:
package toolkit.net.cn;
/**
* @author zhaox
*/
public class Main {
public static void main(String[] args) {
// 短信
SMSDisplay smsDisplay = new SMSDisplay();
// email
EMailDisplay eMailDisplay = new EMailDisplay();
// mp
MPDisaplay mpDisaplay = new MPDisaplay();
// app
APPDisplay appDisplay = new APPDisplay();
AccountData accountData = new AccountData(smsDisplay, eMailDisplay, mpDisaplay, appDisplay,
"guest", "mm123465");
accountData.changePassword("mm000000");
}
}
执行main方法,打印的结果如下:
这些模拟代码基本可以模拟没有使用任何设计模式情况下的代码。
这基本上算是一个很糟糕的示范:
- 通知发送都是针对集体的实现变成,会导致我们以后在增加或删除通知方式时必须修改AccountData类。
- 短信、邮件、公众号、APP这四个类的发送方法基本是一样的,包括参数。
接下来我们使用观察者模式,对上面代码做一个调整,观察者模式定义了对象之间的一对多依赖,当一个对象改变了状态,它的所有依赖者都会收到通知并自动更新。很显然这里AccountData就是观察者模式中的主题对象,对应的短信、邮件、公众号、APP通知类就是依赖者对象。实现观察者模式的方法有很多种,这里就使用包含Subject与Observer接口类的设计的做法,另外Java API内置了观察者模式,java.util包内的Observer接口与Observable类。
- 观察者接口,所有观察者必须实现该接口,这个接口只有update()一个方法,当主题发生改变的时候,该方法被调用。
package toolkit.net.cn; /** * 观察者接口 * @author zhaox */ public interface Observer { /** * 主题发送状态改变,该方法就会被调用 * @param username * @param password */ public void update(String username, String password); }
- 主题接口,对象使用此接口注册为观察者或把自己从观察者中删除。
package toolkit.net.cn; /** * 主题接口 * @author zhaox */ public interface Subject { /** * 注册观察者 * @param observer */ public void registerObserver(Observer observer); /** * 删除观察者 * @param observer */ public void removeObserver(Observer observer); /** * 当主题状态发生改变这个方法被调用来通知素有的观察者 */ public void notifyObservers(); }
- 通知接口,该接口只包含一个send()方法,当需要发送对应的通知时,调用此方法。
package toolkit.net.cn; /** * * @author zhaox */ public interface DisplayMessage { /** * 发送通知 */ public void send(); }
- 在AccountData中实际Subject接口,并增加一个ArrayList来纪录观察者。
package toolkit.net.cn; import java.util.ArrayList; import java.util.List; /** * @author zhaox */ public class AccountData implements Subject { /** * 纪录观察者对象 */ private List<Observer> observers; /** * 用户名 */ private String username; /** * 密码 */ private String password; public String getUsername() { return username; } public String getPassword() { return password; } /** * 密码修改后,会调用该方法 */ private void passwordChanged() { } public AccountData(String username, String password) { this.username = username; this.password = password; observers = new ArrayList<>(16); } /** * 注册观察者 * @param observer */ @Override public void registerObserver(Observer observer) { observers.add(observer); } /** * 取消注册 * @param observer */ @Override public void removeObserver(Observer observer) { int index = observers.indexOf(observer); if (index >= 0) { observers.remove(index); } } @Override public void notifyObservers() { int size = observers.size(); for (int i = 0; i < size; i++) { Observer observer = observers.get(i); observer.update(this.getUsername(), this.getPassword()); } } /** * 模拟修改密码 */ public void changePassword(String newPassword) { this.password = newPassword; this.passwordChanged(); } }
- 短信、邮件、公众号、APP对应的四个类,都实现Observer、DispalyMessage接口,修改如下
package toolkit.net.cn; /** * 短信通知 * @author zhaox */ public class SMSDisplay implements Observer, DisplayMessage { private String username; private String password; private Subject accountData; public SMSDisplay(Subject accountData) { this.accountData = accountData; accountData.registerObserver(this); } @Override public void update(String username, String password) { this.username = username; this.password = password; this.send(); } @Override public void send() { System.out.println("短信-尊敬的用户" + username + ", 您的密码已经修改为" + password); } }
package toolkit.net.cn; /** * 邮箱通知 * @author zhaox */ public class EMailDisplay implements Observer, DisplayMessage { private String username; private String password; private Subject accountData; public EMailDisplay(Subject accountData) { this.accountData = accountData; accountData.registerObserver(this); } @Override public void update(String username, String password) { this.username = username; this.password = password; this.send(); } @Override public void send() { System.out.println("邮件-尊敬的用户" + username + ", 您的密码已经修改为" + password); } }
package toolkit.net.cn; /** * 微信公众号通知 * @author zhaox */ public class MPDisaplay implements Observer, DisplayMessage { private String username; private String password; private Subject accountData; public MPDisaplay(Subject accountData) { this.accountData = accountData; accountData.registerObserver(this); } @Override public void update(String username, String password) { this.username = username; this.password = password; this.send(); } @Override public void send() { System.out.println("公众号-尊敬的用户" + username + ", 您的密码已经修改为" + password); } }
package toolkit.net.cn; /** * APP端通知用户 * @author zhaox */ public class APPDisplay implements Observer, DisplayMessage { private String username; private String password; private Subject accountData; public APPDisplay(Subject accountData) { this.accountData = accountData; accountData.registerObserver(this); } @Override public void update(String username, String password) { this.username = username; this.password = password; this.send(); } @Override public void send() { System.out.println("APP-尊敬的用户" + username + ", 您的密码已经修改为" + password); } }
修改main方法测试:
package toolkit.net.cn;
/**
* @author zhaox
*/
public class Main {
public static void main(String[] args) {
AccountData accountData = new AccountData("guest", "mm123456");
SMSDisplay smsDisplay = new SMSDisplay(accountData);
EMailDisplay eMailDisplay = new EMailDisplay(accountData);
MPDisaplay mpDisaplay = new MPDisaplay(accountData);
APPDisplay appDisplay = new APPDisplay(accountData);
accountData.changePassword("mm000000");
System.out.println("----------------------------");
System.out.println("移除公众号通知");
System.out.println("----------------------------");
accountData.removeObserver(mpDisaplay);
accountData.changePassword("mm888888");
}
}
执行main方法,打印结构如下:
移除公众号的通知后,控制台打印中没有了公众号通知类的打印,结果正确。
上面的代码中主题是一个具有状态的对象,观察者使用这些状态,虽然这些状态并不属于他们。有许多观察者依赖主题来告诉他们状态何时改变了。这就产生了一个关系:一个主题对应多个观察者的关系。
这样做的好处是什么?
- 关于观察者对象,主题只知道观察者对象实现了某个接口(Observer接口),主题并不需要关注观察者的具体类是谁,怎么实现的。
- 任何时候都可以新增观察者,因为主题依赖的是一个实现了Observer接口的对象列表,所以我们可以随时增加一个观察者。我们可以在运行时通过代码用心的观察者取代现有的观察者,主题中的代码无需做任何修改,同样的也可以删除某些观察者。
- 改变主题或者观察者任何一方,并不会影响到另外一个方。因为两者是松耦合的,只要他们之间的接口任被遵守,我们就可以自由的修改。当两个对象之间松耦合,他们依然可以介乎,但是不太清楚彼此的细节。
松耦合的设计能够让我们建立有弹性的OO系统,对象之间的互相依赖降到了最低,能够更好的应对变化。