Observer观察者模式与OCP开放-封闭原则

在学习Observer观察者模式时发现它符合敏捷开发中的OCP开放-封闭原则, 本文经过一个场景从差的设计开始, 逐步向Observer模式迈进, 最后的代码能体现出OCP原则带来的好处, 最后分享Observer模式在本身的项目中的实现.html

场景引入

  • 在一户人家中, 小孩在睡觉, 小孩睡醒后须要吃东西.
  • 分析上述场景, 小孩在睡觉, 小孩醒来后须要有人给他喂东西.
  • 考虑第一种实现, 分别建立小孩类和父亲类, 它们各自经过一条线程执行, 父亲线程不断监听小孩看它有没有醒, 若是醒了就喂食.
public class Observer {
    public static void main(String[] args) {
        Child c = new Child();
        Dad d = new Dad(c);
        new Thread(d).start();
        new Thread(c).start();
    }
}

class Child implements Runnable {
    boolean wakenUp = false;//是否醒了的标志, 供父亲线程探测

    public void wakeUp(){
        wakenUp = true;//醒后设置标志为true
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000);//睡3秒后醒来.
            wakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public boolean isWakenUp() {
        return wakenUp;
    }
}

class Dad implements Runnable{
    private Child c;

    public Dad(Child c){
        this.c = c;
    }

    public void feed(){
        System.out.println("feed child");
    }

    @Override
    public void run() {
        while(true){
            if(c.isWakenUp()){//每隔一秒看看孩子是否醒了
                feed();//醒了就喂饭
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

 

  • 本设计的不合理之处: 父亲线程要每隔一秒去查看一次孩子是否醒了没, 若是小孩连睡三个小时, 父亲线程岂不得连着3个小时每隔一秒访问一下, 这样将极大地耗费掉cpu的资源. 父亲线程也不方便去作些其余的事情.
  • 这能够说是一个糟糕的设计, 迫使咱们对他做出改进. 下面为了能让父亲能正常干活, 咱们把逻辑修改成改成小孩醒后通知父亲喂食.
public class Observer {
    public static void main(String[] args) {
        Dad d = new Dad();
        Child c = new Child(d);
        new Thread(c).start();
    }
}

class Child implements Runnable {
    private Dad d;//持有父亲对象引用

    public Child(Dad d){
        this.d = d;
    }

    public void wakeUp(){
        d.feed();//醒来通知父亲喂饭
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000);//假设睡3秒后醒
            wakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Dad{
    public void feed(){
        System.out.println("feed child");
    }
}
复制代码

 

  • 以上的版本比起原版在性能上有了提高, 可是小孩醒后只能固定调用父亲的喂食方法, 父亲不知道任何小孩醒来的任何信息, 好比几点钟醒的, 睡了多久. 咱们的程序应该具备适当的弹性, 可扩展性, 深刻分析下, 小孩醒了是一个事件, 小孩醒来的时间不一样, 父亲喂食的食材也可能不一样, 那么如何把小孩醒来这一事件的信息告诉父亲呢?
  • 若是对上面的代码进行改动的话, 最直接的方法就是给小孩添加睡醒时间字段, 调用父亲的feed(Child c)方法时把本身做为参数传递给父亲, 父亲经过小孩对象就能得到小孩醒来时的具体信息.
  • 可是根据面向对象思想, 醒来的时间不该该是小孩的属性, 而应该是小孩醒来这件事情的属性, 咱们应该考虑建立一个事件类.
  • 一样是在面向对象对象的原则下, 父亲对小孩进行喂食是父亲的行为, 与小孩无关, 因此小孩应该只负责通知父亲, 具体的行为由父亲决定, 咱们还应该考虑舍弃父亲的feed()方法, 改为一个更加通用的actionToWakeUpEvent, 对起床事件做出响应的方法.
  • 并且小孩醒来后可能不仅被喂饭, 还可能被抱抱, 因此父亲对待小孩醒来事件的方法能够定义的更加灵活.
public class Observer {
    public static void main(String[] args) {
        Dad d = new Dad();
        Child c = new Child(d);
        new Thread(c).start();
    }
}

class Child implements Runnable {
    private Dad d;

    public Child(Dad d){
        this.d = d;
    }

    public void wakeUp(){//经过醒来事件让父亲做出响应
        d.actionToWakeUpEvent(new WakeUpEvent(System.currentTimeMillis(), this));
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            wakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Dad{
    public void actionToWakeUpEvent(WakeUpEvent event){
        System.out.println("feed child");
    }
}

class WakeUpEvent{
    private long time;//醒来的事件
    private Child source;//发出醒来事件的源

    public WakeUpEvent(long time, Child source){
        this.time = time;
        this.source = source;
    }
}
复制代码
  • 显然这个版本的可扩展性高了一些, 咱们接着分析. 因为如今对小孩醒来事件的动做已经不止于喂食了, 若是如今加入一个爷爷类的话, 可让爷爷在小孩醒来的时候做出抱抱小孩的响应.
  • 可是引来的问题是, 要让爷爷知道小孩醒了, 必须在小孩类中添加爷爷字段, 假如还要让奶奶知道小孩醒了, 还要添加奶奶字段, 这种不断修改源代码的作法意味着咱们的程序还存在改进的地方.
  • 在《敏捷软件开发:原则、模式与实践》一书中曾谈到OCP(开发-封闭原则), 里面指出软件类实体(类, 模块, 函数等)应该是能够扩展的, 可是不可修改的. 为了知足OCP原则, 最关键的地方在于抽象, 在本例中, 咱们能够把监听小孩醒来事件向上抽象出一个接口, 接口中有惟一的监听醒来事件的方法. 实现该接口的实体类能够根据醒来事件做出各自的动做.
  • 小孩发出醒来事件后能够不单止通知父亲一人, 他能够把醒来事件发送给全部在他这注册过的监听者.
  • 因此看成出这样的抽象后, 就不单止孩子能发出醒来的事件了, 小狗也能发出醒来的事件, 并被监听.
public class Observer {
    public static void main(String[] args) {
        Child c = new Child();
        c.addWakeUpListener(new Dad());
        c.addWakeUpListener(new GrandFather());
        c.addWakeUpListener(new Dog());
        new Thread(c).start();
    }
}

class Child implements Runnable {
    private ArrayList<WakeUpListener> list = new ArrayList<>();

    public void addWakeUpListener(WakeUpListener l){//对外提供注册监听的方法
        list.add(l);
    }

    public void wakeUp(){
        for(WakeUpListener l : list){//通知全部监听者
            l.actionToWakeUpEvent(new WakeUpEvent(System.currentTimeMillis(), this));
        }
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            wakeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

interface WakeUpListener{
    public void actionToWakeUpEvent(WakeUpEvent event);
}

class Dad implements WakeUpListener{
    @Override
    public void actionToWakeUpEvent(WakeUpEvent event){
        System.out.println("feed child");
    }
}

class GrandFather implements WakeUpListener{
    @Override
    public void actionToWakeUpEvent(WakeUpEvent event) {
        System.out.println("hug child");
    }
}

class Dog implements WakeUpListener{
    @Override
    public void actionToWakeUpEvent(WakeUpEvent event) {
        System.out.println("wang wang...");
    }
}

class WakeUpEvent{
    private long time;
    private Child source;//事件源

    public WakeUpEvent(long time, Child source){
        this.time = time;
        this.source = source;
    }
}
复制代码
  • 经过上面的例子, 咱们能清楚地看到整个观察者模式的模型, 当一个对象的发出某个事件后, 会通知全部的依赖对象, 在OCP原则下, 依赖对象响应事件的具体动做和事件发生源是彻底解耦的, 咱们能够在不修改源码的状况下随时加入新的事件监听者, 做出新的响应.

 

在联网坦克项目中使用观察者模式

  • 以前写了个网络版的坦克小游戏, 这里是项目的GitHub地址
  • 在学习观察者模式后进一步考虑游戏中能够改进的地方. 如今子弹打中坦克的逻辑是这样的: 子弹检测到打中坦克后, 首先它会设置本身的生命为false, 而后设置坦克的生命也为false, 最后产生一个爆炸并向服务器发送响应的消息.
public boolean hitTank(Tank t) {//子弹击中坦克的方法
        if(this.live && t.isLive() && this.good != t.isGood() && this.getRect().intersects(t.getRect())) {
            this.live = false;//子弹死亡
            t.setLive(false);//坦克死亡
            tc.getExplodes().add(new Explode(x - 20, y - 20, tc));//产生一个爆炸
            return true;
        }
        return false;
    }
复制代码
  • 这个设计显然不太符合面向对象思想, 由于子弹打中坦克后, 子弹设置为死亡是子弹的事, 可是坦克死亡则应该是坦克本身的事情.
  • 在本来的设计中, 若是咱们想给坦克加上血条不但愿它被打中一次就死亡, 那么就得在子弹打中坦克的方法中修改, 代码的可维护性下降了.
  • 下面将使用Observer观察者模式对这部分代码进行重写, 让坦克本身对被子弹打中做出响应, 并给坦克加入血条, 每被打中一次扣20滴血.
/** * 坦克被击中事件监听者(由坦克实现) */
public interface TankHitListener {
    public void actionToTankHitEvent(TankHitEvent tankHitEvent);
}

public class TankHitEvent {
    private Missile source;

    public TankHitEvent(Missile source){
        this.source = source;
    }
    //省略 get() / set() 方法...
}

/* 坦克类 */
public class Tank implements TankHitListener {
    //...
    
    @Override
    public void actionToTankHitEvent(TankHitEvent tankHitEvent) {
        this.tc.getExplodes().add(new Explode(tankHitEvent.getSource().getX() - 20,
                tankHitEvent.getSource().getY() - 20, this.tc));//坦克自身产生一个爆炸
        if(this.blood == 20){//坦克每次扣20滴血, 若是只剩下20滴了, 那么就标记为死亡.
            this.live = false;
            TankDeadMsg msg = new TankDeadMsg(this.id);//向其余客户端转发坦克死亡的消息
            this.tc.getNc().send(msg);
            this.tc.getNc().sendClientDisconnectMsg();//和服务器断开链接
            this.tc.gameOver();
            return;
        }
        this.blood -= 20;//血量减小20并通知其余客户端本坦克血量减小20.
        TankReduceBloodMsg msg = new TankReduceBloodMsg(this.id, tankHitEvent.getSource());//建立消息
        this.tc.getNc().send(msg);//向服务器发送消息
    }
    
    //...
}
/* 子弹类 */
public class Missile {
    //...
    
    public boolean hitTank(Tank t) {//子弹击中坦克的方法
        if(this.live && t.isLive() && this.good != t.isGood() && this.getRect().intersects(t.getRect())) {
            this.live = false;//子弹死亡
            t.actionToTankHitEvent(new TankHitEvent(this));//告知观察的坦克被打中了
            return true;
        }
        return false;
    }

    //...
}
复制代码

 

总结

  • 观察者模式遵循了OCP原则, 在这种消息广播模型中运用观察者模式能提升咱们程序的可扩展性与可维护性.
  • 从实战项目咱们也能够看到, 若是要运用观察者模式必然要增添一些代码量, 对应的是开发成本的增长, 在坦克项目中我是为使用设计模式而使用设计模式, 其实若是仅仅从简单能用的角度来看, 观察者模式可能不是一种最佳选择.
  • 但因为如今处于学习阶段, 我认为不能由于项目小而不追求更合理的设计, 观察者模式实现了消息发布者和观察者之间的解耦, 使得观察者可以独立处理响应, 符合面向对象思想; 同时对观察者进行抽象, 使得咱们能够不修改源码, 经过添加的方式加入更多的观察者, 符合OCP原则, 这是我学习观察者模式最大的收获.

 

相关文章
相关标签/搜索