訪問者模式的函式式實現
在面向物件的程式設計中,當需要向現有物件新增新操作時,通常使用訪問者模式,但由於設計原因不可能修改物件本身並在實現中直接新增缺少的操作。為此,我們域中的每個物件都必須有一個接受訪問者並將自己傳遞給該訪問者的方法,然後必須實現如下所示的介面。
<b>interface</b> Element { <T> T accept(Visitor<T> visitor); }
此時,我們可以定義一個簡單的業務域,並展示不同目的的不同訪問者如何訪問它。對於此示例,我們的域模型將由簡單的幾何形狀構成。
<b>public</b> <b>static</b> <b>class</b> Square implements Element { <b>public</b> <b>final</b> <b>double</b> side; <b>public</b> Square(<b>double</b> side) { <b>this</b>.side = side; } @Override <b>public</b> <T> T accept(Visitor<T> visitor) { <b>return</b> visitor.visit(<b>this</b>); } } <b>public</b> <b>static</b> <b>class</b> Circle implements Element { <b>public</b> <b>final</b> <b>double</b> radius; <b>public</b> Circle(<b>double</b> radius) { <b>this</b>.radius = radius; } @Override <b>public</b> <T> T accept(Visitor<T> visitor) { <b>return</b> visitor.visit(<b>this</b>); } } <b>public</b> <b>static</b> <b>class</b> Rectangle implements Element { <b>public</b> <b>final</b> <b>double</b> width; <b>public</b> <b>final</b> <b>double</b> height; <b>public</b> Rectangle( <b>double</b> width, <b>double</b> height ) { <b>this</b>.width = width; <b>this</b>.height = height; } @Override <b>public</b> <T> T accept(Visitor<T> visitor) { <b>return</b> visitor.visit(<b>this</b>); } }
正如預期的那樣,我們域的所有類都必須實現Element介面。他們以同樣的方式這樣做,將自己傳遞給訪客。然後,我們可以定義一個Visitor介面,為每個要訪問的物件型別宣告一個抽象方法。
<b>interface</b> Visitor<T> { T visit(Square element); T visit(Circle element); T visit(Rectangle element); }
這就是為什麼,儘管我們域中所有物件的accept()方法具有完全相同的實現,我們不能在一個公共抽象類中概括它們,或者甚至更好地將它直接移動到Element介面中作為其預設方法之一。事實上,呼叫訪問者的物件的編譯時型別是必要的,以確定必須呼叫訪問方法的不同過載版本中的哪一個。現在可以建立此Visitor介面的不同具體實現。例如,我們可以計算出不同形狀的區域:
<b>public</b> <b>static</b> <b>class</b> AreaVisitor implements Visitor<Double> { @Override <b>public</b> Double visit( Square element ) { <b>return</b> element.side * element.side; } @Override <b>public</b> Double visit( Circle element ) { <b>return</b> Math.PI * element.radius * element.radius; } @Override <b>public</b> Double visit( Rectangle element ) { <b>return</b> element.height * element.width; } }
另一個計算他們的周長:
<b>public</b> <b>static</b> <b>class</b> PerimeterVisitor implements Visitor<Double> { @Override <b>public</b> Double visit( Square element ) { <b>return</b> 4 * element.side ; } @Override <b>public</b> Double visit( Circle element ) { <b>return</b> 2 * Math.PI * element.radius; } @Override <b>public</b> Double visit( Rectangle element ) { <b>return</b> ( 2 * element.height + 2 * element.width ); } }
我們終於可以讓這些訪客在計算形狀列表的面積和周長之和。
<b>public</b> <b>static</b> <b>void</b> main(String[] args) { List<Element> figures = Arrays.asList( <b>new</b> Circle( 4 ), <b>new</b> Square( 5 ), <b>new</b> Rectangle( 6, 7 )); <b>double</b> totalArea = 0.0; Visitor<Double> areaVisitor = <b>new</b> AreaVisitor(); <b>for</b> (Element figure : figures) { totalArea += figure.accept( areaVisitor ); } System.out.println(<font>"Total area = "</font><font> + totalArea); <b>double</b> totalPerimeter = 0.0; Visitor<Double> perimeterVisitor = <b>new</b> PerimeterVisitor(); <b>for</b> (Element figure : figures) { totalPerimeter += figure.accept( perimeterVisitor ); } System.out.println(</font><font>"Total perimeter = "</font><font> + totalPerimeter); } </font>
值得注意的是訪問者實際上做了什麼:它允許為每種型別的物件定義一個不同的方法。在函數語言程式設計中,有一種更自然,更強大的習慣用法來實現相同的結果:模式匹配。實際上對於這個用例,它已經足夠有一個在類上工作的switch語句,我真的很想知道為什麼這在Java中是不可能的,而在Java 7中他們增加了切換String的可能性,在我看來它幾乎是無用的在大多數情況下也是一種不好的做法。也就是說,可以實現一個簡單的實用程式類,它可以讓我們擁有類似的功能。
<b>public</b> <b>class</b> LambdaVisitor<A> implements Function<Object, A> { <b>private</b> Map<Class<?>, Function<Object, A>> fMap = <b>new</b> HashMap<>(); <b>public</b> <B> Acceptor<A, B> on(Class<B> clazz) { <b>return</b> <b>new</b> Acceptor<>(<b>this</b>, clazz); } @Override <b>public</b> A apply( Object o ) { <b>return</b> fMap.get(o.getClass()).apply( o ); } <b>static</b> <b>class</b> Acceptor<A, B> { <b>private</b> <b>final</b> LambdaVisitor visitor; <b>private</b> <b>final</b> Class<B> clazz; Acceptor( LambdaVisitor<A> visitor, Class<B> clazz ) { <b>this</b>.visitor = visitor; <b>this</b>.clazz = clazz; } <b>public</b> LambdaVisitor<A> then(Function<B, A> f) { visitor.fMap.put( clazz, f ); <b>return</b> visitor; } } }
LambdaVisitor類實現一個Function,然後轉換泛型Object為型別A的結果。on()方法是我們可以通過它定義此Function的行為的方法。它接受一個Class 作為引數並返回其Acceptor內部類的例項。這個類只有一個方法,然後()接受一個函式 。換句話說,當函式應用於B的例項時,傳遞給on()方法的類會產生型別A的結果,這是LambdaVisitor函式應該返回的結果。Class ,Function雙雙註冊在LambdaVisitor中的Map。最後,then()方法返回原始的LambdaVisitor例項,從而允許為另一個類流暢地註冊另一個Function。
讓我們嘗試將其用於我們的原始任務。例如,我們可以定義一個函式,當應用於我們的域模型中的一個形狀時,返回其區域。
<b>static</b> Function<Object, Double> areaCalculator = <b>new</b> LambdaVisitor<Double>() .on(Square.<b>class</b>).then( s -> s.side * s.side ) .on(Circle.<b>class</b>).then( c -> Math.PI * c.radius * c.radius ) .on(Rectangle.<b>class</b>).then( r -> r.height * r.width );
當此函式應用於形狀時,LambdaVisitor選擇為該物件的類定義的函式,並將其應用於物件本身。例如,當它與Square例項一起傳遞時,選擇該函式
s -> s.side * s.side
並將其應用於該物件以返回Square的區域。請注意,由於型別推斷,我們不必將型別Square重複到lambda的引數宣告中:then()方法已經期望與呼叫on()方法的同一個Class的例項。類似地,我們可以定義計算不同形狀的周長的第二個函式。
<b>static</b> Function<Object, Double> perimeterCalculator = <b>new</b> LambdaVisitor<Double>() .on(Square.<b>class</b>).then( s -> 4 * s.side ) .on(Circle.<b>class</b>).then( c -> 2 * Math.PI * c.radius ) .on(Rectangle.<b>class</b>).then( r -> 2 * r.height + 2 * r.width );
此時,可以直接使用這些函式計算前一個列表中所有形狀的面積和周長之和。
<b>public</b> <b>static</b> <b>void</b> main( String[] args ) { List<Object> figures = Arrays.asList( <b>new</b> Circle( 4 ), <b>new</b> Square( 5 ), <b>new</b> Rectangle( 6, 7 ) ); <b>double</b> totalArea = figures.stream().map( areaCalculator ).reduce( 0.0, (v1, v2) -> v1 + v2 ); System.out.println(<font>"Total area = "</font><font> + totalArea); <b>double</b> totalPerimeter = figures.stream().map( perimeterCalculator ).reduce( 0.0, (v1, v2) -> v1 + v2 ); System.out.println(</font><font>"Total perimeter = "</font><font> + totalPerimeter); } </font>