訪問者模式 in JavaScript

訪問者模式亲桥,即 visitor pattern,是一個(gè)很常見的模式固耘,這是因?yàn)樗苡行У貥?gòu)建出復(fù)雜的系統(tǒng)题篷。更關(guān)鍵的是,在函數(shù)式語言中厅目,它表現(xiàn)起來是如此的直觀番枚。因此,我決定利用一個(gè)簡單的例子损敷,來談?wù)勗L問者模式葫笼,并且希望能夠通過這個(gè)例子,讓大家感受到這一模式的威力拗馒。

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

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

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

一個(gè)簡單的例子

下面我們將會(huì)利用一個(gè)小例子挥等,介紹訪問者模式友绝。

假設(shè)在一個(gè)二維的坐標(biāo)系中,定義一個(gè)類 Point触菜,有兩個(gè)方法九榔,

  • getDistance 用于計(jì)算 point 到原點(diǎn)的距離
  • minus 接收一個(gè)點(diǎn) p 作為參數(shù),將兩個(gè)點(diǎn)的坐標(biāo)相減得到一個(gè)新坐標(biāo)涡相,通過新坐標(biāo)創(chuàng)建一個(gè)新的點(diǎn)

代碼如下:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  // 計(jì)算 point 到原點(diǎn)的距離
  getDistance() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
  
  // point 與另一個(gè)點(diǎn) p 的坐標(biāo)相減得到一個(gè)新坐標(biāo),通過新坐標(biāo)創(chuàng)建一個(gè)新的點(diǎn)
  minus (p) {
    const delX = this.x - p.x;
    const delY = this.y - p.y;
    return new Point(delX, delY);
  }
}

再定義一個(gè)基本的形狀類 Circle剩蟀,Circle 有一個(gè)方法 hasPoint 用于判斷傳進(jìn)來的 point 是否在 circle 的范圍內(nèi)催蝗,代碼如下:

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

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

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

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

var p1 = new Point(1, 2);

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

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

上面的例子雖然符合我們的期望。但是缰冤,這個(gè)系統(tǒng)還過于簡單犬缨。

仔細(xì)觀察就會(huì)發(fā)現(xiàn),創(chuàng)建出來的形狀都是基于原點(diǎn)的棉浸。為了增加一些難度怀薛,我們新增一個(gè)類 Trans,讓形狀可以位移迷郑。注意新的類 Trans 的 hasPoint 方法的實(shí)現(xiàn)枝恋。

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);
  }
}

讓我們?cè)偬砑右恍┖唵蔚睦影伞?/p>

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

通過上面的例子可以發(fā)現(xiàn)创倔,傳遞給 Trans 的 shape 不僅僅只能是基本的形狀 Circle,Square焚碌,還能是位移之后的 Trans畦攘。這是因?yàn)?Trans.hasPoint 的實(shí)現(xiàn)是依賴傳進(jìn)來的 shape.hasPoint,但是這個(gè) shape 具體是什么它并不關(guān)心十电。而這正是訪問者模式的核心所在知押。

通過讓 Circle,Square鹃骂,Trans 實(shí)現(xiàn)同一個(gè)方法 hasPoint台盯,并且通過 Trans 實(shí)現(xiàn)形狀的組合功能,從而讓這個(gè)系統(tǒng)更加強(qiáng)大偎漫∫遥可以想象,我們可以像定義 Trans 一樣象踊,引入更多的轉(zhuǎn)換功能温亲,比如實(shí)現(xiàn) Rotate,Scale 類等等杯矩,并且讓各種 shape 相互組合栈虚,得到更加復(fù)雜的 shape。從而做到史隆,在不修改原有代碼的情況下魂务,構(gòu)建出更加復(fù)雜的系統(tǒng)。

分析與變換

細(xì)心的讀者也許發(fā)現(xiàn)了泌射,上面的例子雖然有趣粘姜,當(dāng)是似乎和本文開頭所講的函數(shù)式語言關(guān)系不大,和王垠所定義的訪問者模式也不相同(甚至和 Java 中的訪問者模式也不一樣)熔酷。

這是因?yàn)楣陆簦趯?shí)際開發(fā)中,為了讓系統(tǒng)各個(gè)部分更加清晰拒秘,尤其是大型系統(tǒng)号显,人們會(huì)更傾向于將所有的 hasPoint 方法的實(shí)現(xiàn)放在一起,然后將這些實(shí)現(xiàn)作為部件添加到 Shape 中躺酒。而完成了這一步押蚤,才算是真正實(shí)現(xiàn)了訪問者模式。

想在 Java 中實(shí)現(xiàn)訪問者模式會(huì)比較繞羹应,所幸我們用的是 JavaScript揽碘。下面,我會(huì)將上面的例子做一些簡單的變換,使其更符合預(yù)期钾菊。但要記住帅矗,這些變換從本質(zhì)上來說其實(shí)都是等價(jià)的,只是代碼形式的轉(zhuǎn)換而已煞烫。

首先浑此,去除所有形狀中的 hasPoint 方法,并且引入一個(gè) 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;
  }
}

然后凛俱,我們創(chuàng)建一個(gè)新函數(shù),將原先所有的 hasPoint 方法集中在一起料饥,這個(gè)函數(shù)就是訪問者模式的關(guān)鍵啦蒲犬。

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);     // 遞歸調(diào)用 hasPoint
     default:
      console.error('HAS_POINT -- unexpteced type', s.type);
  }
}

這個(gè)新函數(shù)只是利用 switch 將原來的 hasPoint 方法集合,但它確實(shí)符合王垠定義中的兩個(gè)關(guān)鍵點(diǎn)

  • 模式匹配
  • 遞歸

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

如果你是面向?qū)ο蟮闹覍?shí)粉絲的話原叮,還可以添加一個(gè)抽象類,通過讓所有的 shape 都繼承這一抽象類巡蘸,重新獲得原來的 hasPoint方法奋隶。代碼如下:

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

最后

希望這個(gè)簡單的例子,能向大家闡明訪問者模式是如何構(gòu)建復(fù)雜系統(tǒng)的悦荒。其關(guān)鍵是:

  • 通過組合的方式構(gòu)建出更加復(fù)雜的系統(tǒng)
  • 利用遞歸達(dá)到解耦的效果

比如上面的例子唯欣,定義了組合類 Trans( 甚至 Rotate,Scale 等)搬味,讓基本類 Circle境氢,Square 得以通過不同的組合方式構(gòu)建出更加復(fù)雜的 shape。而 hasPoint 函數(shù)中的遞歸碰纬,則讓 Trans 可以不關(guān)心其接收的 shape 的類型萍聊,從而達(dá)到解耦的效果。

希望這篇文章悦析,能夠?qū)δ阌兴鶈l(fā)脐区,有所幫助。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末她按,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炕柔,更是在濱河造成了極大的恐慌酌泰,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匕累,死亡現(xiàn)場(chǎng)離奇詭異陵刹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)欢嘿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門衰琐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來也糊,“玉大人,你說我怎么就攤上這事羡宙±晏辏” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵狗热,是天一觀的道長钞馁。 經(jīng)常有香客問我,道長匿刮,這世上最難降的妖魔是什么僧凰? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮熟丸,結(jié)果婚禮上训措,老公的妹妹穿的比我還像新娘。我一直安慰自己光羞,他們只是感情好绩鸣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狞山,像睡著了一般全闷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萍启,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天总珠,我揣著相機(jī)與錄音,去河邊找鬼勘纯。 笑死局服,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的驳遵。 我是一名探鬼主播淫奔,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼堤结!你這毒婦竟也來了唆迁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤竞穷,失蹤者是張志新(化名)和其女友劉穎唐责,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘾带,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鼠哥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朴恳。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抄罕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出于颖,到底是詐尸還是另有隱情呆贿,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布恍飘,位于F島的核電站榨崩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏章母。R本人自食惡果不足惜母蛛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乳怎。 院中可真熱鬧彩郊,春花似錦、人聲如沸蚪缀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽询枚。三九已至违帆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間金蜀,已是汗流浹背刷后。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留渊抄,地道東北人尝胆。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像护桦,于是被迫代替她去往敵國和親含衔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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