前言
窗外的麻雀在树上多嘴,在家办公的日子,活不多,设计模式系列到是可以继续学习了。坐在二楼卧室,聆听鸟鸣,夹杂着十指滚落键盘和零星传来的人声狗吠,抬头看着窗外的油菜花,还挺惬意的。疫情之下,很多公司都难以幸免的开始降薪,毫不例外,我也中奖了。带着复杂的心情,我敲起了博客。。。今天学习观察者模式。
面向对象思想的实现方式
开发小为最近从项目经理那又拿到了一个需求,是一个生产奶站和订牛奶用户的故事。生产奶站需要把用户订的牛奶发给用户,并且包括含脂量和含钙量,而其中的两个含量值是可以让生产奶站改变的。小为考虑了一下,觉得可以这样设计:
创建两个类,一个是生产奶站,一个是牛奶用户。其中生产奶产把含脂率和含钙率两个属性和用户订的牛奶产品,通过sendData()方法发送给牛奶用户,牛奶用户接受牛奶及其信息即可。直接上代码如下:
生产奶站类:
/**
* 生产奶站
*/
public class MilkStore {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
private CattleMilkUser cattleMilkUser;
public MilkStore(CattleMilkUser cattleMilkUser){
this.cattleMilkUser = cattleMilkUser;
}
public BigDecimal getFatContent() {
return fatContent;
}
public BigDecimal getCalciumContent() {
return calciumContent;
}
public void sendData(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
cattleMilkUser.getInfo(getFatContent(),getCalciumContent());
}
}
可以看到,生产奶站类在构造方法中直接将牛奶用户对象传进去,以便防止后续牛奶用户对象为空,它也是用来将生产奶站中的含脂量、含钙量传给牛奶用户对象,然后再调用getInfo()将数据传入。
牛奶用户对象:
/**
* 牛奶用户
*/
public class CattleMilkUser {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
public void getInfo(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
receiveMilk();
}
/**
* 牛奶用户收到牛奶
*/
public void receiveMilk(){
System.out.println("牛奶用户收到牛奶,其中含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
之前说过含脂率和含钙率可以人为改变的,所以我这边把数据会先保存在类的全局变量中,再去使用,然后牛奶用户收到牛奶后输出对应的含脂率和含钙率。
测试代码如下:
/**
* 测试类
*/
public class TestMilk {
public static void main(String[] args) {
CattleMilkUser cattleMilkUser = new CattleMilkUser();
MilkStore milkStore = new MilkStore(cattleMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
}
}
执行结果如下:
牛奶用户收到牛奶,其中含脂率:7%,含钙率:6%
到这里,前面的需求基本是实现了。过了一会又有个羊奶用户想订羊奶,然后收到羊奶时也想知道对应的含脂率和含钙率。小为一想,简单啊,先创建一个羊奶用户,在MilkStore 类的构造函数中添加一行羊奶用户,在sendData()方法中添加一行羊奶用户,不就搞定了。代码如下:
羊奶用户:
/**
* 羊奶用户
*/
public class GoatsMilkUser {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
public void getInfo(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
receiveMilk();
}
/**
* 羊奶用户收到牛奶
*/
public void receiveMilk(){
System.out.println("羊奶用户收到羊奶,其中含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
生产奶站MilkStore 类只贴了需要增加的代码:
private GoatsMilkUser goatsMilkUser;
public MilkStore(CattleMilkUser cattleMilkUser,GoatsMilkUser goatsMilkUser){
this.cattleMilkUser = cattleMilkUser;
this.goatsMilkUser = goatsMilkUser;
}
public void sendData(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
cattleMilkUser.getInfo(getFatContent(),getCalciumContent());
goatsMilkUser.getInfo(getFatContent(),getCalciumContent());
}
测试代码:
/**
* 测试类
*/
public class TestMilk {
public static void main(String[] args) {
CattleMilkUser cattleMilkUser = new CattleMilkUser();
GoatsMilkUser goatsMilkUser = new GoatsMilkUser();
MilkStore milkStore = new MilkStore(cattleMilkUser,goatsMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
}
}
结果如下:
牛奶用户收到牛奶,含脂率:7%,含钙率:6%
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
看到这里上述需求也实现了,但是如果又出现马奶呢,而且牛奶用户不想订了呢,这样一看扩展性相当不好。那怎么办呢,今天学的观察者模式终于可以闪亮登场了。
观察者模式的实现方式
我们先分析一下,上一篇文章我也说过,实现需求的过程中,要思考那些是变化的那些是没变的,将那些变化的部分抽象成接口+实现。这里是用户会变,所以将它们抽象接口+实现。用户一变,生产奶站里面的方法也要变化,那也可以抽象成接口+实现。那用观察者模式很好的能处理这类问题。
观察者模式:对象之间多对一依赖的一种设计方案,被依赖的对象为Subject,依赖的对象为Observer,Subject通知Observer变化。
1、这里的Subject就是生产奶站;这里的Observer就是订奶用户。
2、Subject(主题):注册、移除和通知。
3、Observer(观察者):接受输入。
那我们现在可以设计成这样了,如下:
对象间一对多的依赖关系就可以使用观察者模式,生产奶站是一,订奶用户是多。这里面接口就对应了注册、移除和通知三个方法,MilkStore实现Subject接口但还是保留自己之前的方法。
代码如下:
Subject接口
/**
* 主题
*/
public interface Subject {
/**
* 注册
*/
void registerObserver(Observer o);
/**
* 移除
*/
void removeObserver(Observer o);
/**
* 通知
*/
void notifyObserver();
}
观察者接口:
/**
* 观察者
*/
public interface Observer {
/**
* 接受输入
*/
void update(BigDecimal fatContent, BigDecimal calciumContent);
}
实现Subject接口后的生产奶站类:
/**
* 生产奶站
*/
public class MilkStore implements Subject{
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
private List<Observer> observerList;
public MilkStore(){
observerList = new ArrayList<>();
}
public BigDecimal getFatContent() {
return fatContent;
}
public BigDecimal getCalciumContent() {
return calciumContent;
}
public void sendData(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
notifyObserver();
}
@Override
public void registerObserver(Observer o) {
observerList.add(o);
}
@Override
public void removeObserver(Observer o) {
if(observerList.contains(o)){
observerList.remove(o);
}
}
@Override
public void notifyObserver() {
for (Observer observer : observerList) {
observer.update(getFatContent(),getCalciumContent());
}
}
}
用一个list将观察者们装起来,然后将注册,移除和通知三个方法实现。第三个通知方法,里面用到了for循环遍历,目前是简化的例子直接将参数数据传给了观察者。这里面可能考虑到方法里的参数会有变化,参数大大小,可以选择给观察者一个通知,然后让观察者自己去拉取数剧会更好。
实现了观察者接口的牛奶用户类:
/**
* 牛奶用户
*/
public class CattleMilkUser implements Observer{
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
@Override
public void update(BigDecimal fatContent, BigDecimal calciumContent) {
this.fatContent = fatContent;
this.calciumContent = calciumContent;
receiveMilk();
}
/**
* 牛奶用户收到牛奶
*/
private void receiveMilk(){
System.out.println("牛奶用户收到牛奶,含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
和之前的类相比,只是把getInfo()改为update()方法。
实现了观察者接口的羊奶用户类:
/**
* 羊奶用户
*/
public class GoatsMilkUser implements Observer{
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
@Override
public void update(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
receiveMilk();
}
/**
* 羊奶用户收到牛奶
*/
private void receiveMilk(){
System.out.println("羊奶用户收到羊奶,其中含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
测试类:
/**
* 测试类
*/
public class TestSubject {
public static void main(String[] args) {
CattleMilkUser cattleMilkUser = new CattleMilkUser();
GoatsMilkUser goatsMilkUser = new GoatsMilkUser();
MilkStore milkStore = new MilkStore();
//注册牛奶用户
milkStore.registerObserver(cattleMilkUser);
//注册羊奶用户
milkStore.registerObserver(goatsMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
}
}
执行结果如下:
牛奶用户收到牛奶,含脂率:7%,含钙率:6%
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
现在牛奶用户不想订了,就可以移除掉,代码如下:
/**
* 测试类
*/
public class TestSubject {
public static void main(String[] args) {
CattleMilkUser cattleMilkUser = new CattleMilkUser();
GoatsMilkUser goatsMilkUser = new GoatsMilkUser();
MilkStore milkStore = new MilkStore();
//注册牛奶用户
milkStore.registerObserver(cattleMilkUser);
//注册羊奶用户
milkStore.registerObserver(goatsMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
System.out.println("------------------------------------");
//移除牛奶用户
milkStore.removeObserver(cattleMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
}
}
执行结果如下:
牛奶用户收到牛奶,含脂率:7%,含钙率:6%
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
------------------------------------
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
可以看到,观察者模式能够很好的动态去扩展对象间的依赖,即使再多一个马奶,只需要创建一个马奶类,实现观察者接口就可以了,很灵活。而且即便是生产奶站死机了,牛奶用户或羊奶用户能正常运转,这个就达到了松耦合的目的,反过来也一样。到这里设计模式的观察者模式基本上学完了。
java内置观察者
Java本身提供了内置的观察者模式。
1、内置观察者的Observable 对应 设计模式观察者的Subject。
2、内置观察者的Observer 对应 设计模式观察者Observer。
区别:Observable是一个类,虽然是个类,但是它已经实现了注册、移除和通知方法,所以我们去继承Observable类的时候就不用再去实现注册、移除和通知方法了。
直接上代码,继承Observable类之后的生产奶站类:
/**
* 生产奶站
*/
public class MilkStore extends Observable {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
private List<Observer> observerList;
public MilkStore(){
observerList = new ArrayList<>();
}
public BigDecimal getFatContent() {
return fatContent;
}
public BigDecimal getCalciumContent() {
return calciumContent;
}
public void sendData(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
this.setChanged();
this.notifyObservers(new Data(getFatContent(),getCalciumContent()));
}
/**
* 数据对象,方便java内置观察者通知方法使用
*/
public class Data{
/**
* 含脂率
*/
public BigDecimal fatContent;
/**
* 含钙率
*/
public BigDecimal calciumContent;
public Data(BigDecimal fatContent,BigDecimal calciumContent){
this.fatContent = fatContent;
this.calciumContent = calciumContent;
}
}
}
因为java内置观察者的通知有两个方法,我现在使用的是和上面的设计模式观察者例子一样的通知方法,直接将数据传给观察者,所以我写了个内部类Data,不一定非要写内部类,我是为了方便。
实现了内置观察者的牛奶用户类:
/**
* 牛奶用户
*/
public class CattleMilkUser implements Observer {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
@Override
public void update(Observable o, Object arg) {
this.fatContent = ((MilkStore.Data)(arg)).fatContent;
this.calciumContent = ((MilkStore.Data)(arg)).calciumContent;
receiveMilk();
}
/**
* 牛奶用户收到牛奶
*/
private void receiveMilk(){
System.out.println("牛奶用户收到牛奶,含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
主要update方法获取参数发生了变化,下同。
实现了内置观察者的羊奶用户类:
/**
* 羊奶用户
*/
public class GoatsMilkUser implements Observer {
/**
* 含脂率
*/
private BigDecimal fatContent;
/**
* 含钙率
*/
private BigDecimal calciumContent;
@Override
public void update(Observable o, Object arg) {
this.fatContent = ((MilkStore.Data)(arg)).fatContent;
this.calciumContent = ((MilkStore.Data)(arg)).calciumContent;
receiveMilk();
}
/**
* 羊奶用户收到牛奶
*/
private void receiveMilk(){
System.out.println("羊奶用户收到羊奶,其中含脂率:"+fatContent+"%,含钙率:"+calciumContent+"%");
}
}
测试类:
/**
* 测试类
*/
public class TestSubject {
public static void main(String[] args) {
CattleMilkUser cattleMilkUser = new CattleMilkUser();
GoatsMilkUser goatsMilkUser = new GoatsMilkUser();
MilkStore milkStore = new MilkStore();
//注册牛奶用户
milkStore.addObserver(cattleMilkUser);
//注册羊奶用户
milkStore.addObserver(goatsMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
System.out.println("------------------------------------");
//移除牛奶用户
milkStore.deleteObserver(cattleMilkUser);
milkStore.sendData(BigDecimal.valueOf(7),BigDecimal.valueOf(6));
}
}
执行结果如下:
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
牛奶用户收到牛奶,含脂率:7%,含钙率:6%
------------------------------------
羊奶用户收到羊奶,其中含脂率:7%,含钙率:6%
看到结果,我突然发现java内置观察者的通知顺序和设计模式观察者的不一样,设计模式观察者是先进先出的,先注册先通知。而java内置观察者是先注册后通知,这是一个小细节。
学到这里,观察者模式已经全部学完了,写了不少,花的时间挺长的。如果你能够看到了这里,那说明我写的没有白费,顺手再点个赞吧^ _ ^。