本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/visitor
在訪問者模式(Visitor Pattern)中苞轿,通過一個訪問者類仿畸,來封裝對數(shù)據(jù)結(jié)構(gòu)中不同類型元素的執(zhí)行算法吹榴。通過這種方式唇撬,元素的執(zhí)行算法可以隨著訪問者改變而改變瓜喇。這種類型的設(shè)計模式屬于行為型模式吊趾。
例子
我們假設(shè)有一些形狀茬斧,包括三角形威蕉、矩形和圓形這三種不同的幾何形狀。我們知道不同的形狀其參數(shù)是不同的:
- 三角形:三條邊的長度確定一個三角形窜管;
- 矩形:長和寬確定一個矩形散劫;
- 圓形就一個參數(shù)——半徑。
那么任務(wù)來了幕帆,我們把一系列類型和參數(shù)不同的形狀用ArrayList
來管理获搏,然后依次遍歷,并計算出它們的周長失乾。
一種計算任務(wù)
這個任務(wù)非常簡單常熙,由于用ArrayList
來管理,因此需要各個不同形狀抽象出統(tǒng)一的接口Shape
碱茁。這個接口定義一個共同的方法getPerimeter
裸卫,然后這三種形狀都實(shí)現(xiàn)這個方法就OK了嘛。其代碼如下:
Shape.java
public interface Shape {
double getPerimeter();
}
Triangle.java(三個屬性:三條邊的長度)
public class Triangle implements Shape {
private double edgeA;
private double edgeB;
private double edgeC;
public Triangle(double edgeA, double edgeB, double edgeC) {
this.edgeA = edgeA;
this.edgeB = edgeB;
this.edgeC = edgeC;
}
public double getEdgeA() {
return edgeA;
}
public double getEdgeB() {
return edgeB;
}
public double getEdgeC() {
return edgeC;
}
public double getPerimeter() {
return edgeA + edgeB + edgeC;
}
}
Rectangle.java(兩個屬性:長和寬)
public class Rectangle implements Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
public double getLength() {
return length;
}
public double getWidth() {
return width;
}
public double getPerimeter() {
return (length + width) * 2;
}
}
Circle.java(一個屬性:半徑)
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void getPerimeter() {
return 2 * Math.PI * radius;
}
}
齊活兒~ 寫個Client
交卷了:
Client.java
public class Client {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<Shape>();
shapes.add(new Triangle(1.3, 2.2, 3.1));
shapes.add(new Circle(1.2));
shapes.add(new Triangle(2.4, 3.3, 4.2));
shapes.add(new Ractangle(2.1, 3.2));
shapes.add(new Circle(5.6));
for (Shape shape : shapes) {
System.out.println(shape.getPerimeter());
}
}
}
這是面向接口編程的最基本用法了吧纽竣。不過別忙著高興墓贿,如果這個時候再加一個任務(wù)——“求面積”呢?那就在接口里再增加一個getArea
方法蜓氨,然后所有的形狀都實(shí)現(xiàn)了唄~
好吧聋袋,也可以,不過任務(wù)還遠(yuǎn)沒有結(jié)束穴吹,如果還要算出能夠包住每個形狀的最小的圓的直徑呢幽勒?以及能夠置于形狀內(nèi)的最大的圓的直徑呢?等等等等刀荒。。棘钞。
每次提出新的計算任務(wù)后缠借,頻繁修改接口及其實(shí)現(xiàn)類,這顯然是不符合“開閉”原則的宜猜,而且明顯也是不優(yōu)雅的泼返。況且,天知道將來還會需要計算什么幺蛾子姨拥!
多種計算任務(wù)绅喉,策略模式
必須要調(diào)整一下設(shè)計思路以滿足靈活性。
我們曾經(jīng)遇到過對于同一個對象進(jìn)行不同運(yùn)算的設(shè)計——比如策略模式叫乌,無論是計算周長柴罐、面積都是不同的策略,我們將不同的策略作為對象傳遞給形狀憨奸,就可以得到該策略相應(yīng)的結(jié)果革屠。比如對于矩形:
// 對于矩形,在Rectangle.java
public double accept(Strategy strategy) {
return strategy.calculate(this);
}
// 所有的策略都實(shí)現(xiàn)統(tǒng)一的接口 Strategy
public interface Strategy {
double calculate(Rectangle rect);
}
// 對于計算周長的策略,在PerimeterStrategy.java
public class PerimeterStrategy implements Strategy {
public double calculate(Rectangle rect) {
return 2 * (rect.getLength() + rect.getWidth());
}
}
// 對于計算面積的策略似芝,在AreaStrategy.java
public class AreaStrategy implements Strategy {
public double calculate(Rectangle rect) {
return rect.getLength() * rect.getWidth();
}
}
不同類型對象的多種計算任務(wù)那婉,訪問者模式
上邊的例子是針對矩形的,那么三角形和圓形也都實(shí)現(xiàn)相應(yīng)的策略類的話党瓮,類的數(shù)量就很快增長起來了详炬,似乎也不優(yōu)雅嘛! 其實(shí)很簡單寞奸,因為無論是周長策略還是面積策略呛谜,各個形狀都要實(shí)現(xiàn),那對于同一種策略打個包不就OK了嗎:
Calculator.java
public interface Calculator {
double ofShape(Triangle triangle);
double ofShape(Circle circle);
double ofShape(Square square);
}
Perimeter.java(各種形狀周長策略的打包)
public class Perimeter implements Calculator {
public double ofShape(Triangle triangle) {
return triangle.getEdgeA() + triangle.getEdgeB() + triangle.getEdgeC();
}
public double ofShape(Circle circle) {
return circle.getRadius() * Math.PI * 2;
}
public double ofShape(Square square) {
return square.getEdge() * 4;
}
}
Area.java(各種形狀面積策略的打包)
public class Area implements Calculator {
public double ofShape(Triangle triangle) {
double a = triangle.getEdgeA(), b = triangle.getEdgeB(), c = triangle.getEdgeC();
double p = (a + b + c) / 2;
return Math.sqrt(p * (p - a) * (p - b) * (p - c));
}
public double ofShape(Circle circle) {
return Math.PI * circle.getRadius() * circle.getRadius();
}
public double ofShape(Square square) {
return Math.pow(square.getEdge(), 2);
}
}
兩種策略的打包都實(shí)現(xiàn)自Calculator
接口蝇闭,以后還有啥計算需求呻率,起個類實(shí)現(xiàn)這個接口就可以了。
那對于各個形狀呻引,剛才的代碼也要稍微調(diào)整一下:
Shape.java
public interface Shape {
// double getPerimeter();
// 對于不同的計算策略來者不拒
double accept(Calculator calculator);
}
Triangle.java(三個屬性:三條邊的長度)
public class Triangle implements Shape {
private double edgeA;
private double edgeB;
private double edgeC;
public Triangle(double edgeA, double edgeB, double edgeC) {
this.edgeA = edgeA;
this.edgeB = edgeB;
this.edgeC = edgeC;
}
public double getEdgeA() {
return edgeA;
}
public double getEdgeB() {
return edgeB;
}
public double getEdgeC() {
return edgeC;
}
// public double getPerimeter() {
// return edgeA + edgeB + edgeC;
// }
// 方法接受策略對象為參數(shù)礼仗,方法內(nèi)將自身作為參數(shù)再傳給策略的方法
public double accept(Calculator calculator) {
return calculator.ofShape(this);
}
}
在accept
方法中,接受策略對象為參數(shù)逻悠,方法內(nèi)將自身作為參數(shù)再傳給策略的具體方法元践,這種方式叫做“雙重分派”,高大上的名字往往不好記也不好理解童谒,哈哈单旁,其實(shí)不記也罷,通過這種巧妙的回調(diào)方式實(shí)現(xiàn)不同策略對不同類型對象的計算任務(wù)饥伊。
我們再測試一下:
Client.java
public class Client {
public static void main(String[] args) {
// 一個含有5個元素的List象浑,包含三種不同的形狀
List<Shape> shapes = new ArrayList<Shape>();
shapes.add(new Triangle(1.3, 2.2, 3.1));
shapes.add(new Circle(1.2));
shapes.add(new Triangle(2.4, 3.3, 4.2));
shapes.add(new Rectangle(2.1, 3.2));
shapes.add(new Circle(5.6));
// 計算周長和面積的不同策略(訪問者)
Perimeter perimeter = new Perimeter();
Area area = new Area();
// 將周長和面積的計算策略傳入(接受不同訪問者的訪問)
for (Shape shape : shapes) {
System.out.printf("周長 : %5.2f\t 面積 : %5.2f\n", shape.accept(perimeter), shape.accept(area));
}
}
}
將不同的策略對象傳遞給各個元素,從而對不同類型的元素進(jìn)行不同策略的計算琅豆。是不是感覺代碼優(yōu)雅了不少呢_
測試結(jié)果:
周長 : 6.60 面積 : 1.20
周長 : 7.54 面積 : 4.52
周長 : 9.90 面積 : 3.95
周長 : 10.60 面積 : 6.72
周長 : 35.19 面積 : 98.52
總結(jié)
上邊的例子就是應(yīng)用了訪問者模式愉豺。為啥叫訪問者模式呢?
其實(shí)例子中的策略就相當(dāng)于依次訪問各個元素的訪問者茫因,每個元素可以接受(accept
)不同訪問者作為參數(shù)蚪拦,從而交由訪問者做出不同的操作。
我們也可以看出訪問者模式的應(yīng)用場景具有如下特點(diǎn):
- 通常用于處理數(shù)據(jù)結(jié)構(gòu)中不同類型元素的遍歷處理問題冻押。這里所說的數(shù)據(jù)結(jié)構(gòu)比如例子中的列表驰贷,或者數(shù)組、Map洛巢、Stack括袒、Set,甚至復(fù)雜的樹稿茉,重點(diǎn)不在于數(shù)據(jù)結(jié)構(gòu)箱熬,而在于不同類型的元素放到一起类垦,要“因材施教”。
- 即使對于每種類型的元素城须,也有不同的“訪問方式”蚤认,將不同的“訪問方式”作為不同的對象傳遞給元素「夥ィ“訪問者”相當(dāng)于對各種類型的元素的同一種“訪問方式”的打包砰琢。
- 使用到了雙重分派,在
accept
方法中良瞧,接受策略對象為參數(shù)陪汽,方法內(nèi)將自身作為參數(shù)再傳給策略的具體方法。
所以褥蚯,這是一個 m x n
的問題挚冤,多種元素對應(yīng)多種“訪問方式”。