访问者模式的函数式实现

19-01-31 banq
                   

在面向对象的编程中,当需要向现有对象添加新操作时,通常使用访问者模式,但由于设计原因不可能修改对象本身并在实现中直接添加缺少的操作。为此,我们域中的每个对象都必须有一个接受访问者并将自己传递给该访问者的方法,然后必须实现如下所示的接口。

interface Element {
    <T> T accept(Visitor<T> visitor);
}

此时,我们可以定义一个简单的业务域,并展示不同目的的不同访问者如何访问它。对于此示例,我们的域模型将由简单的几何形状构成。

public static class Square implements Element {
    public final double side;
 
    public Square(double side) {
        this.side = side;
    }
 
    @Override
    public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}
 
public static class Circle implements Element {
    public final double radius;
 
    public Circle(double radius) {
        this.radius = radius;
    }
 
    @Override
    public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}
 
public static class Rectangle implements Element {
    public final double width;
    public final double height;
 
    public Rectangle( double width, double height ) {
        this.width = width;
        this.height = height;
    }
 
    @Override
    public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}

正如预期的那样,我们域的所有类都必须实现Element接口。他们以同样的方式这样做,将自己传递给访客。然后,我们可以定义一个Visitor接口,为每个要访问的对象类型声明一个抽象方法。

interface Visitor<T> {
    T visit(Square element);
    T visit(Circle element);
    T visit(Rectangle element);
}

这就是为什么,尽管我们域中所有对象的accept()方法具有完全相同的实现,我们不能在一个公共抽象类中概括它们,或者甚至更好地将它直接移动到Element接口中作为其默认方法之一。事实上,调用访问者的对象的编译时类型是必要的,以确定必须调用访问方法的不同重载版本中的哪一个。现在可以创建此Visitor接口的不同具体实现。例如,我们可以计算出不同形状的区域:

public static class AreaVisitor implements Visitor<Double> {
 
    @Override
    public Double visit( Square element ) {
        return element.side * element.side;
    }
 
    @Override
    public Double visit( Circle element ) {
        return Math.PI * element.radius * element.radius;
    }
 
    @Override
    public Double visit( Rectangle element ) {
        return element.height * element.width;
    }
}

另一个计算他们的周长:

public static class PerimeterVisitor implements Visitor<Double> {
 
    @Override
    public Double visit( Square element ) {
        return 4 * element.side ;
    }
 
    @Override
    public Double visit( Circle element ) {
        return 2 * Math.PI * element.radius;
    }
 
    @Override
    public Double visit( Rectangle element ) {
        return ( 2 * element.height + 2 * element.width );
    }
}

我们终于可以让这些访客在计算形状列表的面积和周长之和。

public static void main(String[] args) {
    List<Element> figures = Arrays.asList( new Circle( 4 ), new Square( 5 ), new Rectangle( 6, 7 ));
 
    double totalArea = 0.0;
    Visitor<Double> areaVisitor = new AreaVisitor();
    for (Element figure : figures) {
        totalArea += figure.accept( areaVisitor );
    }
    System.out.println("Total area = " + totalArea);
 
    double totalPerimeter = 0.0;
    Visitor<Double> perimeterVisitor = new PerimeterVisitor();
    for (Element figure : figures) {
        totalPerimeter += figure.accept( perimeterVisitor );
    }
    System.out.println("Total perimeter = " + totalPerimeter);
}

值得注意的是访问者实际上做了什么:它允许为每种类型的对象定义一个不同的方法。在函数式编程中,有一种更自然,更强大的习惯用法来实现相同的结果:模式匹配。实际上对于这个用例,它已经足够有一个在类上工作的switch语句,我真的很想知道为什么这在Java中是不可能的,而在Java 7中他们增加了切换String的可能性,在我看来它几乎是无用的在大多数情况下也是一种不好的做法。也就是说,可以实现一个简单的实用程序类,它可以让我们拥有类似的功能。

public class LambdaVisitor<A> implements Function<Object, A> {
    private Map<Class<?>, Function<Object, A>> fMap = new HashMap<>();
 
    public <B> Acceptor<A, B> on(Class<B> clazz) {
        return new Acceptor<>(this, clazz);
    }
 
    @Override
    public A apply( Object o ) {
        return fMap.get(o.getClass()).apply( o );
    }
 
    static class Acceptor<A, B> {
        private final LambdaVisitor visitor;
        private final Class<B> clazz;
 
        Acceptor( LambdaVisitor<A> visitor, Class<B> clazz ) {
            this.visitor = visitor;
            this.clazz = clazz;
        }
 
        public LambdaVisitor<A> then(Function<B, A> f) {
            visitor.fMap.put( clazz, f );
            return visitor;
        }
    }
}

LambdaVisitor类实现一个Function,然后转换泛型Object为类型A的结果。on()方法是我们可以通过它定义此Function的行为的方法。它接受一个Class 作为参数并返回其Acceptor内部类的实例。这个类只有一个方法,然后()接受一个函数。换句话说,当函数应用于B的实例时,传递给on()方法的类会产生类型A的结果,这是LambdaVisitor函数应该返回的结果。Class ,Function双双注册在LambdaVisitor中的Map。最后,then()方法返回原始的LambdaVisitor实例,从而允许为另一个类流畅地注册另一个Function。

让我们尝试将其用于我们的原始任务。例如,我们可以定义一个函数,当应用于我们的域模型中的一个形状时,返回其区域。

static Function<Object, Double> areaCalculator = new LambdaVisitor<Double>()
        .on(Square.class).then( s -> s.side * s.side )
        .on(Circle.class).then( c -> Math.PI * c.radius * c.radius )
        .on(Rectangle.class).then( r -> r.height * r.width );

当此函数应用于形状时,LambdaVisitor选择为该对象的类定义的函数,并将其应用于对象本身。例如,当它与Square实例一起传递时,选择该函数

s -> s.side * s.side

并将其应用于该对象以返回Square的区域。请注意,由于类型推断,我们不必将类型Square重复到lambda的参数声明中:then()方法已经期望与调用on()方法的同一个Class的实例。类似地,我们可以定义计算不同形状的周长的第二个函数。

static Function<Object, Double> perimeterCalculator = new LambdaVisitor<Double>()
        .on(Square.class).then( s -> 4 * s.side )
        .on(Circle.class).then( c -> 2 * Math.PI * c.radius )
        .on(Rectangle.class).then( r -> 2 * r.height + 2 * r.width );

此时,可以直接使用这些函数计算前一个列表中所有形状的面积和周长之和。

public static void main( String[] args ) {
    List<Object> figures = Arrays.asList( new Circle( 4 ), new Square( 5 ), new Rectangle( 6, 7 ) );
 
    double totalArea = figures.stream().map( areaCalculator ).reduce( 0.0, (v1, v2) -> v1 + v2 );
    System.out.println("Total area = " + totalArea);
 
    double totalPerimeter = figures.stream().map( perimeterCalculator ).reduce( 0.0, (v1, v2) -> v1 + v2 );
    System.out.println("Total perimeter = " + totalPerimeter);
}