PicCollage 的員工大概可以分成兩類:Regular Memnber(正職)與 Intern(實習生)。
每個員工會輪流當 weekie(週值日生),負責處理當週的一些大小事。Regular Memebr weekie 一個禮拜會有兩個,負責澆花、主持 team meeting、推薦午餐地點等等;而 Intern weekie 則主要負責補充 PicCafe(我們的零食區 ?)。
(PicCafe)
現在我們想要讓員工根據所屬類別來做出相應的 weekie 職責。如果這個人是 Regular Memeber 那他就會澆花;如果是實習生,那他就會補充零食。聽起來很直觀,畫成圖大概就長這樣:
也就是在員工本來在做的事(doFunWork
跟 alwaysBeLearning
)之外,增加一個新的 beWeekie
行為。
很快的,我們有了一個新的任務:在 PicCollage 團隊中,我們除了 Weekie,還有一個 Mentor 的角色:每一個員工都會有一個 Mentor,是個可以讓員工在工作上解惑或是傾訴的對象,負責照顧這個人的心理健康(?)。現在,我們想要能夠找出每個員工的 Mentor 是誰。
那就跟
beWeekie()
一樣,分別在RegularMember
與Intern
中加入displayMentor()
方法就好啦?
是可以的,但是有幾個不太 OK 的點:
beWeekie()
跟 displayMentor()
其實都不是跟 Employee 最直接相關的行為,比較像是附屬功能,直接放在 Employee
類別中並不符合單一功能原則(Single Responsibility Principle)。RegularMember
與 Intern
,所以只需要改兩個地方,但假設我們今天還有一個 Contractor
類別,那就要改三個地方,如果 Employee 底下再加上 RemoteWorker
,那就要改四個地方......既然這些行為放在類別本身不妥,那該怎麼安置他們呢?答案就在標題上:Visitor Pattern!
Visitor 是一種行為型設計模式,讓你將演算法從物件中抽離出來。
登登!基本上,Visitor Pattern 讓我們將與該類別比較不相關的行為抽出來,集中到 Visitor 裡面管理。這些 Visitor 們定義好功能,當一個物件(這裡稱作 Element)需要有這些附屬行為時,就呼叫 Visitor。如此一來每個 Element 可以留下最相關的核心行為,並且只要實作一個接受 Visitor 的接口,就算之後還有新的附屬功能需要添加,也不再需要改到 Element 本身的程式碼。
首先,Visitor Interface 定義了一系列 visiting methods,每個 visit()
都接收一個 Concrete Element 作為參數。如果我們使用的語言有支援 overloading(接受同名的方法,呼叫時依照參數種類/數量來判斷要用哪一個,像是 C++ 或是 Java),那麼這些 visiting methods 的名字可以是一樣的。
每個實作 Visitor 的類別都會實現一個特定的(附屬)行為,並根據它訪問的類別做出該行為的不同版本。
Element 開了一個介面(這邊是 accept()
方法)給 Visitor。這個介面有一個參數來接受 Visitor,並在裡面呼叫對應的行為。這邊使用一個叫做 Double Dispatch 的技巧—在下面的程式碼範例中,我們可以看到每個 Element 的 accept()
方法中會將自己丟進它呼叫的 Visitor 方法,讓該 Visitor 可以做相應的行為。
Client 通常代表一個集合或是一個組合物件(complex object)(比如說一個 Composite)。
這裡原本分別在 RegularMember
與 Intern
中的 beWeekie()
邏輯都被放到了 WeekieVisitor
裡面統一管理。而在這兩個員工類別中,都新增了 accept()
來接受 Visitor 的「拜訪」。這個介面除了可以接受 WeekieVisitor
來做出 Weekie 相關的邏輯,也可以接受 MentorVisitor
來做出 Mentor 相關的邏輯。
class Visitor
def visit_rm(regular_member)
raise NotImplementedError
end
def visit_intern(intern)
raise NotImplementedError
end
end
class WeekieVisitor < Visitor
def visit_rm(regular_member)
"#{regular_member.name} is watering flowers!"
end
def visit_intern(intern)
# do weekie duties for interns
"#{intern.name} is adding snacks to PicCafe!"
end
end
class MentorVisitor < Visitor
def visit_rm(regular_member)
mentor = mentor_of(regular_member)
mentees = mentees_of(regular_member)
puts "#{regular_member.name}'s mentor is #{mentor}."
puts "#{regular_member.name}'s mentees are #{mentees}."
end
# intern 不會當 mentor,所以只有 mentor 資訊
def visit_intern(intern)
mentor = mentor_of(intern)
puts "#{intern.name}'s mentor is #{mentor}."
end
private
def mentor_of(employee)
# return the name of employee's mentor
end
def mentees_of(employee)
# return a list of mentees' names
end
end
# Element
class Employee
attr_reader: name
def do_fun_work
# good times
end
def always_be_learning
# learn learn learn
end
# for visitor
def accept(visitor)
end
end
class RegularMember < Employee
def accept(visitor)
visitor.visit_rm(self)
end
end
class Intern < Employee
def accept(visitor)
visitor.visit_intern(self)
end
end
Visitor 有時候可以被看作是高級版的 Command Pattern。它們都是將部分行為/邏輯從原本類別中抽出來,不過 Command 中獨立出來的行為只限制於單一類別,而 Visitor 則可以使用在不同的類別。
Visitor 就到這,下一篇是 Iterator!
作者:Jenny