訪問者模式 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 的類型,從而達到解耦的效果。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容