Observer观察者模式与OCP开放-封闭原则
在学习Observer观察者模式时发现它符合敏捷开发中的OCP开放-封闭原则, 本文通过一个场景从差的设计开始, 逐步向Observer模式迈进, 最后的代码能体现出OCP原则带来的好处, 最后分享Observer模式在自己的项目中的实现.
场景引入
- 在一户人家中, 小孩在睡觉, 小孩睡醒后需要吃东西.
- 分析上述场景, 小孩在睡觉, 小孩醒来后需要有人给他喂东西.
- 考虑第一种实现, 分别创建小孩类和父亲类, 它们各自通过一条线程执行, 父亲线程不断监听小孩看它有没有醒, 如果醒了就喂食.
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原则, 这是我学习观察者模式最大的收获.
Observer观察者模式与OCP开放-封闭原则的更多相关文章
- OCP开放封闭原则
一.定义 软件实体(类.模块.函数等)应该是可以扩展的,但是不可修改. 如果正确的应用了OCP原则,那么 以后在进行同样的改动时,就只需要添加新的代码,不必修改已经正常运行的代码. 二.OCP概述 1 ...
- 开放封闭原则(Open Closed Principle)
在面向对象的设计中有很多流行的思想,比如说 "所有的成员变量都应该设置为私有(Private)","要避免使用全局变量(Global Variables)",& ...
- 开放-封闭原则(OCP)开-闭原则 和 依赖倒转原则,单一职责原则
单一职责原则 1.单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因 2.如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会消弱或抑制这个类完成其他职责的能力. ...
- 开放封闭原则(OCP)
开放封闭原则 转:http://baike.baidu.com/view/2493421.htm转:http://dev.csdn.net/article/38/38826.shtm 开放封闭原则(O ...
- (转) 面向对象设计原则(二):开放-封闭原则(OCP)
原文:https://blog.csdn.net/tjiyu/article/details/57079927 面向对象设计原则(二):开放-封闭原则(OCP) 开放-封闭原则(Open-closed ...
- 开放-封闭原则(OCP)
怎样的升级才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本呢?开放-封闭原则(The Open-Closed Principle, OCP)为我们提供了指引.软件 ...
- 设计模式六大原则——开放封闭原则(OCP)
什么是开闭原则? 定义:是说软件实体(类.模块.函数等等)应该可以扩展,但是不可修改. 开闭原则主要体现在两个方面: 1.对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况. ...
- 1开放封闭原则OCP
一.什么是开放封闭原则 开放封闭原则(Open-Closed Principle):一个软件实体 应当对扩展开放,则修改关闭. 在设计一个模块时,应当使得这个模块可以在不被修 改的前提下被扩展.也就是 ...
- C++ 设计模式 开放封闭原则 简单示例
C++ 设计模式 开放封闭原则 简单示例 开放封闭原则(Open Closed Principle)描述 符合开放封闭原则的模块都有两个主要特性: 1. 它们 "面向扩展开放(Open Fo ...
随机推荐
- spring+springmvc+mybatis构建系统
今天和大家分享的是spring+springmvc+mybatis搭建框架的例子,说到这里不得不说现在市面上一流大公司还有很多用这种架子,创业型公司大部分都用springboot集成的mvc+myba ...
- Spring Aop技术原理分析
本篇文章从Aop xml元素的解析开始,分析了Aop在Spring中所使用到的技术.包括Aop各元素在容器中的表示方式.Aop自动代理的技术.代理对象的生成及Aop拦截链的调用等等.将这些技术串联起来 ...
- PHP之连接mysql小练习
mysql Test.sql 1 -- phpMyAdmin SQL Dump -- version 4.6.6 -- https://www.phpmyadmin.net/ -- -- Host: ...
- PHP之cookies小练习
//5-1.php 1 <? error_reporting(E_ALL ^ E_NOTICE); if ($_COOKIE['username']!="") { echo ...
- 【转】MySQL datetime数据类型设置当前时间为默认值
转自http://blog.csdn.net/u014694759/article/details/30295285 方法一: MySQL目前不支持列的Default 为函数的形式,如达到你某列的默认 ...
- 开启irqbalance提升服务器性能
操作系统 性能调休 公司有次压测存在一个问题:CPU资源压不上去,一直在40%已达到了性能瓶颈,后定位到原因,所在的服务器在压测过程中产生的中断都落在CPU0上处理,这种中断并没有均衡到各个CPU ...
- 关于CSS定位属性 position 的使用
CSS中一般通过浮动和定位来对标签进行位置操作.下面我们来讨论一下定位的用法和需要注意的地方. 1.首先,说一下position的几个属性值 (1)none属性值,这个是定义不进行定位,默认为不定位, ...
- 如何通过织云 Lite 愉快地玩转 TSW
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 织云 Lite & TSW 织云 Lite 是一款轻量型服务管理平台,提供标准化的应用打包操作,可连接持续集成系统,完成线上程序分发 ...
- mac上如何解压和压缩rar文件
许多喜欢mac的人都知道,这个os没有像win上winRAR或者hao123解压等类似软件,对于文件的压缩和解压很不方便,在下载rar的文件包之后就会束手无策,很是尴尬至极,为了避免这种情况,自己动手 ...
- BZOJ_1316_树上的询问_点分治
BZOJ_1316_树上的询问_点分治 Description 一棵n个点的带权有根树,有p个询问,每次询问树中是否存在一条长度为Len的路径,如果是,输出Yes否输出No. Input 第一行两个整 ...