設計模式的征途—16.訪問者(Visitor)模式
在患者就醫時,醫生會根據病情開具處方單,很多醫院都會存在以下這個流程:劃價人員拿到處方單之後根據藥品名稱和數量計算總價,而藥房工作人員根據藥品名稱和數量準備藥品,如下圖所示。
在軟件開發中,有時候也需要處理像處方單這樣的集合對象結構,在該對象結構中存儲了多個不同類型的對象信息,而且對同一對象結構中的元素的操作方式並不唯一,可能需要提供多種不同的處理方式。在設計模式中,有一種模式可以滿足上述要求,其模式動機就是以不同的方式操作復雜對象結構,該模式就是訪問者模式。
訪問者模式(Visitor) | 學習難度:★★★★☆ | 使用頻率:★☆☆☆☆ |
一、OA系統員工數據匯總設計
1.1 需求背景
Background:M公司開發部想要為某企業開發一個OA系統,在該OA系統中包含一個員工信息管理子系統,該企業包括正式員工和臨時工,每周HR部門和財務部等部門需要對員工數據進行匯總,匯總數據包括員工工作時間、員工工資等等。該企業的基本制度如下:
(1)正式員工(Full time Employee)每周工作時間為40小時,不同級別、不同部門的員工每周基本工資不同;如果超過40小時,超出部分按照100元/小時作為加班費;如果少於40小時,所缺時間按照請假處理,請假鎖扣工資以80元/小時計算,直到基本工資扣除到0為止。除了記錄實際工作時間外,HR部需要記錄加班時長或請假時長,作為員工平時表現的一項依據。
(2)臨時員工(Part time Employee)每周工作時間不固定,基本工資按照小時計算,不同崗位的臨時工小時工資不同。HR部只需要記錄實際工作時間。
HR人力資源部和財務部工作人員可以根據各自的需要對員工數據進行匯總處理,HR人力資源部負責匯總每周員工工作時間,而財務部負責計算每周員工工資。
1.2 初始設計
M公司開發人員針對需求,提出了一個初始的解決方案,其核心代碼如下:
public class EmployeeList { // 員工集合 private IList<Employee> empList = newList<Employee>(); // 增加員工 public void AddEmployee(Employee emp) { this.empList.Add(emp); } // 處理員工數據 public void Handle(string deptName) { if (deptName.Equals("財務部")) { foreach (var emp in empList) { if (emp.GetType().Equals("FullTimeEmployee")) { Console.WriteLine("財務部處理全職員工數據!"); } else { Console.WriteLine("財務部處理兼職員工數據!"); } } } else if (deptName.Equals("人力資源部")) { foreach (var emp in empList) { if (emp.GetType().Equals("FullTimeEmployee")) { Console.WriteLine("人力資源部處理全職員工數據!"); } else { Console.WriteLine("人力資源部處理兼職員工數據!"); } } } } }
不難發現,該解決方案存在以下問題:
(1)EmployeeList類非常龐大,承擔了過多的職責,既不便於代碼復用,也不便於系統擴展,違背了單一職責原則。
(2)包含了大量的if-else語句,測試和維護的難度增大。
(3)如果要新增一個部門來操作員工數據集合,那麽不得不修改EmployeeList類的源代碼,違背了開閉原則。
訪問者模式是一個可以考慮用來解決的方案,它可以在一定程度上解決上述問題(大部分問題)。
二、訪問者模式概述
2.1 訪問者模式簡介
訪問者模式是一種較為復雜的行為型模式,它包含訪問者和被訪問元素兩個主要組成部分,這些被訪問的元素通常具有不同的類型,且不同的訪問者可以對它們進行不同的訪問操作。例如:處方單中的各種藥品信息就是被訪問的元素,而劃價人員和藥房工作人員就是訪問者。訪問者模式可以使得用戶在不修改現有系統的情況下擴展系統的功能,為這些不同類型的元素增加新的操作。
訪問者(Visitor)模式:提供一個作用於某對象結構中的各元素的操作表示,它使得可以在不改變各元素的類的前提下定義作用於這些元素的新操作。訪問者模式是一種對象行為型模式。
2.2 訪問者模式結構
訪問者模式結構圖中包含以下5個角色:
(1)Visitor(抽象訪問者):抽象訪問者為對象結構中每一個具體元素類ConcreteElement聲明一個訪問操作,從這個操作的名稱或參數類型可以清楚知道需要訪問的具體元素的類型,具體訪問者則需要實現這些操作方法,定義對這些元素的訪問操作。
(2)ConcreteVisitor(具體訪問者):具體訪問者實現了抽象訪問者聲明的方法,每一個操作作用於訪問對象結構中一種類型的元素。
(3)Element(抽象元素):一般是一個抽象類或接口,定義一個Accept方法,該方法通常以一個抽象訪問者作為參數。
(4)ConcreteElement(具體元素):具體元素實現了Accept方法,在Accept方法中調用訪問者的訪問方法以便完成一個元素的操作。
(4)ObjectStructure(對象結構):對象結構是一個元素的集合,用於存放元素對象,且提供便利其內部元素的方法。
三、重構OA系統員工數據匯總
3.1 重構後的設計結構
在上圖中,FinanceDepartment表示財務部,HRDepartment表示人力資源部,它們充當具體訪問者的角色,其抽象父類Department充當抽象訪問者角色;EmployeeList充當對象結構,用於存儲員工列表;FullTimeEmployee表示全職員工,PartTimeEmployee表示兼職員工,它們充當具體元素角色,而其父類IEmployee(這裏實現形式是interface)充當抽象元素角色。
3.2 具體代碼實現
(1)抽象元素=>IEmployee
/// <summary> /// 抽象元素類:Employee /// </summary> public interface IEmployee { void Accept(Department handler); }
(2)具體元素=>FullTimeEmployee,PartTimeEmployee
/// <summary> /// 具體元素類:FullTimeEmployee /// </summary> public class FullTimeEmployee : IEmployee { public string Name { get; set; } public double WeeklyWage { get; set; } public int WorkTime { get; set; } public FullTimeEmployee(string name, double weeklyWage, int workTime) { this.Name = name; this.WeeklyWage = weeklyWage; this.WorkTime = workTime; } public void Accept(Department handler) { handler.Visit(this); } } /// <summary> /// 具體元素類:PartTimeEmployee /// </summary> public class PartTimeEmployee : IEmployee { public string Name { get; set; } public double HourWage { get; set; } public int WorkTime { get; set; } public PartTimeEmployee(string name, double hourWage, int workTime) { this.Name = name; this.HourWage = hourWage; this.WorkTime = workTime; } public void Accept(Department handler) { handler.Visit(this); } }
(3)對象結構=>EmployeeList
/// <summary> /// 對象結構類:EmployeeList /// </summary> public class EmployeeList { private IList<IEmployee> empList = new List<IEmployee>(); public void AddEmployee(IEmployee emp) { this.empList.Add(emp); } public void Accept(Department handler) { foreach (var emp in empList) { emp.Accept(handler); } }
(4)抽象訪問者=>Department
/// <summary> /// 抽象訪問者類:Department /// </summary> public abstract class Department { // 聲明一組重載的訪問方法,用於訪問不同類型的具體元素 public abstract void Visit(FullTimeEmployee employee); public abstract void Visit(PartTimeEmployee employee); }
(5)具體訪問者=>FinanceDepartment,HRDepartment
/// <summary> /// 具體訪問者類:FinanceDepartment /// </summary> public class FinanceDepartment : Department { // 實現財務部對兼職員工數據的訪問 public override void Visit(PartTimeEmployee employee) { int workTime = employee.WorkTime; double hourWage = employee.HourWage; Console.WriteLine("臨時工 {0} 實際工資為:{1} 元", employee.Name, workTime * hourWage); } // 實現財務部對全職員工數據的訪問 public override void Visit(FullTimeEmployee employee) { int workTime = employee.WorkTime; double weekWage = employee.WeeklyWage; if (workTime > 40) { weekWage = weekWage + (workTime - 40) * 50; } else if (workTime < 40) { weekWage = weekWage - (40 - workTime) * 80; if (weekWage < 0) { weekWage = 0; } } Console.WriteLine("正式員工 {0} 實際工資為:{1} 元", employee.Name, weekWage); } } /// <summary> /// 具體訪問者類:HRDepartment /// </summary> public class HRDepartment : Department { // 實現人力資源部對兼職員工數據的訪問 public override void Visit(PartTimeEmployee employee) { int workTime = employee.WorkTime; Console.WriteLine("臨時工 {0} 實際工作時間為:{1} 小時", employee.Name, workTime); } // 實現人力資源部對全職員工數據的訪問 public override void Visit(FullTimeEmployee employee) { int workTime = employee.WorkTime; Console.WriteLine("正式員工 {0} 實際工作時間為:{1} 小時", employee.Name, workTime); if (workTime > 40) { Console.WriteLine("正式員工 {0} 加班時間為:{1} 小時", employee.Name, workTime - 40); } else if (workTime < 40) { Console.WriteLine("正式員工 {0} 請假時間為:{1} 小時", employee.Name, 40 - workTime); } } }
(6)客戶端調用與測試
public class Program { public static void Main(string[] args) { EmployeeList empList = new EmployeeList(); IEmployee fteA = new FullTimeEmployee("梁思成", 3200.00, 45); IEmployee fteB = new FullTimeEmployee("徐誌摩", 2000, 40); IEmployee fteC = new FullTimeEmployee("梁徽因", 2400, 38); IEmployee fteD = new PartTimeEmployee("方鴻漸", 80, 20); IEmployee fteE = new PartTimeEmployee("唐宛如", 60, 18); empList.AddEmployee(fteA); empList.AddEmployee(fteB); empList.AddEmployee(fteC); empList.AddEmployee(fteD); empList.AddEmployee(fteE); Department dept = AppConfigHelper.GetDeptInstance() as Department; if (dept != null) { empList.Accept(dept); } Console.ReadKey(); } }
其中,AppConfigHelper用於從配置文件中獲得具體訪問者實例,配置文件如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="DeptName" value="Manulife.ChengDu.DesignPattern.Visitor.HRDepartment, Manulife.ChengDu.DesignPattern.Visitor" /> </appSettings> </configuration>
AppConfigHelper的具體代碼如下:
public class AppConfigHelper { public static string GetDeptName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["DeptName"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetDeptInstance() { string assemblyName = AppConfigHelper.GetDeptName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }View Code
編譯運行後的結果如下:
如果需要更換具體訪問者類,無須修改源代碼,只需要修改一下配置文件。例如這裏將訪問者由人力資源部更改為財務部:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="DeptName" value="Manulife.ChengDu.DesignPattern.Visitor.FinanceDepartment, Manulife.ChengDu.DesignPattern.Visitor" /> </appSettings> </configuration>
此時再次運行則會得到以下結果:
可以看出,如果我們要在系統中新增訪問者,那麽無需修改源代碼,只需新增一個新的具體訪問者類即可,從這一點看,訪問者模式符合開閉原則。
但是,如果我們要在系統中新增具體元素,比如新增一個新的員工類型為“退休人員”,由於原系統並未提供相應的訪問接口,因此必須對原有系統進行修改。所以,從新增新的元素來看,訪問者模式違背了開閉原則。
因此,訪問者模式與抽象工廠模式類似,對於開閉原則的支持具有“傾斜”性,可以方便地新增訪問者,但是添加新的元素較為麻煩。
四、訪問者模式總結
4.1 主要優點
(1)增加新的訪問操作十分方便,不痛不癢 => 符合開閉原則
(2)將有關元素對象的訪問行為集中到一個訪問者對象中,而不是分散在一個個的元素類中,類的職責更加清晰 => 符合單一職責原則
4.2 主要缺點
(1)增加新的元素類很困難,需要在每一個訪問者類中增加相應訪問操作代碼 => 違背了開閉原則
(2)元素對象有時候必須暴露一些自己的內部操作和狀態,否則無法供訪問者訪問 => 破壞了元素的封裝性
4.3 應用場景
(1)一個對象結構包含多個類型的對象,希望對這些對象實施一些依賴其具體類型的操作。=> 不同的類型可以有不同的訪問操作
(2)對象結構中對象對應的類很少改變 很少改變 很少改變(重要的事情說三遍),但經常需要在此對象結構上定義新的操作。
參考資料
劉偉,《設計模式的藝術—軟件開發人員內功修煉之道》
作者:周旭龍
出處:http://edisonchou.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
設計模式的征途—16.訪問者(Visitor)模式