设计模式分享二

设计模式分享二

上次分享了设计模式中的单例模式,工厂模式,适配器模式和命令模式。对设计模式有了初步的理解。各使用一句话总结以上4个设计模式。程序中只需要一份实例。将对象的生成统一管理,让程序依赖抽象而非具体,依赖接口而非实现,在程序运行的时候才生成抽象或者接口对应的实例。依据已有的代码做上层数据转换实现新的功能。将功能原子化。将命令的发出和执行分为不同的实体来实现(调用者和执行者),支持宏命令和撤销功能,以上就是对上次设计模式分享的回顾。

本次计划分享的设计模式有代理模式,观察者模式,状态模式和生产者-消费者-仓库模式。

  • 代理模式:

定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。举例说明,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。

场景:在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之前起到中介的作用。或者,通过代理能统一对象引用前后各种逻辑处理。

优势:多个实现类实现之后,如果有统一的修改,不去修改每个实现类中的代码,对修改关闭,而去修改代理类。保证功能的纯洁性。比如,权限控制,日志管理。

实例一:游戏杀怪升级

抽象接口:

1510736621(1)

具体实现类:

clipboard

代理类实现:

clipboard

不同的实现类实现了对扩展开放,对修改关闭。而修改在代理类中实现。同时,对于通用的业务逻辑在代理类中实现,保持了实现类的功能单纯性。

静态代理测试:

1510737635(1)

打印执行结果:

1510737765(1)


结果可以看出,在代理类中,不仅依赖实现类实现了原有的业务功能,也在代理类中实现了其它业务逻辑,便于扩展。

实例二:以买车为例,消费者实现买车的具体动作。但是在买车前后还有很多事情,判断钱多少或者卖车险等等。所以,当前去4s店买车就是消费者的代理类。

买车接口:

1510738695(1)

买车实现(消费者买车)

1510738735(1)

4s店代理模式:

clipboard

静态测试类:

1510810982

运行结果:

1510811315(1)

动态代理:

动态代理实现类:实现InvocationHandler接口

主要使用了反射的知识。InvocationHandler就是反射功能的接口

clipboard

动态代理测试类:

1510813716(1)

动态代理测试结果:

1510813821(1)

使用Java动态代理的好处:

1、减少编程的工作量:假如需要实现多种代理处理逻辑,只要写多个代理处理器就可以了(处理代理附带代码),无需每种方式都写一个代理类。

2、系统扩展性和维护性增强,程序修改起来也方便多了(一般只要改代理处理器类就行了)。

代理模式,真是鬼斧神工,叹为观止。

代理模式完成了开闭原则,对修改关闭。业务逻辑修改集中在代理类中。横向业务扩展是可以的。

  • 观察者模式

定义

观察者模式中,一个被观察者管理所有相依于它的观察者物件,并且在本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

角色

抽象被观察者角色:把所有对观察者对象的引用保存在一个集合中,每个被观察者角色都可以有任意数量的观察者。被观察者提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。

抽象观察者角色:为所有具体的观察者定义一个接口,在得到主题的通知时更新自己。

具体被观察者角色:在被观察者内部状态改变时,给所有登记过的观察者发出通知。具体被观察者角色通常用一个子类实现。

具体观察者角色:该角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。通常用一个子类实现。如果需要,具体观察者角色可以保存一个指向具体主题角色的引用。

适用场景

1) 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

2) 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。

3) 当一个对象必须通知其它对象,而它又不能假定其它对象是谁(希望不是具体的对象,所以是接口对象的list)。换言之, 你不希望这些对象是紧密耦合的。

实例一:

老师布置作业,多个同学作为观察者关注老师的动向,一旦老师布置作业,就会通知所有关注的学生。学生依据老师布置得做作业完成家庭作业。

抽象观察者角色(学生接口):

1510819088(1)

具体观察者(学生的实体类):

创建具体观察者,就给被观察者添加注册。同时实现自己的具体(变化逻辑-抽象观察接口方法)

1510819124(1)

抽象被观察者接口(添加观察者,移出观察者,通知观察者三个方法):

1510819285(1)

具体被观察者(观察者List,移出,添加,通知的具体实现):

clipboard

观察者模式测试类:

1510819716(1)

测试类运行结果:

clipboard

被观察者状态发生变化,一次通知所有观察者。各自的观察者发生变化。

实例代码二:

抽象的观察者:

1510820544(1)

具体观察者 - 保安

1510820583(1)

具体观察者 - 警察

1510820616(1)

具体观察者-盗贼

1510820674(1)

抽象的被观察者,在其中声明方法(添加、移除观察者,通知观察者):

1510820744(1)

具体观察者 -宝物运输

1510820807(1)

测试类:

1510820974(1)

测试结果:

clipboard

通过第二个实例代码,更加清晰访问者模式的各个角色以及之间的组织结构。观察者模式简单,思路清晰。观察者又称之为订阅/发布模式。在事件主动知会的情景下使用。

  • 状态模式

定义

当一个对象的状态改变时允许改变行为。这时候,对象看起来像是改变了类型。状态模式的核心在于封装,状态变更引起行为的变更,从外部看好像是对应的类发生了变化。

个人理解类似switch/case或者if/else语句,不过状态模式使用设计的比较巧妙,对扩展开放,对修改关闭。

实例场景:游戏NPC不断有新的功能添加,需要在不修改以前代码的情况下去不断扩展功能。

状态接口:

1510823638(1)

具体状态--攻击

1510823679(1)

具体状态--逃跑

1510823719(1)

具体状态--潜伏

1510823750(1)

具体状态--巡视

1510823781(1)

状态工厂(引入状态工厂--根据状态NPC获取加载不同的具体状态)接口

1510823880(1)

具体状态:

1510823909(1)

使用状态类(当前为NPC)

clipboard

测试类:

1510824216(1)

测试执行结果:

1510824309(1)

测试类可以看出,NPC状态发生变化,状态对应的行为也发生了变化。本质上是行为跟着状态走,状态为NPC的属性,依赖抽象状态所以具体的状态可以切换。这样的设计让代码更加紧凑,简洁。易于扩展。

  • 生产者-消费者-仓库模式

生产者消费者模式是并发、多线程编程中经典的设计模式,生产者和消费者通过分离的执行工作解耦,简化了开发模式,生产者和消费者可以以不同的速度生产和消费数据。

生产者消费者模型旨在解决什么问题:生产者和消费者之间用中间类似一个队列一样的东西串起来。这个队列可以想像成一个存放产品的“仓库”,生产者只需要关心这个“仓库”,并不需要关心具体的消费者,对于生产者而言甚至都不知道有这些消费者存在。对于消费者而言他也不需要关心具体的生产者,到底有多少生产者也不是他关心的事情,他只要关心这个“仓库”中还有没有东西。这种模型是一种松耦合模型。这个模型的产生就是为了复用和解耦。

1511310651(1)

UML类图:

1511310767(1)

生产者和消费者模式在生活当中随处可见,它描述的是协调与协作的关系。

优点:它的确是一种实用的设计模式,常用于编写多线程或并发代码

  1. 它简化的开发,你可以独立地或并发的编写消费者和生产者,它仅仅只需知道共享对象是谁。
  2. 生产者不需要知道谁是消费者或者有多少消费者,对消费者来说也是一样。
  3. 生产者和消费者可以以不同的速度执行。
  4. 分离的消费者和生产者在功能上能写出更简洁、可读、易维护的代码。

实现方式:

  1. 塞队列实现生产者消费者模式

阻塞队列实现生产者消费者模式超级简单,它提供开箱即用支持阻塞的方法put()和take(),开发者不需要写困惑的wait-nofity代码去实现通信。BlockingQueue 一个接口,Java5提供了不同的现实,如ArrayBlockingQueue和LinkedBlockingQueue,两者都是先进先出(FIFO)顺序。而ArrayLinkedQueue是自然有界的,LinkedBlockingQueue可选的边界。下面这是一个完整的生产者消费者代码例子,对比传统的wait、nofity代码,它更易于理解。

代码如下:

生产者:

1511311355(1)

其中,生产者继承runnable接口,依赖BlockingQueue队列仓库。生产产品使用sharedQueue.put(i);其中put方法在实现的过程中加锁,保证了队列添加队列的时候,其它生产者和消费者线程都不能访问队列。

1511312308(1)

1511312328(1)

clipboard

消费者:消费者依赖队列仓库,take()方法获取的时候同样加锁,独占队列资源。如果资源为空,则当前消费者线程等待。

1511312622(1)

测试类:

1511312967(1)

运行结果,由于生产者和消费者都是单线程执行,生产者,消费者结果都比较好看:

1511313136(1)

消费者和生产者都是多线程运行结果:

1511314362(1)

由以上例子可以看出,使用LinkedBlockingQueue作为仓库,有LinkedBlockingQueue的put和take方法自己来保证对资源的独占,解决了多线程对资源占用的问题。让问题变得简单。初次之外,示例中仓库队列选择的是LinkedBlockingQuence,它具有linkedlist的属性,具备自动扩容的属性。所以,在具体交互中没有仓库满了这种情况,所以会比较简单。

这种简单的实现方式在 城市观测盒子 程序中有使用到。需求场景,城市多个监测点会随机的上报检测数据。存在高并发,需要相应及时,不能丢失数据。数据上传为socket通道。当时选择的处理方式,多个线程监控socket通道,由于每个城市检测仪器和service连接会产生一个session,所以,每个生产者监控一批session,周期性的扫描。如果session有数据上传,就把session使用put方法加入到通道队列中去。所以,每个生产者对service而言是周期性扫描固定的session,每个消费者每个时刻处理一个session,不会产生资源抢占。对通道队列而言,所有的消费者都对应同一个通道。存在资源抢占,完全有可能同一个时刻多个生产者 都有数据进入队列的操作。使用put(),内部维护了通道加锁的机制。同样,对于消费者而言,也是所有的消费者对应同一个通道,存在队列抢占的情况,使用take()方法避免多线程对资源的抢占。

clipboard

以上就是两次对设计模式的分享。设计模式的理解就是在特定需求场景下的一特定的设计经验。让代码依赖于抽象而非具体,依赖于接口而非实现。对修改关闭,对扩展开放。

下次分享的话,想着能和大家分享集中特别重要的知识点,spring框架的核心思想(依赖注入和控制反转),易筋经之反射(结合springBean分析),多线程。

2017年11月16日 17:34:29

毛平

posted @ 2018-02-27 11:20  IT迷途小书童  阅读(244)  评论(0编辑  收藏  举报