行为型模式主要是用在描述类或对象的交互以及职责划分。
另外可以根据处理范围的不同,设计模式又可以分为类模式和对象模式,类模式主要处理类与子类的关系
通过处理这些关系来建立继承,属于静态关系,在编译的时候确定下来;对象模式处理对象之间的关系,运行时发生
变化,属于动态关系。
行为型模式供有11种
一.职责链模式
定义:通过给多个对象处理请求的机会,减少请求的发送者和接受者之间的耦合。将接受对象链接
起来,在链中传递请求,直到有一个对象处理这个请求。
不保证每个请求都被接受,因为一个请求没有一个明确的接受者的时候,不能保证一定会被处理,就会
被一直传递下去
职责链包含如下角色
- Handler:抽象处理者
- ConceteHandle:具体处理者
- Client:客户类
抽象处理者角色:定义一个处理请求的抽象类,包含了几个方法
具体处理中角色:具体处理者实现了抽象处理者处理请求的方法,继承了可以指向下一个处理者的指针。
具体处理者接到请求后,可以选择将请求处理掉,或则请求传给下家。由于具体处理者持有对
下家的引用,因此,如果需要可以访问下家。
应用场景
很常见的一个场景:请假,假定假期三天以内,由项目经理直接审批;3-5天,由部门经理审批;
大于五天,则由总经理通过。
如果把上面的场景应用到责任链模式中去,项目经理,部门经理,总经理就是一个个具体的责任人,他们
可以对请求作出处理,但是他们只能在自己的权限范围内处理该请求。
抽象处理者
/**
* 抽象处理者
*/
public static abstract class Handler {
private Handler nextHandler;
// 当前领导能审批通过的最多天数
public int maxDay;
protected Handler(int maxDay) {
this.maxDay = maxDay;
}
//设置责任链中下一个处理请求的对象
public void setNextHandler(Handler handler) {
nextHandler = handler;
}
protected void handleRequest(int day) {
if (day <= maxDay) {
reply(day);
} else {
if (nextHandler != null) {
//审批权限不够,继续上报
nextHandler.handleRequest(day);
} else {
LogUtil.d("没有更高的领导审批了");
}
}
}
protected abstract void reply(int day);
}
具体处理者
项目经理
/**
* 项目经理
*/
class ProjectManager extends Handler {
public ProjectManager(int day) {
super(day);
}
@Override
protected void reply(int day) {
LogUtil.d(day + "天请假,项目经理直接审批通过");
}
}
部门经理
/**
* 部门经理
*/
class DepartmentManager extends Handler {
public DepartmentManager(int day) {
super(day);
}
@Override
protected void reply(int day) {
LogUtil.d(day + "天请假,部门经理审批通过");
}
}
总经理
/**
* 总经理
*/
class GeneralManager extends Handler {
public GeneralManager(int day) {
super(day);
}
@Override
protected void reply(int day) {
LogUtil.d(day + "天请假,总经理审批通过");
}
}
代码调用
Handler projectManager = new ProjectManager(3);
Handler departmentManager = new DepartmentManager(5);
Handler generalManager = new GeneralManager(15);
//创建职责链
projectManager.setNextHandler(departmentManager);
departmentManager.setNextHandler(generalManager);
//发起一次请求
projectManager.handleRequest(10);
在Java中的应用包括filter,请求被一个filter消费后(拦截)就不在发给下一个filter了。
二 观察者模式
观察者模式(Observer pattern),定义了对象间一种一对多的依赖关系,当被观察者状态发生变化,它的
观察者们会收到通知并自动更新。
应用
在Java语言的java.util库里面,提供了一个Obserable类(被观察者继承)以及一个Observer接口(观察者实现),构成了Java语言对观察者模式的
支持。
Observable 被观察者
被观察者,一个被观察者对象可以有多个观察者对象。
public class ConcreteObservable extends Observable {
private int data = 0;
public int getData() {
return data;
}
public void setData(int i) {
//具体逻辑按需
if (data != i) {
this.data = i;
setChanged();
}
}
private static ConcreteObservable observable;
public static synchronized ConcreteObservable getInstance() {
if (observable == null) {
observable = new ConcreteObservable();
}
return observable;
}
public ConcreteObservable post() {
//只有在setChange()被调用后,notifyObservers()才会去调用update(),否则什么都不干。
notifyObservers();
return observable;
}
public ConcreteObservable post(Object arg) {
setChanged();
//只有在setChange()被调用后,notifyObservers()才会去调用update(),否则什么都不干。
notifyObservers(arg);
return observable;
}
}
Observable 源码:
public class Observable {
private boolean changed = false;
private final ArrayList<Observer> observers;
/** Construct an Observable with zero Observers. */
public Observable() {
observers = new ArrayList<>();
}
/**
* 将一个观察者添加到观察者集合
*/
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!observers.contains(o)) {
observers.add(o);
}
}
/**
* 删除观察者
*/
public synchronized void deleteObserver(Observer o) {
observers.remove(o);
}
/**
* 如果该对象发生了变化,由所指示的 hasChanged方法,则通知其所有观察者,并调用clearChanged方法来指示该对象不再改变
*/
public void notifyObservers() {
notifyObservers(null);
}
/**
* 如果该对象发生了变化,由所指示的 hasChanged方法,则通知其所有观察者,并调用clearChanged方法来指示该对象不再改变
*/
public void notifyObservers(Object arg) {
Observer[] arrLocal;
synchronized (this) {
if (!hasChanged())
return;
arrLocal = observers.toArray(new Observer[observers.size()]);
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
arrLocal[i].update(this, arg);
}
/**
* 清除观察者列表,不再有任何观察者
*/
public synchronized void deleteObservers() {
observers.clear();
}
/**
* 观测对象为已改变
*/
protected synchronized void setChanged() {
changed = true;
}
/**
* 指示对象不再改变,或者说,它已通知其所有最近改变其观察员
*/
protected synchronized void clearChanged() {
changed = false;
}
/**
* 测试此对象已经改变
*/
public synchronized boolean hasChanged() {
return changed;
}
/**
* 返回观察者人数
*/
public synchronized int countObservers() {
return observers.size();
}
}
Observer 观察者
Observer 是个接口,在需要观察的地方实现:
public class ObserverActivity extends AppCompatActivity implements Observer {
ConcreteObservable observable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_observer);
observable = ConcreteObservable.getInstance();
observable.addObserver(this);
observable.setData(1);
observable.post();
observable.setData(1);
observable.post();
observable.post(2);
}
@Override
public void update(Observable o, Object arg) {
ConcreteObservable concreteObservable = (ConcreteObservable) o;
Log.d("wxl", "ObserverActivity=" + concreteObservable.getData() + ",arg=" + arg);
}
@Override
protected void onDestroy() {
super.onDestroy();
//删除观察者
observable.deleteObserver(this);
}
}
Observer源码:
public interface Observer {
/**
* 当被观察者对象的状态发生变化时,被观察者对象的notifyObservers()方法就会* 调用这一方法。
*/
void update(Observable o, Object arg);
}
最后打印:
ObserverActivity=1,arg=null
ObserverActivity=1,arg=2
三 封装调用-命令模式
命令模式可将“动作的请求者”从“动作的执行者”对象中解耦。
理解命令模式
让我们回到餐厅,研究顾客,女招待,订单以及快餐厨师之间的交互。通过这些的互动,你将
体会到命令模式所涉及的对象,也知道他们如何解耦。
将以上流程代入到编程的对象中进一步思考对象与方法之间的关系:
分析餐厅对应的角色与职责
1、顾客:发出请求的对象
2、订单:封装了准备餐点的请求
3、女招待:接受到订单,然后调用订单orderUp方法,将订单提交到柜台,不需要知道订单细节
4、厨师:收到订单后,按订单实现对应餐点的所有方法制作餐点。
命令模式类图:
- Command:定义命令的接口,声明执行的方法。
- ConcreteCommand: 具体的命令, 实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- Receiver:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
- Client: 创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。
命令模式定义:将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其它对象。命令模式也支持可撤销的操作。
实现命令接口:
public abstract class Command{
public abstract void Execute();
}
OrderCommand:具体的命令,继承自Command抽象类
public class OrderCommand implements Command{
//持有接受者对象
SeniorChef receiver;
Order order;
public OrderCommand(SeniorChef receiver, Order order){
this.receiver = receiver;
this.order = order;
}
public override void Execute(){
Console.WriteLine("{0}桌的订单:", order.DiningTable);
foreach (string item in order.FoodDic.Keys){
//通常会转调接收者对象的相应方法,让接收者来真正执行功能
receiver.MakeFood(order.FoodDic[item],item);
}
Thread.Sleep(2000);//停顿一下 模拟做饭的过程
Console.WriteLine("{0}桌的饭弄好了", order.DiningTable);
}
}
Invoker调用者,seniorChef:接收者
public class Waiter{
ArrayList commands = null;//可以持有很多的命令对象
public Waiter(){
commands = new ArrayList();
}
public void SetCommand(Command cmd){
commands.Add(cmd);
}
//提交订单 喊 订单来了,厨师开始执行
public void OrderUp(){
Console.WriteLine("美女服务员:叮咚,大厨,新订单来了.......");
Console.WriteLine("资深厨师:收到");
for (int i = 0; i < commands.Count; i++){
Command cmd = commands[i] as Command;
if (cmd != null){
cmd.Execute();
}
}
}
}
命令模式优点:
1.降低对象之间的耦合度。
2.新的命令可以很容易地加入到系统中。
3.可以比较容易地设计一个组合命令。
4.调用同一方法实现不同的功能
缺点:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
适用环境:
1.系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
2.系统需要在不同的时间指定请求、将请求排队和执行请求。
3.系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
4.系统需要将一组操作组合在一起,即支持宏命令。
Java中Runable是一个典型的命令模式,Runable相当于命令的角色,Thread相当于是调研组,
start方法就是其执行方法。
生活中的命令模式
餐厅点菜场景,客人订单发起者、厨师订单执行者,客人不会直接对厨师直接下单,而是通过服务员这个订单接收者,客人和厨师解耦。如果不这样做会怎样?厨师先去大厅,接受用户的订单,然后厨师拿着订单去做菜,做完菜再把菜端给客人,这个时间段其他用户需要等待,如果采用命令行模式,服务员负责给用户点单,厨师只要负责做好一件事就是做菜,分工明确,客人不需要等待,有点像流水线,提高了用户体验和工作效率。
命令模式,Client生成命令,传给调用对象对象Invoker,调用对象Invoker寻找可以处理该命令的
Receiver,接受者执行命令。
将请求者和行为实现者解耦。
四 迭代器模式
在容器里存放了大量的同类型对象,我们如果想逐个访问容器中的对象,就必须要知道容器的结构。比如,ArrayList 和 LinkedList 的遍历方法一定是不一样的,如果再加上HashMap, TreeMap,以及我们现在正在研究的BinarySearchTree,对于容器的使用者而言,肯定是一个巨大的负担。作为容器的使用者,我肯定希望所有的容器能提供一个统一的接口,这个接口可以遍历容器中的所有内容,又可以把容器的细节屏蔽掉。
幸运的是,确实存在这样的一个统一的接口,这个接口就是 java.util.Iterator。先看类图,下面的这幅图所表示的意思就是,在一个容器类上实现createIterator方法,这个方法可以创建一个迭代器,使用这个迭代器就可以遍历容器中的所有元素了。
Iterator上定义了各种方法,用于遍历。图中只列出了一小部分。我们可以看一下JDK中是怎么定义的:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
有两个default方法。default 这个关键字,我们以前没接触过。这是从1.8引入的,为了和以前的接口兼容,这个我们先不去管它,还有 forEachRemaing是一个高阶函数,它可以接受一个lambda表达式,这个我们会在后面的 Java 函数式编程中慢慢介绍,也先不去管它。所以 Iterator中也就只有两个方法,一个是hasNext,告诉使用者,还有没有下一个元素,如果为false,说明遍历已经完成了。还有一个是next,目的是取下一个元素。
我们先来体验一下。
List中的迭代器
我们先使用这个迭代器遍历一下List,例如:
public class Main {
public static void main(String args[]) {
LinkedList<Integer> ll = new LinkedList<>();
for (int i = 0; i < 10; i++)
ll.addLast(i);
Iterator<Integer> ii = ll.iterator();
while (ii.hasNext())
System.out.println(ii.next());
}
}
请大家再试一下ArrayList,体会一下 Iterator 的用法。可以看到,我们根本就不需要关心后面具体的数据结构,只需要使用 Iterator,不管后面是数组还是链表,都可以很方便地遍历整个容器。
大家想一下,如果要让自己来实现这样的一个Iterator,你会怎么写?这里给出ArrayList的源代码,可以参考一下:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
我把 remove 等方法删掉了,这个方法是在 ListIterator定义的,我们先不去管它。这里只留下了 hasNext 和 next 方法。
可以看到,ArrayList 的 Iterator 就是在数组中逐个前进,逻辑非常简单。
五:中介者模式
一、认识中介者模式
1、概念
我们先从例子入手,再着手介绍其概念。对于中介者模式,你脑海中首先映入的肯定就是中介,我们的生活中到处充满着中介,比如说婚姻介绍所,房产中介,甚至于联合国都是中介。他们的作用都是加强处理人与人之间或者是国与国之间的关系。如果没有这种中介会怎么样呢?就以联合国为例,这世界上有200多个国家,每个国家之间的关系是超级复杂的。这些国家之间沟通交流的模式是这个样的:
我们可以看到,这个图是真的麻烦,看着就恶心。国与国之间的交流竟然这么麻烦。好在二战之后联合国出现了,有效地解决了他们之间的沟通障碍,有问题就到联合国去争论去吵闹。
现在看着应该会舒服很多了。有了联合国,使得国与国之间的关系变得不那么复杂,现在我们把场景视线类比到我们的代码开发中,假设对象之间也存在着复杂的关系,那么有没有类似于联合国这样的组织帮我们去管理呢?肯定是有的,就是今天所要说的中介者模式。而且通过上面这张图也可以看到,任何一个国家(对象)出现变动,只能影响到自己,对别人没有任何影响。OK,到这一步我们就再来看一下中介者模式的真正含义:
概念:用一个中介者对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互
2、类图
到了这一步,我们通过例子来看一下类图;
从上面这张图出现了几个角色我们分别来分析一下:
(1)IMediator:抽象中介者,它定义了对象之间交流的方法。就好比国与国之间的沟通方式。
(2)ConcreteMediator:具体中介者,联合国的正式组成运营。
(3)IColleague:抽象同事类,就好比是一个抽象的国家模型。里面定义了一些功能。
(4) ConcreteColleague: 具体同事类。就好比是一个个具体的国家,英国、美国。
现在我们就使用代码来实现一下吧。
二、代码实现
这里实现的是只有两个国家之间的交流,也就是中国和美国。
(1)第一步:定义抽象中介者,也就是联合国模型
public interface IMediator {
//国与国之间交流的方法
public void communicate(String msg,IColleague country);
}
这里出现了IColleague,代表的是国家模型。国与国之间的交流,联合国都是通过communicate方法实现的。
(2)第二步:定义抽象同事类,也就是国家模型
public abstract class IColleague {
//国家名称
protected String countryName;
//联合国
protected Mediator mediator;
IColleague(String countryName,Mediator mediator){
this.countryName = countryName;
this.mediator = mediator;
}
public abstract void sendMsg(String msg);
public abstract void receiveMsg(String msg);
}
(3)第三步:具体同事类,这里有中国和美国
首先是中国:
public class ChinaColleague extends IColleague {
ChinaColleague(String countryName, Mediator mediator) {
super(countryName, mediator);
}
//中国有问题,就告诉联合国,不会直接通知各国
@Override
public void sendMsg(String msg) {
mediator.communicate(msg,this);
}
//中国接受联合国的信息
@Override
public void receiveMsg(String msg) {
System.out.println("中国获得信息:" + msg);
}
}
然后是美国:
public class USAColleague extends IColleague {
USAColleague(String countryName, Mediator mediator) {
super(countryName, mediator);
}
//美国有问题,就告诉联合国,不会直接通知各国
@Override
public void sendMsg(String msg) {
mediator.communicate(msg, this);
}
//美国接受联合国的信息
@Override
public void receiveMsg(String msg) {
System.out.println("美国获得信息:" + msg);
}
}
(4)第四步:具体中介者,真实的联合国发挥中介角色
public class Mediator implements IMediator {
// 联合国要有所有国家的信息
private ChinaColleague china;
private USAColleague usa;
//每个国家的getter和setter方法
@Override
public void communicate(String msg, IColleague country) {
//msg如果是中国发送的话,就让美国接受消息
if (country == china) {
usa.receiveMsg(msg);
} else {
//msg如果是美国发送的话,就让中国接受消息
china.receiveMsg(msg);
}
}
}
当然,这个类其实是中介者模式的核心,可以有很多种写法,比如说这里只有俩个国家,就一个一个列出来就好了,如果国家比较多的话,可以使用List进行保存。通知其他国家信息的时候,也可以换一种方式。
(5)第五步:客户端演示
public class Client {
public static void main(String[] args) {
//创建联合国、中国、美国
Mediator mediator = new Mediator();
ChinaColleague china = new ChinaColleague("中国", mediator);
USAColleague usa = new USAColleague("美国", mediator);
//让中国和美国注册到联合国
mediator.setChina(china);
mediator.setUsa(usa);
china.sendMsg("台湾是属于中国的一部分");
usa.sendMsg("美国坚持对台军售");
}
}
//输出
美国获得信息:台湾是属于中国的一部分
中国获得信息:美国坚持对台军售
自己动手敲一遍,可以好好的体会一下。
三、总结分析
优点的话很明显了。都是为了降低代码的耦合度,减少对象之间的关联性,让每一个对象都能够独立。但是这种模式虽好,可不要滥用呀。从文章一开始你也发现了,讲解的例子都是那种网状关系的对象,转换为星型关系。过度使用,会造成更加复杂的结果。因此,且行且珍惜。
6 备忘录模式
备忘录是自己写的,所以不会破坏封装。
模式定义
备忘录模式(Memento Pattern)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式,备忘录模式属于行为型模式。
模式解决的问题
备忘录模式让用户回到之前的某个状态,就像“后悔药”一样,穿越时空回到过去的某个状态。
模式角色
发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
代码实现
/**
* @author zengjianlu
* 备忘录类
*/
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
------------------------------------------------------
/**
* @author zengjianlu
* 发起人
*/
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Memento saveStateToMemento(){
return new Memento(state);
}
public void getStateFromMemento(Memento memento){
state = memento.getState();
}
}
------------------------------------------------------
/**
* 备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象
* 备忘录管理者
* @author zengjianlu
*/
public class Storage {
private List<Memento> mementos = new ArrayList<Memento>();
public void add(Memento state){
mementos.add(state);
}
public Memento get(int index){
return mementos.get(index);
}
}
------------------------------------------------------
/**
* @author zengjianlu
* 测试类
*/
public class Test {
public static void main(String[] args) {
Originator originator = new Originator();
Storage storage = new Storage();
originator.setState("状态:第1关");
storage.add(originator.saveStateToMemento());
originator.setState("状态:第2关");
storage.add(originator.saveStateToMemento());
originator.setState("状态:第3关");
System.out.println("当前" + originator.getState());
System.out.println("我想回到第2关");
originator.getStateFromMemento(storage.get(1));
System.out.println("当前" + originator.getState());
}
}
运行结果:
优缺点分析
优点:
给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:
消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。当状态很多的情况,可以将状态存储到数据库中。
生活中模式情景再现
游戏存档,小时候小霸王游戏,魂斗罗、超级玛丽、冒险岛、西游记、忍者神龟等游戏都支持存档功能,方便用户快速回到上一存档点。
编辑器中的ctri + z,idea、记事本、word、excel、ppt、visio、project、git、svn等都提供撤回操作,给用户提供“后悔药”。
浏览器后退功能,火狐、谷歌、IE、360、猎豹、safari、搜狗等浏览器都支持备忘录模式。
数据库的事务管理,mysql的事务主要通过undo、redo日志来实现事务的原子性,要么全部成功提交,要么全部失败回滚。
7策略模式
策略模式
策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。
首先有一个所有算法的抽象类
上下文有一个属性指向一个具体的算法类
客户端调用上下文 上下文调用算法类的同名算法
算法可以扩展,传给上下文就好了。
深入理解策略模式:
策略模式的作用:就是把具体的算法实现从业务逻辑中剥离出来,成为一系列独立算法类,使得它们可以相互替换。
策略模式的着重点:不是如何来实现算法,而是如何组织和调用这些算法,从而让我们的程序结构更加的灵活、可扩展。
我们前面的第一个报价管理的示例,发现每个策略算法实现对应的都是在QuoteManager 中quote方法中的if else语句里面,我们知道if else if语句里面的代码在执行的可能性方面可以说是平等的,你要么执行if,要么执行else,要么执行else if。
策略模式就是把各个平等的具体实现进行抽象、封装成为独立的算法类,然后通过上下文和具体的算法类来进行交互。各个策略算法都是平等的,地位是一样的,正是由于各个算法的平等性,所以它们才是可以相互替换的。虽然我们可以动态的切换各个策略,但是同一时刻只能使用一个策略。
在这个点上,我们举个历史上有名的故事作为示例:
三国刘备取西川时,谋士庞统给的上、中、下三个计策:
上策:挑选精兵,昼夜兼行直接偷袭成都,可以一举而定,此为上计计也。
中策:杨怀、高沛是蜀中名将,手下有精锐部队,而且据守关头,我们可以装作要回荆州,引他们轻骑来见,可就此将其擒杀,而后进兵成都,此为中计。
下策:退还白帝,连引荆州,慢慢进图益州,此为下计。
这三个计策都是取西川的计策,也就是攻取西川这个问题的具体的策略算法,刘备可以采用上策,可以采用中策,当然也可以采用下策,由此可见策略模式的各种具体的策略算法都是平等的,可以相互替换。
那谁来选择具体采用哪种计策(算法)?
在这个故事中当然是刘备选择了,也就是外部的客户端选择使用某个具体的算法,然后把该算法(计策)设置到上下文当中;
我们上面的策略接口采用的是接口的形式来定义的,其实这个策略接口,是广义上的接口,不是语言层面的interface,也可以是一个抽象类,如果多个算法具有公有的数据,则可以将策略接口设计为一个抽象类,把公共的东西放到抽象类里面去。
策略模式在JDK中的应用:
在多线程编程中,我们经常使用线程池来管理线程,以缓解线程频繁的创建和销毁带来的资源的浪费,
在创建线程池的时候,经常使用一个工厂类来创建线程池Executors,实际上Executors的内部使用的是类ThreadPoolExecutor.它有一个最终的构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
2 int maximumPoolSize,
3 long keepAliveTime,
4 TimeUnit unit,
5 BlockingQueue<Runnable> workQueue,
6 ThreadFactory threadFactory,
7 RejectedExecutionHandler handler) {
8 if (corePoolSize < 0 ||
9 maximumPoolSize <= 0 ||
10 maximumPoolSize < corePoolSize ||
11 keepAliveTime < 0)
12 throw new IllegalArgumentException();
13 if (workQueue == null || threadFactory == null || handler == null)
14 throw new NullPointerException();
15 this.corePoolSize = corePoolSize;
16 this.maximumPoolSize = maximumPoolSize;
17 this.workQueue = workQueue;
18 this.keepAliveTime = unit.toNanos(keepAliveTime);
19 this.threadFactory = threadFactory;
20 this.handler = handler;
21 }
corePoolSize:线程池中的核心线程数量,即使这些线程没有任务干,也不会将其销毁。
maximumPoolSize:线程池中的最多能够创建的线程数量。
keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间。
unit:keepAliveTime的时间单位。
workQueue:在线程池中的线程还没有还得及执行任务之前,保存任务的队列(当线程池中的线程都有任务在执行的时候,仍然有任务不断的提交过来,这些任务保存在workQueue队列中)。
threadFactory:创建线程池中线程的工厂。
handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。
RejectedExecutionHandler 是一个策略接口,用在当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。
public interface RejectedExecutionHandler {
/**
*当ThreadPoolExecutor的execut方法调用时,并且ThreadPoolExecutor不能接受一个任务Task时,该方法就有可能被调用。
* 不能接受一个任务Task的原因:有可能是没有多余的线程来处理,有可能是workqueue队列中没有多余的位置来存放该任务,该方法有可能抛出一个未受检的异常RejectedExecutionException
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
该策略接口有四个实现类:
AbortPolicy:该策略是直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常。
DiscardPolicy:该策略也是将任务抛弃掉(对于提交的任务不管不问,什么也不做),不过并不抛出异常。
DiscardOldestPolicy:该策略是当执行器未关闭时,从任务队列workQueue中取出第一个任务,并抛弃这第一个任务,进而有空间存储刚刚提交的任务。使用该策略要特别小心,因为它会直接抛弃之前的任务。
CallerRunsPolicy:该策略并没有抛弃任何的任务,由于线程池中已经没有了多余的线程来分配该任务,该策略是在当前线程(调用者线程)中直接执行该任务。
类ThreadPoolExecutor中持有一个RejectedExecutionHandler接口的引用,以便在构造函数中可以由外部客户端自己制定具体的策略并注入。下面看一下其类图:
策略模式的优点:
1.策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
2.策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。
3.扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。
策略模式的缺点:
1.客户端必须了解所有的策略,清楚它们的不同:
如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。
2.增加了对象的数量:
由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。
3.只适合偏平的算法结构:
由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。
策略模式的本质:
分离算法,选择实现。
如果你仔细思考策略模式的结构和功能的话,就会发现:如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程,就能够享受到面向接口编程带来的好处,通过一个统一的策略接口来封装和分离各个具体的策略实现,无需关系具体的策略实现。
貌似没有上下文什么事,但是如果没有上下文的话,客户端就必须直接和具体的策略实现进行交互了,尤其是需要提供一些公共功能或者是存储一些状态的时候,会大大增加客户端使用的难度;引入上下文之后,这部分工作可以由上下文来完成,客户端只需要和上下文进行交互就可以了。这样可以让策略模式更具有整体性,客户端也更加的简单。
策略模式体现了开闭原则:策略模式把一系列的可变算法进行封装,从而定义了良好的程序结构,在出现新的算法的时候,可以很容易的将新的算法实现加入到已有的系统中,而已有的实现不需要修改。
策略模式体现了里氏替换原则:策略模式是一个扁平的结构,各个策略实现都是兄弟关系,实现了同一个接口或者继承了同一个抽象类。这样只要使用策略的客户端保持面向抽象编程,就可以动态的切换不同的策略实现以进行替换。
8 状态模式
好了,接下来,我们先来看看状态模式的定义吧。
定义:(源于Design Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
上述是百度百科中对状态模式的定义,定义很简单,只有一句话,请各位形象的去理解这句话,它说当状态改变时,这个对象的行为也会变,而看起来就像是这个类改变了一样。
这正是应验了我们那句话,有些人一旦发生过什么事以后,就像变了个人似的,这句话其实与状态模式有异曲同工之妙。
我们仔细体会一下定义当中的要点。
1、有一个对象,它是有状态的。
2、这个对象在状态不同的时候,行为不一样。
3、这些状态是可以切换的,而非毫无关系。
前两点比较好理解,第3点有时候容易给人比较迷惑的感觉,什么叫这些状态是可以切换的,而非毫无关系?
举个例子,比如一个人的状态,可以有很多,像生病和健康,这是两个状态,这是有关系并且可以转换的两个状态。再比如,睡觉、上班、休息,这也算是一组状态,这三个状态也是有关系的并且可以互相转换。
那如果把生病和休息这两个状态放在一起,就显得毫无意义了。所以这些状态应该是一组相关并且可互相切换的状态。
下面我们来看看状态模式的类图。
类图中包含三个角色。
Context:它就是那个含有状态的对象,它可以处理一些请求,这些请求最终产生的响应会与状态相关。
State:状态接口,它定义了每一个状态的行为集合,这些行为会在Context中得以使用。
ConcreteState:具体状态,实现相关行为的具体状态类。
如果针对刚才对于人的状态的例子来分析,那么人(Person)就是Context,状态接口依然是状态接口,而具体的状态类,则可以是睡觉,上班,休息,这一系列状态。
不过LZ也看过不少状态模式的文章和帖子,包括《大话设计模式》当中,都举的是有关人的状态的例子,所以这里给大家换个口味,我们换一个例子。
我们来试着写一个DOTA的例子,最近貌似跟DOTA干上了,不为其他,就因为DOTA伴随了LZ四年的大学时光。
玩过的朋友都知道,DOTA里的英雄有很多状态,比如正常,眩晕,加速,减速等等。相信就算没有玩过DOTA的朋友们,在其它游戏里也能见到类似的情况。那么假设我们的DOTA没有使用状态模式,则我们的英雄类会非常复杂和难以维护,我们来看下,原始版的英雄类是怎样的。
package com.state;
//英雄类
public class Hero {
public static final int COMMON = 1;//正常状态
public static final int SPEED_UP = 2;//加速状态
public static final int SPEED_DOWN = 3;//减速状态
public static final int SWIM = 4;//眩晕状态
private int state = COMMON;//默认是正常状态
private Thread runThread;//跑动线程
//设置状态
public void setState(int state) {
this.state = state;
}
//停止跑动
public void stopRun() {
if (isRunning()) runThread.interrupt();
System.out.println("--------------停止跑动---------------");
}
//开始跑动
public void startRun() {
if (isRunning()) {
return;
}
final Hero hero = this;
runThread = new Thread(new Runnable() {
public void run() {
while (!runThread.isInterrupted()) {
try {
hero.run();
} catch (InterruptedException e) {
break;
}
}
}
});
System.out.println("--------------开始跑动---------------");
runThread.start();
}
private boolean isRunning(){
return runThread != null && !runThread.isInterrupted();
}
//英雄类开始奔跑
private void run() throws InterruptedException{
if (state == SPEED_UP) {
System.out.println("--------------加速跑动---------------");
Thread.sleep(4000);//假设加速持续4秒
state = COMMON;
System.out.println("------加速状态结束,变为正常状态------");
}else if (state == SPEED_DOWN) {
System.out.println("--------------减速跑动---------------");
Thread.sleep(4000);//假设减速持续4秒
state = COMMON;
System.out.println("------减速状态结束,变为正常状态------");
}else if (state == SWIM) {
System.out.println("--------------不能跑动---------------");
Thread.sleep(2000);//假设眩晕持续2秒
state = COMMON;
System.out.println("------眩晕状态结束,变为正常状态------");
}else {
//正常跑动则不打印内容,否则会刷屏
}
}
}
下面我们写一个客户端类,去试图让英雄在各种状态下奔跑一下。
package com.state;
public class Main {
public static void main(String[] args) throws InterruptedException {
Hero hero = new Hero();
hero.startRun();
hero.setState(Hero.SPEED_UP);
Thread.sleep(5000);
hero.setState(Hero.SPEED_DOWN);
Thread.sleep(5000);
hero.setState(Hero.SWIM);
Thread.sleep(5000);
hero.stopRun();
}
}
状态模式解决的问题:状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
不用说,状态模式是可以解决我们上面的if else结构的,我们采用状态模式,利用多态的特性可以消除掉if else结构。这样所带来的好处就是可以大大的增加程序的可维护性与扩展性。
下面我们就使用状态模式对上面的例子进行改善,首先第一步,就是我们需要定义一个状态接口,这个接口就只有一个方法,就是run。
package com.state;
public interface RunState {
void run(Hero hero);
}
与状态模式类图不同的是,我们加入了一个参数Hero(Context),这样做的目的是为了具体的状态类当达到某一个条件的时候可以切换上下文的状态。下面列出四个具体的状态类,其实就是把if else拆掉放到这几个类的run方法中。
package com.state;
public class CommonState implements RunState{
public void run(Hero hero) {
//正常跑动则不打印内容,否则会刷屏
}
}
package com.state;
public class SpeedUpState implements RunState{
public void run(Hero hero) {
System.out.println("--------------加速跑动---------------");
try {
Thread.sleep(4000);//假设加速持续4秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------加速状态结束,变为正常状态------");
}
}
package com.state;
public class SpeedDownState implements RunState{
public void run(Hero hero) {
System.out.println("--------------减速跑动---------------");
try {
Thread.sleep(4000);//假设减速持续4秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------减速状态结束,变为正常状态------");
}
}
package com.state;
public class SwimState implements RunState{
public void run(Hero hero) {
System.out.println("--------------不能跑动---------------");
try {
Thread.sleep(2000);//假设眩晕持续2秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------眩晕状态结束,变为正常状态------");
}
}
这下我们的英雄类也要相应的改动一下,最主要的改动就是那些if else可以删掉了,如下。
package com.state;
//英雄类
public class Hero {
public static final RunState COMMON = new CommonState();//正常状态
public static final RunState SPEED_UP = new SpeedUpState();//加速状态
public static final RunState SPEED_DOWN = new SpeedDownState();//减速状态
public static final RunState SWIM = new SwimState();//眩晕状态
private RunState state = COMMON;//默认是正常状态
private Thread runThread;//跑动线程
//设置状态
public void setState(RunState state) {
this.state = state;
}
//停止跑动
public void stopRun() {
if (isRunning()) runThread.interrupt();
System.out.println("--------------停止跑动---------------");
}
//开始跑动
public void startRun() {
if (isRunning()) {
return;
}
final Hero hero = this;
runThread = new Thread(new Runnable() {
public void run() {
while (!runThread.isInterrupted()) {
state.run(hero);
}
}
});
System.out.println("--------------开始跑动---------------");
runThread.start();
}
private boolean isRunning(){
return runThread != null && !runThread.isInterrupted();
}
}
可以看到,现在我们的英雄类优雅了许多,我们使用刚才同样的客户端运行即可得到同样的结果。
对比我们的原始例子,现在我们使用状态模式之后,有几个明显的优点:
一、我们去掉了if else结构,使得代码的可维护性更强,不易出错,这个优点挺明显,如果试图让你更改跑动的方法,是刚才的一堆if else好改,还是分成了若干个具体的状态类好改呢?答案是显而易见的。
二、使用多态代替了条件判断,这样我们代码的扩展性更强,比如要增加一些状态,假设有加速20%,加速10%,减速10%等等等(这并不是虚构,DOTA当中是真实存在这些状态的),会非常的容易。
三、状态是可以被共享的,这个在上面的例子当中有体现,看下Hero类当中的四个static final变量就知道了,因为状态类一般是没有自己的内部状态的,所有它只是一个具有行为的对象,因此是可以被共享的。
四、状态的转换更加简单安全,简单体现在状态的分割,因为我们把一堆if else分割成了若干个代码段分别放在几个具体的状态类当中,所以转换起来当然更简单,而且每次转换的时候我们只需要关注一个固定的状态到其他状态的转换。安全体现在类型安全,我们设置上下文的状态时,必须是状态接口的实现类,而不是原本的一个整数,这可以杜绝魔数以及不正确的状态码。
状态模式适用于某一个对象的行为取决于该对象的状态,并且该对象的状态会在运行时转换,又或者有很多的if else判断,而这些判断只是因为状态不同而不断的切换行为。
上面的适用场景是很多状态模式的介绍中都提到的,下面我们就来看下刚才DOTA中,英雄例子的类图。
可以看到,这个类图与状态模式的标准类图是几乎一模一样的,只是多了一条状态接口到上下文的依赖线,而这个是根据实际需要添加的,而且一般情况下都是需要的。
状态模式也有它的缺点,不过它的缺点和大多数模式相似,有两点。
1、会增加的类的数量。
2、使系统的复杂性增加。
尽管状态模式有着这样的缺点,但是往往我们牺牲复杂性去换取的高可维护性和扩展性是相当值得的,除非增加了复杂性以后,对于后者的提升会乎其微。
状态模式在项目当中也算是较经常会碰到的一个设计模式,但是通常情况下,我们还是在看到if else的情况下,对项目进行重构时使用,又或者你十分确定要做的项目会朝着状态模式发展,一般情况下,还是不建议在项目的初期使用。
9.解释器模式
10 模板方法模式讲解(包含与类加载器不得不说的故事)
通常情况下,模板方法模式用于定义构建某个对象的步骤与顺序,或者定义一个算法的骨架。
我们刚才的示例明显就是构建一个String对象的过程,在这里要声明一点,对于模板方法模式,父类提供的构建步骤和顺序或者算法骨架,通常是不希望甚至是不允许子类去覆盖的,所以在某些场景中,可以直接将父类中提供骨架的方法声明为final类型。
模板方法模式还有一种使用的方式,为了给子类足够的自由度,可以提供一些方法供子类覆盖,去实现一些骨架中不是必须但却可以有自定义实现的步骤。
比如上述的例子当中,我们应该都知道,HTML页面中有一些标签是可有可无的。比如meta标签,link标签,script标签等。那么我们可以将刚才的例子细化一下,去看一下上面说的供子类覆盖的方法是什么。我们将刚才的抽象父类细化成如下形式。
所以为了不强制子类实现不必要的抽象方法,但又不剥夺子类自由选择的权利,我们在父类提供一个默认的空实现,来让子类自由选择是否要覆盖掉这些方法。
说到模板方法模式,我们JDK当中有一个类与它还有一个不得不说的故事,那就是类加载器。
JDK类加载器可以大致分为三类,分别是启动类加载器,扩展类加载器,以及应用程序加载器。
这三者加载类的路径分别为如下:
启动类加载器:JAVA_HOME/lib目录下,以及被-Xbootcalsspath参数设定的路径,不过启动类加载器加载的类是有限制的,如果JVM不认识的话,你放在这些目录下也没用。
扩展类加载器:JAVA_HOME/lib/ext目录下,以及被java.ext.dirs系统变量指定的路径。
应用程序类加载器:用户自己的类路径(classpath),这个类加载器就是我们经常使用的系统类加载器,并且JDK中的抽象类ClassLoader的默认父类加载器就是它。
在这里为什么说类加载器和模板方法模式有关呢,是因为ClassLoader类就使用了模板模式,去保证类加载过程中的唯一性。LZ先给各位看下这个类当中的模板模式的应用。
11访问者模式
访问者模式的几个特点:
1、访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。
2、访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。
3、访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。