假设有这样一个场景:使用一个画图软件画一辆汽车,细节忽略,我们暂且只画汽车的车身、轮胎、底盘三个部分,画完之后进行上色,上色之后再进行矫正。对于画图而言,车身、底盘和轮胎组成一辆汽车,这样的结构很稳定,不会说多出一个翅膀,而不稳当的部分在于汽车的颜色和细节部分,本文要介绍的访问者模式,致力于将“不稳定”的部分抽离出来,从而达到数据结构和数据操作两部分相分离的目的。
1.访问者模式
访问者模式(Visitor Pattern), 提供一个作用于某对象结构中的各元素的操作表示,它使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
访问者模式的主要意图是将数据结构与数据操作相分离,它解决数据结构和容易变更部分的耦合问题。UML类图是:
- Visitor:访问者接口,定义了数据“易变”部分的执行方法;
- ConcreteVisitor:访问者实现类,可扩展,不同的状态可以由不同实现类实现,符合开放-封闭原则;
- Element:稳定的数据结构接口;
- ConcreteElement:具体的实现类,这部分应该是不易变的,稳定的;
- ObjectStruture: 对象结构,用来分派请求。
2.代码实现
使用画一辆汽车的例子说明。过程是:
- 画好车身、底盘、轮子的轮廓并上色;
- 对车身、底盘、轮子做微调;
定义车体部件接口,定义接收访问者visitor的方法
interface CarElement { void accept(CarElementVisitor visitor); }
轮子、车身和底盘部件的实现类,注意不同部件有不同属性或状态比如轮子的数量和车身的类型(SUV,轿车)。
class Wheel implements CarElement { int count; public Wheel(int count) { this.count = count; } public int getCount() { return count; } @Override public void accept(CarElementVisitor visitor) { visitor.visit(this); } } class Chassis implements CarElement { @Override public void accept(CarElementVisitor visitor) { visitor.visit(this); } } class Body implements CarElement { private String type; public Body(String type) { this.type = type; } public String getType() { return type; } @Override public void accept(CarElementVisitor visitor) { visitor.visit(this); } }
汽车部件访问者接口,定义了三个visit方法分别访问不同部件。注意,这里定义的方法参数是实现类而非抽象,即抽象依赖于实现,违反了依赖倒置原则。这也是访问者模式的最大缺陷。
interface CarElementVisitor { /** 访问轮子 */ void visit(Wheel wheel); /** 访问底盘 */ void visit(Chassis chassis); /** 访问车身 */ void visit(Body body); }
两个访问者实现类,CarElementPrintVisitor访问每个部件并对其涂色;CarElementCheckVisitor访问每个部件并做微调。
class CarElementPrintVisitor implements CarElementVisitor{ @Override public void visit(Wheel wheel) { System.out.println("print " + ((Wheel)wheel).getCount() + " wheels"); } @Override public void visit(Chassis chassis) { System.out.println(chassis.getClass().getSimpleName() + " printed already!"); } @Override public void visit(Body body) { System.out.println(body.getType() + " Body print!"); } } class CarElementCheckVisitor implements CarElementVisitor { @Override public void visit(Wheel wheel) { System.out.println("wheels are checked!"); } @Override public void visit(Chassis chassis) { System.out.println("chassis are checked!"); } @Override public void visit(Body body) { System.out.println("body are checked!"); } }
汽车部件结构接口,elements列表储存各个部件,accept方法负责对visitor的访问任务进行分派,访问者模式用到了二次分派技术,即
- 第一次分派:调用结构对象accept方法,把访问者visitor分派到各个具体实体;
- 第二次分派:部件accept方法接受到visitor,调用visitor的visit方法实现对自身(传入this)的访问;
class CarStruture { List<CarElement> elements; public CarStruture() { elements = new ArrayList<>(); } public void addElement(CarElement element) { elements.add(element); } public void removeElement(CarElement element) { elements.remove(element); } public void accept(CarElementVisitor visitor) { for (CarElement element : elements) { element.accept(visitor); } } }
调用
public class VisitorDemo { public static void main(String[] args) { CarStruture cars = new CarStruture(); cars.addElement(new Wheel(4)); cars.addElement(new Chassis()); cars.addElement(new Body("SUV")); CarElementVisitor visitor = new CarElementPrintVisitor(); cars.accept(visitor); visitor = new CarElementCheckVisitor(); cars.accept(visitor); } }
print 4 wheels Chassis printed already! SUV Body print! wheels are checked! chassis are checked! body are checked!
3. 总结
访问者模式违反了依赖倒置原则,访问者依赖于实体的实现而非抽象,导致如果实体类的扩展或者修改会影响到访问者,显然是“不好看”的。所以访问者模式只适用于实体的数据结构非常稳定,不怎么变化的场景中。以下场合可以考虑访问者模式:
- 需要将数据操作从数据结构中分离出来;
- 新的数据结构在预期的未来会频繁扩展;
- 数据结构稳定不易改变;
- 需要独立地对元素的各个实现对象进行操作;