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 ...
随机推荐
- ubantu和虚拟机tools 安装 小问题集结
一.虚拟机 就安装虚拟机而言,个人觉得还是比较简易的,毕竟VMware workstation pro 是一个开源的软件,只要在网上搜索即可,这里我提供一个虚拟机的资源: 链接:http://pan. ...
- HTTP协议、Ajax请求
今天这篇文章呢,主要讲的就是关于HTTP协议.Ajax请求以及一些相关的小知识点.虽然内容不算多,可是是很重点的东西~ HTTP协议 1. http:超文本传输协议.简单.快速.灵活.无状态.无连接. ...
- Python_排版函数
import textwrap doc='''Beautiful is better than ugly. Explicit is better than implicit. Simple is be ...
- PAT1004:Counting Leaves
1004. Counting Leaves (30) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue A fam ...
- PAT1101:Quick Sort
1101. Quick Sort (25) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CAO, Peng There is a ...
- PAT1042:Shuffling Machine
1042. Shuffling Machine (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue Shu ...
- 0517JS综合练习、挂事件练习
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> ...
- Autopep8的使用
什么是Autopep8 在python开发中, 大家都知道,python编码规范是PEP8,但是在市级开发中有的公司严格要求PEP8规范开发, 有的公司不会在乎那些,在我的理解中,程序员如果想走的更高 ...
- Nginx从听说到学会
第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的"同行"Apache吧!Nginx同Apache一样都是一种WEB服务器.基于REST架构风格,以统一 ...
- Netty与网络编程
Netty什么? Netty项目是一个提供异步事件驱动网络应用框架和快速开发可维护的高性能高扩展性服务端和客户端协议工具集的成果.换句话说,Netty是一个NIO客户端服务端框架,它使得快速而简单的开 ...