理解设计模式之组合模式、迭代器模式、访问者模式

迭代器模式

提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。

如果要对所有集合提供统一的for循环操作,首先要抽象出Iterator接口:

1
2
3
4
public interface Iterator<E> {
boolean hasNext();
E next();
}

集合类实现迭代接口,有两种方式:继承和组合。那是继承Iterator好还是组合一个Iterator的实现类好呢?这里很难分清isA和hasA的关系,也就是说都可以。

但是考虑到Iterator的实现可以是不同的,组合不同的Iterator比继承Iterator产生不同的子类从分离变化的角度考虑更好(当然,这里我觉得完全可以采用继承),迭代器模式采用的是组合方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public abstract class Aggregate {
Iterator createIterator() {
return null;
}
}
public class ConcreteAggregate extends Aggregate {
@Override
Iterator createIterator() {
// 创建一个对外的具体迭代器
return new ConcreteIterator(this);
}
}
public class ConcreteIterator implements Iterator {
// 注入集合,让迭代器提供数据支撑
private ConcreteAggregate concreteAggregate;
public ConcreteIterator(ConcreteAggregate concreteAggregate) {
this.concreteAggregate = concreteAggregate;
}
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
return null;
}
}

迭代器的实现模式几乎是通用的,UML类图如下:
迭代器模式UML类图

随着语言的发展,集合迭代的需求逐渐朝着更真实更优雅的方向发展,从手动迭代器,到内置迭代器,到stream api,几乎越来越看不到迭代器的实现,越是如此越说明迭代器模式的应用。
迭代器模式应该给了我们很大启发,它只是集合操作的需求之一,集合操作的更多需求比如filter,map等是否都可以借鉴这种类似的思想呢?显然可以,这里不作展开。

组合模式

将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。

组合对象包括一系列单个对象,如何保证对组合对象和单个对象操作的一致性?
这个问题再简单不过,只要他们具备该操作的统一抽象行为即可,把这个行为抽象成接口Component:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IComponent {
void operation();
}
public class Leaf implements IComponent {
@Override
public void operation() {
}
}
public class Composite implements IComponent {
@Override
public void operation() {
}
}

但是Composite毕竟是组合对象,它包括一系列单个对象,当你操作组合对象的时候,应该要把这种操作传递给它的“子民”们,所以充实一下Composite对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Composite implements IComponent {
private List<IComponent> components;
@Override
public void operation() {
for (IComponent component : components) {
component.operation();
}
}
public void add(IComponent component) {
components.add(component);
}
public void remove(IComponent component) {
components.remove(component);
}
public IComponent getChild(int index) {
return components.get(index);
}
}

在传递operation给子对象的时候,得益于迭代器模式,不用关心List集合具体迭代的过程就能做到迭代(List换成其它集合,迭代的代码基本不变)。
现在当对IComponent对象发送operation不用的时候,不用考虑是组合对象还是单个对象,Leaf和Composite都能很好的执行了。
我特意定义IComponent为接口,让大家思考一下这个地方如果用抽象类会怎么样?
事实上,为了让IComponent能最大化重用Leaf和Composite(作为具备统一行为的对象它们应该有很多相同的实现)的实现,有必要使用抽象类代替IComponent并包含具体代码:

1
2
3
4
5
6
7
8
9
// 取代IComponent,为Leaf和Composite提供缺省操作
public abstract class Component {
public void operation() {
// default impl code
}
// ...
// more common code
}

在上述代码结构中,它们在add,remove,getChild这些行为表现出不一致性,我们是否有必要进一步改进?
如果把add,remove,getChild定义在抽象Component里,那么无疑表现出高度一致性,client就能透明的使用这些组件,此种模式称为透明式组合模式。透明式带来的隐患是,Leaf的add,remove,getChild这些操作是无意义的,这种无意义的后果是不可预测的,是不安全的。安全式组合模式不理财这些不一致性,当然这些行为的不一致性将导致Leaf和Component的强制转换,这是安全式的缺点。
两种方式各有优劣,根据软件具体情况做出取舍决定,UML类图如下:
组合器模式UML类图

访问者模式

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式并没有传说中的那么复杂,也不需要先理解什么双分派(当然如果你懂的话自然更好),就当成一个小技巧会更简单。

1
2
3
4
5
6
7
8
public class Element {
public void a() {}
public void b() {}
}
// Client调用a,b方法:
Element element = new Element();
element.a(); // 或者element.b()

如果想封装对a,b方法的调用,让客户不关心调用的方法,怎么实现?
增加一个间接层类Visitor,Element把自己交给Visitor,让Visitor来控制调用哪个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Visitor {
public void visitElement(Element element) {
// 把具体元素操作的调用延迟到这里
element.a();
}
}
public class Element {
public void a() {}
public void b() {}
public void accept(Visitor visitor) {
visitor.visitElement(this);
}
}
// Client不知道a,b方法,实际上调用了a方法
Element element = new Element();
element.accept(new Visitor());

这样做的好处是,解耦了元素和操作之间的调用(对于client而言)。这个特性适合统一处理具有不同数据结构的集合的各元素上的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 我们为各不同元素定义统一的访问方法
public interface IElement {
void accept(Visitor visitor);
}
public class ElementA {
public void a() {
System.out.println("AAAAAAAA");
}
@Override
public void accept(Visitor visitor) {
visitor.visitElementA(this);
}
}
public class ElementB {
public void b() {
System.out.println("BBBBBBBBB");
}
@Override
public void accept(Visitor visitor) {
visitor.visitElementB(this);
}
}
// Visitor封装具体各元素的操作的调用
public class Visitor {
public void visitElementA(ElementA elementA) {
elementA.a();
}
public void visitElementB(ElementB elementB) {
elementB.b();
}
}
// Client code
IElement elementA = new ElementA();
IElement elementB = new ElementB();
List<IElement> elements = new ArrayList<>();
elements.add(elementA);
elements.add(elementB);
// 使用迭代器模式统一处理这些不同的元素,实现调用不同方法,amazing!
for (IElement element : elements) {
element.accept(new Visitor());
}

从client的代码也可以看的出来,通过IElement硬是把这些不同数据结构拧在了一起(ObjectStructure),尽管有些别扭,但是这显然比下面的代码要好的多:

1
2
3
4
5
6
7
8
9
// 访问者模式就是为了优化这样的代码结构
for (IElement element : elements) {
element.accept(new Visitor());
if (element instanceof ElementA) {
((ElementA) element).a();
} else if (element instanceof ElementB) {
((ElementB) element).b();
}
}

访问者模式的UML类图:
访问者模式UML类图

小结

迭代器模式随着发展,已经成为各大语言的标准语法糖。
组合模式内部递归常常要用到迭代器模式。