疫情之下,设计模式之观察者模式

前言

窗外的麻雀在树上多嘴,在家办公的日子,活不多,设计模式系列到是可以继续学习了。坐在二楼卧室,聆听鸟鸣,夹杂着十指滚落键盘和零星传来的人声狗吠,抬头看着窗外的油菜花,还挺惬意的。疫情之下,很多公司都难以幸免的开始降薪,毫不例外,我也中奖了。带着复杂的心情,我敲起了博客。。。今天学习观察者模式。

面向对象思想的实现方式

开发小为最近从项目经理那又拿到了一个需求,是一个生产奶站和订牛奶用户的故事。生产奶站需要把用户订的牛奶发给用户,并且包括含脂量和含钙量,而其中的两个含量值是可以让生产奶站改变的。小为考虑了一下,觉得可以这样设计:
在这里插入图片描述
创建两个类,一个是生产奶站,一个是牛奶用户。其中生产奶产把含脂率和含钙率两个属性和用户订的牛奶产品,通过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内置观察者是先注册后通知,这是一个小细节。

学到这里,观察者模式已经全部学完了,写了不少,花的时间挺长的。如果你能够看到了这里,那说明我写的没有白费,顺手再点个赞吧^ _ ^。

猜你喜欢

转载自blog.csdn.net/m0_37827190/article/details/104633883