Java設(shè)計模式百例 - 訪問者模式

本文源碼見: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)多種“訪問方式”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赞庶,一起剝皮案震驚了整個濱河市训挡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歧强,老刑警劉巖澜薄,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異摊册,居然都是意外死亡肤京,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門茅特,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忘分,“玉大人,你說我怎么就攤上這事白修《事停” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵熬荆,是天一觀的道長舟山。 經(jīng)常有香客問我绸狐,道長卤恳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任寒矿,我火速辦了婚禮突琳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘符相。我一直安慰自己拆融,他們只是感情好蠢琳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著镜豹,像睡著了一般傲须。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趟脂,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天泰讽,我揣著相機(jī)與錄音,去河邊找鬼昔期。 笑死已卸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的硼一。 我是一名探鬼主播累澡,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼般贼!你這毒婦竟也來了愧哟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤具伍,失蹤者是張志新(化名)和其女友劉穎翅雏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體人芽,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡望几,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萤厅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橄抹。...
    茶點(diǎn)故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惕味,靈堂內(nèi)的尸體忽然破棺而出楼誓,到底是詐尸還是另有隱情,我是刑警寧澤名挥,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布疟羹,位于F島的核電站,受9級特大地震影響禀倔,放射性物質(zhì)發(fā)生泄漏榄融。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一救湖、第九天 我趴在偏房一處隱蔽的房頂上張望愧杯。 院中可真熱鬧,春花似錦鞋既、人聲如沸力九。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跌前。三九已至棕兼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抵乓,已是汗流浹背程储。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臂寝,地道東北人章鲤。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像咆贬,于是被迫代替她去往敵國和親败徊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法掏缎,類相關(guān)的語法皱蹦,內(nèi)部類的語法,繼承相關(guān)的語法眷蜈,異常的語法沪哺,線程的語...
    子非魚_t_閱讀 31,625評論 18 399
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)酌儒,斷路器辜妓,智...
    卡卡羅2017閱讀 134,654評論 18 139
  • 轉(zhuǎn)自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帥199207閱讀 8,520評論 3 93
  • 知識太確定,往往不適合每個人的 環(huán)境忌怎。而自己在具體環(huán)境中實(shí)踐得到的知識和教訓(xùn)肖爵,用以指導(dǎo)下一步的行動衍慎,往往很有用酒繁。雖...
    昔時橫波目閱讀 498評論 0 0
  • 關(guān)鍵詞:問 1膳凝、你認(rèn)為什么樣的人是銷售精英?你覺得銷售精英應(yīng)該具備什么樣的心態(tài)鸥印? 買好貨勋功、收好款;銷售的產(chǎn)品不一樣...
    啟廈閱讀 195評論 0 0