訪問者模式亲桥,即 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ā)脐区,有所幫助。