分公司=一部门——组合模式

分公司=一部门——组合模式

继上一篇“如果再回到从前——备忘录模式”后,本文继续讲解《大话设计模式》第19章“分公司=一部门——组合模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 分公司不就是一部门吗?

时间:5月10日19点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“大鸟,请教你一个问题?快点帮帮我。”

“今天轮到你做饭,你可别忘记了。”

“做饭?好说好说!先帮我解决问题吧。再弄不出来,我要失业了。”

“有这么严重吗?什么问题呀。”

“我们公司最近接了一个项目,是为一家在全国许多城市都有分销机构的大公司做办公管理系统,总部有人力资源、财务、运营等部门。”

“这是很常见的OA系统,需求分析好的话,应该不难开发的。”

“是呀,我开始也这么想,这家公司试用了我们开发的系统后感觉不错,他们希望可以在他们的全部分公司推广,一起使用。他们在北京有总部,在全国几大城市设有分公司,比如上海设有华东区分部,然后在一些省会城市还设有办事处,比如南京办事处、杭州办事处。现在有个问题是,总公司的人力资源部、财务部等办公管理功能在所有的分公司或办事处都需要有。你说怎么办?”

“你打算怎么办呢?”大鸟不答反问道。

“因为你之前讲过简单复制是最糟糕的设计,所以我的想法是共享功能到各个分公司,也就是让总部、分公司、办事处用同一套代码,只是根据ID的不同来区分。”

“要糟了。”

“你怎么知道,的确是不行,因为他们的要求,总部、分部和办事处是成树状结构的,也就是有组织结构的,不可以简单的平行管理。这下我就比较痛苦了,因为实际开发时就得一个一个的判断它是总部,还是分公司的财务,然后再执行其相应的方法。”

分公司=一部门——组合模式

“你有没有发现,类似的这种部分与整体情况很多见,例如卖电脑的商家,可以卖单独配件也可以卖组装整机,又如复制文件,可以一个一个文件复制粘贴还可以整个文件夹进行复制,再比如文本编辑,可以给单个字加粗、变色、改字体,当然也可以给整段文字做同样的操作。其本质都是同样的问题。”

“你是意思是,分公司或办事处与总公司的关系,就是部分与整体的关系?”

“对的,你希望总公司的组织结构,比如人力资源部、财务部的管理功能可以复用于分公司。这其实就是整体与部分可以被一致对待的问题。”

“哈,我明白了,就像你举的例子,对于Word文档里的文字,对单个字的处理和对多个字、甚至整个文档的处理,其实是一样的,用户希望一致对待,程序开发者也希望一致处理。但具体怎么做呢?”

“首先,我们来分析一下你刚才讲到的这个项目,如果把北京总公司当做一棵大树的根部的话,它的下属分公司其实就是这棵树的什么?”

“是树的分枝,哦,至于各办事处是更小的分支,而它们的相关的职能部门由于没有分枝了,所以可以理解为树叶。”

“小菜理解得很快,尽管天下没有两片相同的树叶,但同一棵树上长出来的树叶样子也不会相差到哪去。也就是说,你所希望的总部的财务部管理功能也最好是能复用到子公司,那么最好的办法就是,我们在处理总公司的财务管理功能和处理子公司的财务管理功能的方法都是一样的。”

“有点晕了,别绕弯子了,你是不是想讲一个新的设计模式给我。”

2 组合模式

“哈,小菜够直接。这个设计模式叫做‘组合模式’。”

组合模式(Composite),将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。[DP]

分公司=一部门——组合模式
组合模式(Composite)结构图

Component为组合中的对象声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理 Component的子部件。

abstract class Component
{
  protected string name;

  public Component(string name)
  {
    this.name = name;
  }

  // 通常都用Add和Remove方法来提供增加或移除树叶或树枝的功能
  public abstract void Add(Component c);
  public abstract void Remove(Component c);
  public abstract void Display(int depth);
}

Leaf在组合中表示叶节点对象,叶节点没有子节点。

class Leaf : Component
{
  public Leaf(string name) : base(name)
  {
  }

  // 由于叶子没有再增加分支和树叶,所以Add和Remove方法实现它没有意义,但这样做可以消除叶节点和枝节点对象在抽象层次的区别,它们具备安全一致的接口

  public override void Add(Component c)
  {
    Console.WriteLine("Cannot add to a leaf");
  }
  public override void Remove(Component c)
  {
    Console.WriteLine("Cannot remove from a leaf");
  }

  // 叶节点的具体方法,此处是显示其名称和级别
  public override void Display(int depth)
  {
    Console.WriteLine(new string('-', depth) + name);
  }

}

Composite定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关的操作,比如增加Add和删除 Remove.

class Composite : Component
{
  // 一个子对象集合用来存储其下属的枝节点和叶节点
  private List<Component> children = new List<Component>();

  public Composite(string name) : base(name)
  {
  }

  public override void Add(Component c)
  {
    children.Add(c);
  }

  public override void Remove(Component c)
  {
    children.Remove(c);
  }

  public override void Display(int depth)
  {
    Console.WriteLine(new string('-', depth) + name);

    foreach (var item in children)
    {
      item.Display(depth + 2);
    }
  }
}

客户端代码,能通过Component接口操作组合部件的对象。

static void Main(string[] args)
{
  // 生成树根root,根上长出两叶LeafA和LeafB
  Composite root = new Composite("root");
  root.Add(new Leaf("Leaf A"));
  root.Add(new Leaf("Leaf B"));

  // 根上长出分枝Composite X,分枝上也有两叶 LeafXA和LeafXB
  Composite comp = new Composite("Composite X");
  comp.Add(new Leaf("Leaf XA"));
  comp.Add(new Leaf("Leaf XB"));

  root.Add(comp);

  // 在Composite X上再长出分枝CompositeXY,分枝上也有两叶LeafXYA和LeafXYB
  Composite comp2 = new Composite("Composite XY");
  comp2.Add(new Leaf("Leaf XYA"));
  comp2.Add(new Leaf("Leaf XYB"));

  comp.Add(comp2);

  root.Add(new Leaf("Leaf C"));

  Leaf leaf = new Leaf("Leaf D");
  root.Add(leaf);
  root.Remove(leaf);

  root.Display(1);

  Console.Read();
}

结果显示

-root
---Leaf A
---Leaf B
---Composite X
-----Leaf XA
-----Leaf XB
-----Composite XY
-------Leaf XYA
-------Leaf XYB
---Leaf C

3 透明方式与安全方式

“树可能有无数的分枝,但只需要反复用Composite就可以实现树状结构了。小菜感觉如何?”

“有点懂,但还是有点疑问,为什么Leaf类当中也有Add和Remove,树叶不是不可以再长分枝吗?”

“是的,这种方式叫做透明方式,也就是说在Component 中声明所有用来管理子对象的方法,其中包括Add、Remove等。这样实现Component接口的所有子类都具备了Add和Remove。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为Leaf类本身不具备Add)、RemoveO方法的功能,所以实现它是没有意义的。

“哦,那么如果我不希望做这样的无用功呢?也就是Leaf类当中不用Add和Remove方法,可以吗?”

“当然是可以,那么就需要安全方式,也就是在Component接口中不去声明Add和Remove方法,那么子类的Leaf也就不需要去实现它,而是在Composite声明所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类将不具有相同的接口,客户端的调用需要做相应的判断,带来了不便。

“那我喜欢透明式,那样就不用做任何判断了。”

“开发怎么能随便有倾向性?两者各有好处,视情况而定吧。”

4 何时使用组合模式

“什么地方用组合模式比较好呢?”

“当你发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。

“哦,我想起来了。以前曾经用过的ASP.NET的TreeView控件就是典型的组合模式应用。”

“又何止是这个,你应该写过自定义控件吧,也就是把一些基本的控件组合起来,通过编程写成一个定制的控件,比如用两个文本框和一个按钮就可以写一下自定义的登录框控件,实际上,所有的Web控件的基类都是System.Web.UI.Control,而Control基类中就有Add和Remove方法,这就是典型的组合模式的应用。”

“哦,对的对的,这就是部分与整体的关系。”

“好了,你是不是可以把你提到的公司管理系统的例子练习一下了?”

“OK,现在感觉不是很困难了。”

5 公司管理系统

半小时后,小菜写出了代码。

分公司=一部门——组合模式
代码结构图

公司类 抽象类或接口

abstract class Company
{
  protected string name;

  public Company(string name)
  {
    this.name = name;
  }

  public abstract void Add(Company c);        // 增加
  public abstract void Remove(Company c);     // 移除
  public abstract void Display(int depth);    // 显示
  public abstract void LineOfDuty();   // 发行职责(不同的部门需履行不同的职责)
}

具体公司类 实现接口 树枝节点

/// <summary>
/// 具体公司,树枝节点
/// </summary>
class ConcreteCompany : Company
{
  private List<Company> children = new List<Company>();

  public ConcreteCompany(string name)
    : base(name)
  {
  }

  public override void Add(Company c)
  {
    children.Add(c);
  }

  public override void Remove(Company c)
  {
    children.Remove(c);
  }

  public override void Display(int depth)
  {
    Console.WriteLine(new string('-', depth) + name);

    foreach (var component in children)
    {
      component.Display(depth + 2);
    }
  }

  // 履行职责
  public override void LineOfDuty()
  {
    foreach (var component in children)
    {
      component.LineOfDuty();
    }
  }
}

人力资源部与财务部类 树叶节点

// 人力资源部
class HRDepartment : Company
{
  public HRDepartment(string name) : base(name)
  {
  }

  public override void Add(Company c)
  {
  }

  public override void Remove(Company c)
  {
  }

  public override void Display(int depth)
  {
    Console.WriteLine(new string('-', depth) + name);
  }

  public override void LineOfDuty()
  {
    Console.WriteLine($"{name}员工招聘培训管理");
  }
}

// 财务部
class FinanceDepartment : Company
{
  public FinanceDepartment(string name) : base(name)
  {
  }

  public override void Add(Company c)
  {
  }

  public override void Remove(Company c)
  {
  }

  public override void Display(int depth)
  {
    Console.WriteLine(new string('-', depth) + name);
  }

  public override void LineOfDuty()
  {
    Console.WriteLine($"{name}公司财务收支管理");
  }
}

客户端调用

static void Main(string[] args)
{
  ConcreteCompany root = new ConcreteCompany("北京总公司");
  root.Add(new HRDepartment("总公司人力资源部"));
  root.Add(new FinanceDepartment("总公司财务部"));

  ConcreteCompany comp = new ConcreteCompany("上海华东分公司");
  comp.Add(new HRDepartment("华东分公司人力资源部"));
  comp.Add(new FinanceDepartment("华东分公司财务部"));

  root.Add(comp);

  ConcreteCompany comp1 = new ConcreteCompany("南京办事处");
  comp.Add(new HRDepartment("南京办事处人力资源部"));
  comp.Add(new FinanceDepartment("南京办事处财务部"));

  root.Add(comp1);

  ConcreteCompany comp2 = new ConcreteCompany("杭州办事处");
  comp.Add(new HRDepartment("杭州办事处人力资源部"));
  comp.Add(new FinanceDepartment("杭州办事处财务部"));

  root.Add(comp2);

  Console.WriteLine("\n结构图:");
  root.Display(1);

  Console.WriteLine("\n职责:");
  root.LineOfDuty();

  Console.Read();
}

结果显示

结构图:
-北京总公司
---总公司人力资源部
---总公司财务部
---上海华东分公司
-----华东分公司人力资源部
-----华东分公司财务部
-----南京办事处人力资源部
-----南京办事处财务部
-----杭州办事处人力资源部
-----杭州办事处财务部
---南京办事处
---杭州办事处

职责:
总公司人力资源部员工招聘培训管理
总公司财务部公司财务收支管理
华东分公司人力资源部员工招聘培训管理
华东分公司财务部公司财务收支管理
南京办事处人力资源部员工招聘培训管理
南京办事处财务部公司财务收支管理
杭州办事处人力资源部员工招聘培训管理
杭州办事处财务部公司财务收支管理

6 组合模式好处

“小菜写得不错,你想想看,这样写的好处有哪些?”

组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象了。”

“非常好,还有没有?”

“我感觉用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。

“简单点说,就是组合模式让客户可以一致地使用组合结构和单个对象。”

“这也就是说,那家公司开多少个以及多少级办事处都没问题了。”小菜开始兴奋起来同,“哪怕开到地级市、县级市、镇、乡、村、户……”

“喂,发什么神经了。”大鸟提醒道,“开办事处到户?你有毛病呀。”

“不过理论上,用了组合模式,在每家每户设置一个人力资源部和财务部也是很正常的。”小菜得意地说,“哪家不需要婚丧嫁娶、增丁添口等家务事,哪家不需要柴米油盐、衣食住行等流水账。”

“你小子,刚才还在为项目设计不好而犯愁叫失业,现在可好,得意得恨不得全国挨家挨户用你那套软件,瞧你那德行。”

“我就这德行,学到东西,水平当然就不同了。我去考虑真实的设计了。”

“小菜,今天该轮到你烧饭做菜,别想逃。”

“不是还有点剩饭吗?”

“没有菜如何吃呀。”

“大鸟呀,用组合模式呀,如你所说,客户是不用关心吃什么,直接吃米饭或者吃饭菜组合,其效果对客户来说都是填饱肚子,你就将就一下吧。”小菜说完,就逃离了大鸟房间。

“啊,有这样应用组合模式的?你给我回来。”大鸟叫道。


每天学一点,不贪多。

下一篇我们接着读“第20章 想走?可以!先买票——迭代器模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论