訪問者模式 in JavaScript

訪問者模式,即 visitor pattern,是一個很常見的模式,這是因為它能有效地構建出復雜的系統。更關鍵的是,在函數式語言中,它表現起來是如此的直觀。因此,我決定利用一個簡單的例子,來談談訪問者模式,并且希望能夠通過這個例子,讓大家感受到這一模式的威力。

王垠曾在他的文章 解密“設計模式” 中提到過訪問者模式:

所謂的 visitor,本質上就是函數式語言里的含有‘模式匹配’(pattern matching)的遞歸函數。

這一定義還是非常精確的,在我們介紹完訪問者模式后,會再回顧一下這句話。

一個簡單的例子

下面我們將會利用一個小例子,介紹訪問者模式。

假設在一個二維的坐標系中,定義一個類 Point,有兩個方法,

  • getDistance 用于計算 point 到原點的距離
  • minus 接收一個點 p 作為參數,將兩個點的坐標相減得到一個新坐標,通過新坐標創建一個新的點

代碼如下:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  // 計算 point 到原點的距離
  getDistance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
  
  // point 與另一個點 p 的坐標相減得到一個新坐標,通過新坐標創建一個新的點
  minus (p) {
    const delX = this.x - p.x;
    const delY = this.y - p.y;
    return new Point(delX, delY);
  }
}

再定義一個基本的形狀類 Circle,Circle 有一個方法 hasPoint 用于判斷傳進來的 point 是否在 circle 的范圍內,代碼如下:

class Circle {
  constructor(r) {
    this.r = r;
  }
  
  // 判斷 p 是否在 circle 的范圍內
  hasPoint (p) {
    return p.getDistance() <= this.r;
  }
}

再定義一個基本的形狀類 Square,和 Circle 一樣有一個 hasPoint 方法,代碼如下:

class Square {
  constructor(s) {
    this.s = s;
  }
  
  // 判斷 p 是否在 square 的范圍內
  hasPoint (p) {
    return (p.x <= this.s) && (p.y <= this.s);
  }
}

在有了上面的幾個類定義后,我們通過下面的代碼觀察下如何使用這些類:

var p1 = new Point(1, 2);

var square1 = new Square(2);
var circle1 = new Circle(2);

square1.hasPoint(p1);   // true
circle1.hasPoint(p1);   // false

上面的例子雖然符合我們的期望。但是,這個系統還過于簡單。

仔細觀察就會發現,創建出來的形狀都是基于原點的。為了增加一些難度,我們新增一個類 Trans,讓形狀可以位移。注意新的類 Trans 的 hasPoint 方法的實現。

class Trans {
  constructor(point, shape) {
    this.point = point;
    this.shape = shape;
  }
  
  hasPoint (p) {
    var { point, shape } = this;
    var newP = p.minus(point);
    return shape.hasPoint(newP);
  }
}

讓我們再添加一些簡單的例子吧。

var p1 = new Point(1, 2);
var p2 = new Point(1, 1);

var square1 = new Square(2);
var circle1 = new Circle(2);
var trans1 = new Trans(p2, circle1);

square1.hasPoint(p1);       // true
trans1.hasPoint(p1);        // true

通過上面的例子可以發現,傳遞給 Trans 的 shape 不僅僅只能是基本的形狀 Circle,Square,還能是位移之后的 Trans。這是因為 Trans.hasPoint 的實現是依賴傳進來的 shape.hasPoint,但是這個 shape 具體是什么它并不關心。而這正是訪問者模式的核心所在。

通過讓 Circle,Square,Trans 實現同一個方法 hasPoint,并且通過 Trans 實現形狀的組合功能,從而讓這個系統更加強大。可以想象,我們可以像定義 Trans 一樣,引入更多的轉換功能,比如實現 Rotate,Scale 類等等,并且讓各種 shape 相互組合,得到更加復雜的 shape。從而做到,在不修改原有代碼的情況下,構建出更加復雜的系統。

分析與變換

細心的讀者也許發現了,上面的例子雖然有趣,當是似乎和本文開頭所講的函數式語言關系不大,和王垠所定義的訪問者模式也不相同(甚至和 Java 中的訪問者模式也不一樣)。

這是因為,在實際開發中,為了讓系統各個部分更加清晰,尤其是大型系統,人們會更傾向于將所有的 hasPoint 方法的實現放在一起,然后將這些實現作為部件添加到 Shape 中。而完成了這一步,才算是真正實現了訪問者模式。

想在 Java 中實現訪問者模式會比較繞,所幸我們用的是 JavaScript。下面,我會將上面的例子做一些簡單的變換,使其更符合預期。但要記住,這些變換從本質上來說其實都是等價的,只是代碼形式的轉換而已。

首先,去除所有形狀中的 hasPoint 方法,并且引入一個 type 的屬性。代碼如下:

var CIRCLE = 'CIRCLE';
var SQUARE = 'SQUARE';
var TRANS = 'TRANS';

class Circle {
  constructor(r) {
    this.r = r;
    this.type = CIRCLE;
  }
}

class Square {
  constructor(s) {
    this.s = s;
    this.type = SQUARE;
  }
}

class Trans {
  constructor(point, shape) {
    this.point = point;
    this.shape = shape;
    this.type = TRANS;
  }
}

然后,我們創建一個新函數,將原先所有的 hasPoint 方法集中在一起,這個函數就是訪問者模式的關鍵啦。

var hasPoint = (s, p) => {
  // 利用 switch 做模式匹配
  switch (s.type) {
    case CIRCLE:
      return p.getDistance() <= s.r;
    case SQUARE:
      return (p.x <= s.s) && (p.y <= s.s);
    case TRANS:
      var { point, shape } = s;
      var newP = p.minus(point);
      return hasPoint(shape, newP);     // 遞歸調用 hasPoint
     default:
      console.error('HAS_POINT -- unexpteced type', s.type);
  }
}

這個新函數只是利用 switch 將原來的 hasPoint 方法集合,但它確實符合王垠定義中的兩個關鍵點

  • 模式匹配
  • 遞歸

而這正是訪問者模式的特征所在。

如果你是面向對象的忠實粉絲的話,還可以添加一個抽象類,通過讓所有的 shape 都繼承這一抽象類,重新獲得原來的 hasPoint方法。代碼如下:

class AbstractShape {
  hasPoint (p) { return hasPoint(this, p) }
}

最后

希望這個簡單的例子,能向大家闡明訪問者模式是如何構建復雜系統的。其關鍵是:

  • 通過組合的方式構建出更加復雜的系統
  • 利用遞歸達到解耦的效果

比如上面的例子,定義了組合類 Trans( 甚至 Rotate,Scale 等),讓基本類 Circle,Square 得以通過不同的組合方式構建出更加復雜的 shape。而 hasPoint 函數中的遞歸,則讓 Trans 可以不關心其接收的 shape 的類型,從而達到解耦的效果。

希望這篇文章,能夠對你有所啟發,有所幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容