设计模式-组合模式
本篇文章来自于设计模式一书中的“组合模式”
本篇中我们学习如何使用组合模式,通常在程序员开发的系统中,组件即可以是单个的对象,也可以是对象的集合。组合模式包括了这两种情况,组合就是对象的集合,其中的每个对象即可以是一个组合,也可以是简单的对象。在树的术语中,对象可以是带有其他分支的节点,也可以是叶子。
开发方面存在的问题是,对组合中的所有对象都要求具有一个简单、单一的访问接口并要求能够区分节点与叶子,这二者是相互矛盾的。节点可以有孩子并允许添加孩子,而叶子节点不允许有孩子,在某些实现中要防止对它们添加孩子节点。
我们考虑一个实际的需求,一个小公司,初期只有一个人,他当然就是CEO,尽管在初期他过于繁忙,不会考虑到这一点,接下来,他雇佣了两个人来分别管理销售和生产,很快这两个人又分别雇佣了另外一些助手,帮助做广告,运输等业务,这两个人于是成为公司的两位副总经理。随着公司的日益兴旺,公司人员持续增长,最后形成如下图所示的组织成员图。
计算薪水
如果公司盈利,公司中的每个成员都会得到一份薪水,在任何时候都会要求计算从每个员工到整个公司的控制成本。将控制成本定义为员工及其所有下属的薪水。这是一个理想的组合例子。
每个雇员的成本就是他的薪水。
领导一个部门的雇员的成本是他的薪水加上其下属的薪水。
我们希望用一个简单的接口就能正确地生成薪水总和,不管雇员是否有下属。
float GetSalaries();
Employee类
我们将公司设想为由节点构成的一个组合。使用一个类表示所有的员工是可以的,但由于每个层次的雇员有不同的属性,所有至少定义两个类(Employee类和Boss类)会更有效。Employee是叶子,他们下面没有雇员,Boss是节点,他们下面可以有雇员节点。我们先创建IEmployee类,然后从中派生出具体的Employee类。
public interface IEmployee { /// <summary> /// 获取薪水 /// </summary> /// <returns></returns> float GetSalary(); /// <summary> /// 获取名字 /// </summary> /// <returns></returns> string GetName(); /// <summary> /// 是否是叶节点 /// </summary> bool IsLeaf(); /// <summary> /// add subordinate 添加下属 /// </summary> /// <param name="name"></param> /// <param name="salary"></param> void Add(string name, float salary); /// <summary> /// 添加下属 /// </summary> /// <param name="emp"></param> void Add(IEmployee emp); /// <summary> /// 获取当前节点下的所有下属 /// </summary> /// <returns></returns> IEnumerator GetSubordinate(); /// <summary> /// 获取员工 /// </summary> /// <returns></returns> IEmployee GetChild(); /// <summary> /// 从当前节点算起的薪水总和 /// </summary> /// <returns></returns> float GetSalaries(); }
Employee类必须有add,remove,getChild和subordinates等方法的具体实现过程。由于Employee是叶子,所以,这些方法都要返回某种错误提示。GetSubordinate方法可以返回一个空值,但是,如果GetSubordinate方法返回一个空的枚举量,会使用程序更具有一致性。
public IEnumerator GetSubordinate() { return Subordinates.GetEnumerator(); }
由于Employee类的成员不能有下属,它的add和remove方法就必须产生错误提示。在调用Employee类的这些方法时就让它们抛出一个异常。
public virtual void Add(string name, float salary) { throw new Exception("这个是普通员工不能有下属!"); } public virtual void Add(IEmployee emp) { throw new Exception("这个是普通员工不能有下属!"); }
如果要得到某个管理者的雇员列表,可以直接从Subordinates列表中获取他们,同样的,可以使用Subordinates列表返回任意一个雇员及其下属的薪水总和。
public float GetSalaries() { var sum = GetSalary(); //部门领导的薪水 var enumSub = Subordinates.GetEnumerator(); while (enumSub.MoveNext()) { var esub = (IEmployee)enumSub.Current; sum += esub.GetSalaries(); } return sum; }
注意,这个方法是从当前雇员的薪水开始,每一个下属调用GetSalaries方法。当然,这是一个循环过程,任何拥有下属的雇员都会包含在内。
Boss类
Boss类是Employee的一个子类。
public class Boss : Employee { public Boss(string name, float salary) : base(name, salary) { } public override void Add(string name, float salary) { IEmployee emp = new Employee(name, salary); Subordinates.Add(emp); } public override void Add(IEmployee emp) { Subordinates.Add(emp); } }
构造Employee树
我们先创建一个CEO Employee,然后添加他的下属,再添加这些人的下属构造Employee树。
private void BuildEmployeeList() { var random = new Random(); Prez = new Boss("CEO", 200000); var markeVP = new Boss("市场营销副总裁", 100000); Prez.Add(markeVP); var salesMgr = new Boss("销售经理", 50000); var advMgr = new Boss("高级经理", 50000); markeVP.Add(salesMgr); markeVP.Add(advMgr); var prodVP = new Boss("产品副总裁", 10000); Prez.Add(prodVP); advMgr.Add("秘书", 20000); for (var i = 1; i <= 5; i++) { salesMgr.Add("销售" + i, random.Next(1000, 3000)); } var prodMgr = new Boss("产品经理", 40000); var shipMgr = new Boss("运输经理", 35000); prodVP.Add(prodMgr); prodVP.Add(shipMgr); for (int i = 1; i <= 3; i++) { var shipEmp = new Employee("运输" + i, random.Next(25000)); shipMgr.Add(shipEmp); } for (int i = 1; i <= 4; i++) { prodMgr.Add("制造" + i, random.Next(20000)); } }
一旦构造好了这个组合结构,就可以创建一个可视化的TreeView列表:先创建顶端节点,然后不断的调用AddNodes()方法,直到节点中的所有叶子都加了进去。
private void BuildTree() { var node = new EmpNode(Prez); empTree.Nodes.Add(node); AddNodes(node, Prez); empTree.ExpandAll(); } private void AddNodes(EmpNode node, IEmployee prez) { var subordinate = prez.GetSubordinate(); while (subordinate.MoveNext()) { var subordinateEmp = (IEmployee)subordinate.Current; var empNode = new EmpNode(subordinateEmp); node.Nodes.Add(empNode); if (!subordinateEmp.IsLeaf()) { AddNodes(empNode, subordinateEmp); } } }
为了简化TreeNode对象的处理,我们派生一个EmpNode类,它将一个Employee实例作为参数。
public class EmpNode : TreeNode { private IEmployee Emp { get; set; } public EmpNode(IEmployee emp) : base(emp.GetName()) { Emp = emp; } public IEmployee GetEmployee() { return Emp; } }
最终程序如下图
在这个程序的实现里,单击一个雇员,他的成本(薪水的总和)会显示在底部。
private void empTree_AfterSelect(object sender, TreeViewEventArgs e) { var node = (EmpNode)empTree.SelectedNode; GetNodeSum(node); } private void GetNodeSum(EmpNode node) { var emp = node.GetEmployee(); var sum = emp.GetSalaries(); lbSalary.Text = string.Format("雇员成本:{0}", sum); }
自我升职
我们假设有这样一种情况,一个基层雇员还保留现有的工作,但他拥有了新的下属,例如,要求一名销售员去指导销售赏,对于这种情况,比较方便的做法是:在Boxx类中提供一个方法,它将Employee转成Boss.这里另外提供一个构造函数,它将一个雇员转换成老板。
public Boss(IEmployee emp) : base(emp.GetName(), emp.GetSalary()) { }