“ 这一篇文章把剩下剩下的行为型设计模式全部讲完,然后设计模式这一个模块就算结束了,在后面抽出时间再整理一下”
关于行为型设计模式我们已经聊过5种:策略模式、模版方法模式、观察者模式、责任链模式、状态模式。还剩下6种:迭代子模式、命令模式、备忘录模式、访问者模式、中介者模式、解释器模式。在下面我会把这6种设计模式大概的说明一下。
01
—
迭代子模式
迭代子模式又叫游标(Cursor)模式,迭代器我想很多人都知道,在集合中我们经常用到,而迭代子模式可以顺序的访问一个聚集中的元素而不必暴露聚集的内部对象。聚集我们可以理解为集合。这种模式设计就是用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
我们要如何使用这种设计模式呢?我们可以模仿一下Collection,通过它来看一下这种模式。
首先我们定义一个迭代器的接口,用于后面遍历集合:
代码语言:javascript复制interface Iterator {
//前移
public Object previous();
//后移
public Object next();
public boolean hasNext();
//取得第一个元素
public Object first();
}
这个接口定义4个方法,用于对集合进行基本操作。迭代器有了我们就要定义一个集合了:
代码语言:javascript复制interface Collection {
public Iterator iterator();
/*取得集合元素*/
public Object get(int i);
/*取得集合大小*/
public int size();
}
这个集合提供三个方法,一个是返回迭代器的方法,然后是获取集合元素和大小的方法。定义完这两个接口,我们是不是要写实现类了?我们先实现迭代器的接口,重写操作集合的方法:
代码语言:javascript复制class MyIterator implements Iterator {
//访问的集合
private Collection collection;
//初始化游标
private int pos = -1;
//构造器要传入具体的集合实现类
public MyIterator(Collection collection) {
this.collection = collection;
}
//前移,如果游标大于0则迁移
@Override
public Object previous() {
if (pos > 0) {
pos--;
return collection.get(pos);
} else {
System.out.println("不能前移了");
return new Exception();
}
}
//后移,如果游标小于集合元素数量,则向后移动游标
@Override
public Object next() {
if (pos < collection.size() - 1) {
pos ;
return collection.get(pos);
} else {
System.out.println("不能后移了");
return new Exception();
}
}
//判断是否还有下一个原色
@Override
public boolean hasNext() {
if (pos < collection.size() - 1) {
return true;
} else {
return false;
}
}
@Override
public Object first() {
pos = 0;
return collection.get(pos);
}
}
上面的注解我想说的很清楚了,这里就不在多说了,接下来我们实现一个具体的集合类:
代码语言:javascript复制class List implements Collection {
public String string[] = {"A", "B", "C", "D", "E"};
@Override
public Iterator iterator() {
return new MyIterator(this);
}
@Override
public Object get(int i) {
return string[i];
}
@Override
public int size() {
return string.length;
}
}
这个List集合类,直接定义了一个String类型的数组,也就是开始所说的聚集。关于返回这个迭代器,我们可以这样理解,我们在上面把List这个实现类放入到迭代器中,具体的实现是放在迭代器进行操纵的,而不是在集合的实现类中,这样一来就是实现了对底层具体实现的隐藏。
迭代子模式有以下优点和缺点以及使用场景:
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
02
—
命令模式
命令模式就比较容易理解了,是一种数据驱动的设计模式,请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。就好像我们的的客户跟我们的产品经理明确一个需求,那么产品经理就要根据需求的性质去找大相应的开发人员,实现这个需求。这个过程中,客户不考虑过程,只承担一个下发需求或者说命令的角色。
我们首先来写一个需求(命令)的接口:
代码语言:javascript复制interface Order {
void execute();
}
有了命令的接口,那就需要一个命令的执行人,也就是我们产品经理:
代码语言:javascript复制class ProManager {
//将需求放到集合中
List<Order> list = new ArrayList<>();
public void takeOrder(Order order) {
list.add(order);
}
//执行需求
public void placeOrders() {
for (Order order : list) {
order.execute();
}
list.clear();
}
}
既然需求和产品经理都有了,那我们是不是要定义具体的开发需求了,这里我们定义两个:
代码语言:javascript复制class H5 implements Order {
@Override
public void execute() {
System.out.println("前端的需");
}
}
class Ja implements Order {
@Override
public void execute() {
System.out.println("后端的需求");
}
}
剩下的就是执行了。
代码语言:javascript复制 public static void main(String[] args) {
//定义需求
Order h5 = new H5();
Order ja = new Ja();
ProManager proManager = new ProManager();
proManager.takeOrder(h5);
proManager.takeOrder(ja);
proManager.placeOrders();
}
需要知道的是,在产品经理的实现类中,我们拿到需求之后是可以进行命令的转移,也就是交给其他开发人员处理,而不是自己去处理,这就需要类中进行命令分析。
优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。
注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
03
—
备忘录模式
备忘录我想大家很清楚了,它的功能是能够帮助我们记忆,在Java的设计模式中,备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。这句话中说在适当的时候恢复对象,想不想回收站里面恢复我们误删的文件?也就是说,我们有一个文件A,我们建立一个备忘录用来存放A中的一些数据,同时我们使用回收站来存储这些备忘录,在合适的时候,我们可以进行恢复。
那我们就来定义一个备忘录:
代码语言:javascript复制 class Memento {
private String value;
public Memento(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
这个备忘录的作用就是存储文件数据了,那我们定义一个文件:
代码语言:javascript复制class Original {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Original(String value) {
this.value = value;
}
public Memento createMemento(){
return new Memento(value);
}
public void restoreMemento(Memento memento){
this.value = memento.getValue();
}
}
这个类和普通的实体区别在于它提供了一个创建备忘录的方法,通过这个方法我们可以把某一时刻该类的数据存储到备忘录中,在合适的时候通过restoreMemento方法去获取存储的数据。
我们再建立一个回收站来保存这些备忘录
代码语言:javascript复制class Storage {
private Memento memento;
public Storage(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
在这个回收中我们把备忘录放进来,这个类可以改进以下,可以定义一个Map用来存放备忘录,这样可以存放多种对象多种时刻的数据。
看一下测试:
代码语言:javascript复制 public static void main(String[] args) {
// 创建原始类
Original origi = new Original("egg");
// 创建备忘录
Storage storage = new Storage(origi.createMemento());
// 修改原始类的状态
System.out.println("初始化状态为:" origi.getValue());
origi.setValue("niu");
System.out.println("修改后的状态为:" origi.getValue());
// 回复原始类的状态
origi.restoreMemento(storage.getMemento());
System.out.println("恢复后的状态为:" origi.getValue());
}
优点: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。
注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式 备忘录模式。
04
—
访问者模式
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式就适用于这种数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变
我们通过例子来看访问者模式:
既然是访问者,那么最开始肯定是要有被访问者,不然访问者也就没有意义了。
代码语言:javascript复制interface Subject {
public void accept(Visitor visitor);
public String getSubject();
}
class MySubject implements Subject {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
@Override
public String getSubject() {
return "status=200";
}
}
这个被访问者首先要愿意接受访问者的访问,所以提供accept方法用来接受访问者对象,下面我们再实现访问者
代码语言:javascript复制interface Visitor {
public void visit(Subject sub);
}
class MyVisitor implements Visitor {
@Override
public void visit(Subject sub) {
System.out.println("visit the subject:" sub.getSubject());
}
}
测试:
代码语言:javascript复制 public static void main(String[] args) {
Visitor visitor = new MyVisitor();
Subject sub = new MySubject();
sub.accept(visitor);
}
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
05
—
中介者模式
终结者模式同访问者模式一样,都是降低类之间耦合性的,但是同访问者模式(访问者模式是在访问和被访问两种类)不同的是,它提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护
就好比两个人异地通话一样,有手机(中介者)就很方便,但是如果两人直接面对面就很麻烦了。
那我们来实现一个中介者:
代码语言:javascript复制interface Mediator<T> {
public void createMediator(T t, T t2);
public void workAll();
}
class MyMediator<T> implements Mediator<T> {
private T user1;
private T user2;
public T getUser1() {
return user1;
}
public T getUser2() {
return user2;
}
@Override
public void createMediator(T t, T t2) {
this.user1 = t;
this.user2 = t2;
}
@Override
public void workAll() {
System.out.println("通话");
}
}
中介者作用很明显了,把两个对象关联起来,然后在workAll方法中进行相应的操作,
那我们在来定义两个User类,
代码语言:javascript复制abstract class User {
}
class User1 extends User {
}
class User2 extends User {
}
这两个User我就不写任何东西了,因为既然是通话,交给中介就可以了,而不用两个User之间相互进行new。除此之外,如果User数量增加,那么更加麻烦,而有了中介就不一样,只需要范型就搞定:
代码语言:javascript复制 public static void main(String[] args) {
Mediator<User> mediator = new MyMediator();
mediator.createMediator(new User1(), new User2());
mediator.workAll();
}
优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
注意事项:不应当在职责混乱的时候使用。
06
—
解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。解释器模式用来做各种各样的解释器,如正则表达式等的解释器等等!
这里就不举例子,直接引用菜鸟教程的例子:
创建一个接口 Expression 和实现了 Expression 接口的实体类。定义作为上下文中主要解释器的 TerminalExpression 类。其他的类 OrExpression、AndExpression 用于创建组合式表达式。
InterpreterPatternDemo,我们的演示类使用 Expression 类创建规则和演示表达式的解析。
代码语言:javascript复制public class Interpreter行为型 {
//规则:Robert 和 John 是男性
public static Expression getMaleExpression(){
Expression robert = new TerminalExpression("Robert");
Expression john = new TerminalExpression("John");
return new OrExpression(robert, john);
}
//规则:Julie 是一个已婚的女性
public static Expression getMarriedWomanExpression(){
Expression julie = new TerminalExpression("Julie");
Expression married = new TerminalExpression("Married");
return new AndExpression(julie, married);
}
public static void main(String[] args) {
Expression isMale = getMaleExpression();
Expression isMarriedWoman = getMarriedWomanExpression();
System.out.println("John is male? " isMale.interpret("John"));
System.out.println("Julie is a married women? "
isMarriedWoman.interpret("Married Julie"));
}
}
interface Expression {
public boolean interpret(String context);
}
class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data){
this.data = data;
}
@Override
public boolean interpret(String context) {
if(context.contains(data)){
return true;
}
return false;
}
}
class OrExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
class AndExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public AndExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) && expr2.interpret(context);
}
}
优点: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。
缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。
使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 2、一些重复出现的问题可以用一种简单的语言来进行表达。 3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。