admin管理员组

文章数量:1579414

Contents

  • Problem
  • First attempt
  • Be careful...
    • What went wrong?
  • Now comes the visitors...
    • Why does this work?
  • Summary

Problem

You want to add an additional operation to a bunch of classes, AND the operation is implemented differently to different classes. The most direct but least recommended way is of course to go to the source files of the classes, and modify them one by one, this not only makes code unmaintainable but also pollutes the code as there may be too much unnecessary information being stuffed into one single class.

First attempt

The reasonable modification that works is of course, to add an additional class, which is dedicated to provide this particular operation to all the classes. For simplicity, we call this additional class provider. Simply use the ancient overloading technique - make the methods identical in name and type, but differ in parameter types. In this way, whatever application class only needs to call the method of the provider, with the object being passed in as the parameter.

Be careful…

Everything works fine if you know exactly the type of the classes that could use the operation. By that I mean, you put the exact type of the class as the parameter, NOT its parent type, NOT its interface type, JUST the type of the class itself. In this case, the compiler will know precisely which class it needs to interact with, so that it will have no ambiguity at all in determining which implementation(method) to call from the provider.

However, things start to go unexpected when the classes belong to a single family. That is, when the classes belonging to the same parent, you would be tempted use polymorphism to simplify your code. Let’s look at a demonstration, in which case there is an application, class App that tries to call the additional operation op() on some class that belongs to the same parent class Parent, and it has to go through the Provider class to carry out the operation.

public class Parent{}
// Parent has two different child classes
public class Child extends Parent{}
public class AnotherChild extends Parent{}

Here 's the Provider class that provides the operation:

public class Provider{
	public void op(Child c){
		System.out.println("This is Child class");
	}
	public void op(AnotherChild ac){
		System.out.println("This is AnotherChild class");
	}
	// Now since you want to use polymorphism,
	// This means you also need a method with its parameter
	// being the type of the parent class, 
	// otherwise it won't compile!
	public void op(Parent p){
		System.out.println("This is the Parent class");
	}
}

And here is the application that uses the operation. Suppose we want to create a Child object and call op() on that object. We would write:

public class App{
	// calls op() from Provider class, without the need to
	// know the exact type of the parameter - it should be
	// the responsibility of another class/method that 
	// is delegated to generate the objects!
	public void callOp(Parent p){
		Provider provider = new Provider();
		provider.op(p);
	}	
}

Finally we pass a concrete object to callOp(). In reality, everything should be done within App - a method that receives the object from another method, and calls callOp() on the object.

public static void main(String[] args){
    App app = new App();
    app.callOp(new Child());
}

The workflow looks roughly like this. Pretty straightforward, right?

Except it DOESN’T work! if we run the main method, the output is:

This is the Parent class

Whereas we were expecting it to be:

This is Child class

What went wrong?

The problem occurs at compile time. Recall that in Provider, we have methods for both the superclass Parent AND the subclass Child, so when you pass in a Child object, it is both of type Parent and Child, which creates an ambiguity that the compiler couldn’t resolve: Should we call op(Parent), or call op(Child)? In this case, the compiler doesn’t have a chain of preference that follows the inheritance relationship, and it would directly bind whatever comes to the Parent class.

So how do we tell the compiler to be able to recognize the child classes, while it is reluctant to do so?

Now comes the visitors…

Visitor design pattern resolves the exact problem. In this pattern, we start to rename our Provider class as Visitor, and rename the operation it provides op() as visit(). The functionalities remain the same.

First we modify our family of classes in the following way:

public class Parent{
	public void accept(Visitor v){
		v.visit(this)
	}
}
// Parent has two different child classes,
// each has to override the accept method to explicitly
// tell the compiler not to go to its superclass
public class Child extends Parent{
	@Override
	public void accept(Visitor v){
		v.visit(this)
	}
}
public class AnotherChild extends Parent{
	@Override
	public void accept(Visitor v){
		v.visit(this)
	}
}

This is only adding a 3-line method to each of the classes in the family, so it doesn’t affect the readability of the code very much.

Now our Visitor (formerly Provider) class doesn’t have to go through any change, other than being named differently:

public class Visitor{
    public void visit(Child c){
        System.out.println("This is Child class");
    }
    public void visit(AnotherChild ac){
        System.out.println("This is AnotherChild class");
    }
    public void visit(Parent p){
        System.out.println("This is the Parent class");
    }
}

Then, our Application needs to be changed in a way that explicitly informs the compiler about the type of the object:

public class App{
    // calls op() from Provider class
    public void callOp(Parent parent){
        Visitor visitor = new Visitor(); // ok
        parent.accept(visitor); // notice that instead of calling visitor.visit(parent),
        						// we have reverted the order to calling parent.accept(visitor)
    }
}

Finally some code to pass the object to test everything, here nothing is changed:

// exactly the same as the last example
public static void main(String[] args){
    App app = new App();
    app.callOp(new Child());
}

Now if we run the above code, the output is:

This is Child class

if we run app.callOp(new Parent());, the output is:

This is the Parent class

Why does this work?

This example works indicates that the compiler now knows precisely which method to call, even when there are both methods taking parent and child as parameter.

First take a look at the second line of callOp(). Instead of passing the object to Visitor and call directly from visitor, we now take the Visitor to the object, and we call visit()(formerly op()) from the class of the object. Therefore, the compiler would go to the object class, call the operation method from there, knowing precisely which class it’s currently in (the this keyword). This leaves no ambiguity, and everything works again!

Summary

The takeaway here is instead of calling the method directly from Visitor, call it from the object class and pass in this to make it clear to the compiler.

本文标签: VisitorDesignPattern