理解和实现组合模式的绝对初学者教程(C#)






4.95/5 (14投票s)
在本文中,我们将尝试理解什么是组合设计模式。
引言
本文的目的是理解组合模式的基础,并尝试看看它在何时可能有用。我们还将通过一个玩具应用程序来使用组合模式,以更好地理解该模式。本文中的示例代码主要用于说明此模式,并且不会是现实世界的例子。本文的重点仅在于说明组合模式的概念。
背景
在我们的应用程序中,我们经常会遇到需要树形数据结构的情况,在这种情况下,我们需要以无缝的方式处理集合的集合。如果我们有一个嵌套集合的场景,即树形数据结构,并且调用代码需要以这样的方式处理该数据结构,即处理树中包含的单个项目(单个项目或包含多个项目的树节点,即组合)时,不应有任何区别。
我们可以想到的一个经典例子是我们 UI 控件。每个控件都是一个 UIControl
,无论是表单还是按钮。但是,一个表单可以包含其他控件,如按钮和文本框。因此,表单是一个组合控件(UIControl
类型),其中包含许多其他控件(UIControl
类型)。如果我们拖动一个 Panel 控件到表单上,该 Panel 控件本身可以包含其他控件,表单现在就包含 Panel 控件。所以,所有这些对象都是 UIControl
类型,但有些对象是独立的,比如按钮,有些对象实际上包含其他控件。但是,从渲染和事件处理的角度来看,所有这些控件都被视为相同。这意味着用于渲染和事件处理的代码以相同的方式处理组合对象(即包含其他控件的表单)和按钮控件。
总之,每当我们希望调用代码(客户端)以相同的方式处理对象组合和单个对象时,都应该使用组合模式。
在通过示例进一步理解此模式之前,让我们先看看“四人组”对该模式的定义和类图。
“将对象组合成树形结构以表示部分-整体层次结构。组合允许客户端统一地处理单个对象和对象组合”
在理解图中的每个组件之前,我想明确指出(从面向对象的角度)几点。
Composite
是一个Component
。Leaf
是一个Component
。Composite
包含Components
。
让我们尝试理解此类图的每个组件。
Component
:这是组合中对象的契约/接口。在某些情况下,它还可以实现派生类(即 Composite 和 Leaf)共有的默认行为。Leaf
:这是不包含任何 Component 类型对象的对象,即没有子对象。Composite
:此类包含其他 Component 作为子对象,并定义了层次结构中所有 Composite 类型所需的契约/接口。Client
:以完全相同的方式使用 Leaf 和 Composite 类型对象。
使用代码
为了更好地理解这一点,让我们尝试实现一个简单的应用程序,其中我们将看到该模式如何帮助我们以无缝的方式管理嵌套集合和树形结构中的数据。
注意:本示例的重点是说明组合模式以及使用它如何使客户端中的调用代码更加简洁。在代码质量和某些设计选择上做了一些妥协。请忽略这部分,因为重点仅在于模式。我们将随时在代码中提及这些改进点。
因此,为了说明此模式,让我们考虑一个场景:我们有一个组织,该组织允许员工订阅各种类型的培训订阅。我们将创建的一个小型模块是,对于给定员工,找出此情况对组织的成本影响。如果我们看一个典型的组织,会有员工和经理。经理也有经理。因此,组织结构是树形的。
客户端将尝试检索员工的订阅成本。如果该员工不是经理,成本仅限于他自己的订阅;但如果他是经理,成本将包括他自己的订阅以及他的团队成员的订阅成本。让我们尝试处理这个问题陈述,看看如何利用组合模式来设计解决方案,以便客户端能够以相同的方式检索订阅成本,而不管该员工是经理还是非经理。
在看代码之前,让我们可视化建议的解决方案。
让我们从创建一个简单的枚举 SubscriptionType
和一个类 Subscription
开始,该类将跟踪订阅的类型,以及一个类来存储订阅详细信息。
enum SubscriptionType
{
Print,
Portal,
Training
}
class Subscription
{
public SubscriptionType SubscriptionType { get; set; }
public string Name { get; set; }
public float Cost { get; set; }
}
改进说明:这不是设计此类最佳方式,但我们为了代码简单,只关注与此模式相关的部分。目前,此类将帮助我们封装订阅信息。
现在我们有了订阅类,让我们尝试创建模式的 Component
部分。为此,让我们创建一个 IEmployee
类型的接口。
interface IEmployee
{
string Name { get; set; }
int EmployeeId { get; set; }
List<subscription> Subscriptions { get; set; }
float GetCost();
int GetSubscriptionCount(SubscriptionType type);
}
该接口提供了所有 Leaf
和 Composite
类型应实现的契约。它跟踪 Employee
特定的属性和订阅列表。它还具有检索给定员工订阅信息的 C# 方法。
现在让我们看看我们的 Leaf 类,即 Employee
。
class Employee : IEmployee
{
public string Name { get; set; }
public int EmployeeId { get; set; }
public List<subscription> Subscriptions { get; set; }
public float GetCost()
{
if (Subscriptions == null)
{
return 0;
}
float cost = Subscriptions.Select((item => item.Cost)).Sum();
return cost;
}
public int GetSubscriptionCount(SubscriptionType type)
{
if (Subscriptions == null)
{
return 0;
}
int count = Subscriptions.Count(item => item.SubscriptionType == type);
return count;
}
}
改进说明:此类具有与我们 IEmployee
接口相同的成员。实现此目的的一种方法是拥有一个抽象类而不是接口,其中抽象类可以提供 GetCost
和 GetSubscriptionCount
方法的默认实现。
现在我们有了 Employee
类,即 Leaf
节点,让我们看看如何创建 Manager
类。
class Manager : IEmployee
{
public string Name { get; set; }
public int EmployeeId { get; set; }
public List<subscription> Subscriptions { get; set; }
public List<iemployee> TeamMembers { get; set; }
public float GetCost()
{
float subsCost = 0;
if (Subscriptions != null)
{
subsCost = Subscriptions.Select((item => item.Cost)).Sum();
}
float membersCost = 0;
if (TeamMembers != null)
{
membersCost = TeamMembers.Select(item => item.GetCost()).Sum();
}
return subsCost + membersCost;
}
public int GetSubscriptionCount(SubscriptionType type)
{
int subCount = 0;
if (Subscriptions != null)
{
subCount = Subscriptions.Count(item => item.SubscriptionType == type);
}
int membersSubCount = 0;
if (TeamMembers != null)
{
membersSubCount = TeamMembers.Select(item => item.GetSubscriptionCount(type)).Sum();
}
return subCount + membersSubCount;
}
}
如果我们查看 Manager
类,我们可以看到它是一个组合,其中包含一个 IEmployees
集合。此外,GetCost
和 GetSubscriptionCount
方法在此类中被重写,以根据 Manager
自己的订阅及其团队成员 Employee
的订阅来处理成本计算。
这段代码的美妙之处在于,调用者将使用相同的逻辑来从 Manager
和 Employee
对象获取订阅成本。对组合内部子节点的处理完全封装在 Composite
对象本身内部。
改进说明:此处显示的 C# 代码仅用于说明组合模式,因为我们可以清楚地看到 Manager 和 Employee 之间存在“is-a
”关系。那么为什么我们要将它们建模为单独的类呢?这只是为了说明模式本身。在理想情况下,Employee
应该是 Manager
的基类。或者,更好的是,可以使用单个 Employee 类,并使用状态模式来识别当前对象是 Employee
还是 Manager
。
现在我们有了可以使用的类。让我们看看客户端代码如何使用这些类来计算成本。
注意:上面的客户端代码之所以显得混乱且冗长,仅仅是因为需要设置订阅和员工类。首先,我将发布完整的客户端代码,然后我将展示如何使用相同的逻辑来显示成本信息,而不管对象是 Employee
还是 Manager
类型。
class Program
{
static void Main(string[] args)
{
// Now let us create some Employees and assign some subscriptions to them
IEmployee emp1 = new Employee { Name = "A", EmployeeId = 1, Subscriptions = new List<subscription> { GetPluralSightSubscription(), GetLyndaSubscription(), GetMSDNSubscription(), GetTrainingSubscription() } };
IEmployee emp2 = new Employee { Name = "B", EmployeeId = 2, Subscriptions = new List<subscription> { GetPluralSightSubscription(), GetLyndaSubscription() } };
IEmployee emp3 = new Employee { Name = "C", EmployeeId = 3, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp4 = new Employee { Name = "D", EmployeeId = 4, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp5 = new Employee { Name = "E", EmployeeId = 5, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp6 = new Employee { Name = "F", EmployeeId = 6, Subscriptions = new List<subscription> { GetPluralSightSubscription(), GetTrainingSubscription() } };
IEmployee emp7 = new Employee { Name = "G", EmployeeId = 7, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp8 = new Employee { Name = "H", EmployeeId = 8, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp9 = new Employee { Name = "I", EmployeeId = 9, Subscriptions = new List<subscription> { GetPluralSightSubscription() } };
IEmployee emp10 = new Employee { Name = "J", EmployeeId = 10, Subscriptions = new List<subscription> { GetPluralSightSubscription(), GetMSDNSubscription() } };
// lets get cost details of single employee
Console.WriteLine("Lets get cost details of single employee");
PrintCostDetails(emp1);
// Lets check cost details of list of employees
List<iemployee> employees = new List<iemployee> { emp1, emp2, emp3, emp4, emp5, emp6, emp7, emp8, emp9, emp10 };
Console.WriteLine("Lets check cost details of list of employees");
foreach (var item in employees)
{
PrintCostDetails(item);
}
// Now lets setup some managers
IEmployee mng1 = new Manager
{
Name = "MA",
EmployeeId = 11,
Subscriptions = new List<subscription>
{
GetPluralSightSubscription(),
GetLyndaSubscription(),
GetMSDNSubscription(),
GetTrainingSubscription()
},
TeamMembers = new List<iemployee>
{
emp1,
emp2,
emp3,
}
};
IEmployee mng2 = new Manager
{
Name = "MB",
EmployeeId = 13,
Subscriptions = new List<subscription>
{
GetPluralSightSubscription(),
},
TeamMembers = new List<iemployee>
{
emp4,
emp5,
emp6,
}
};
IEmployee mng3 = new Manager
{
Name = "MC",
EmployeeId = 13,
Subscriptions = new List<subscription>
{
GetTrainingSubscription()
},
TeamMembers = new List<iemployee>
{
emp7,
emp8,
emp9,
emp10,
}
};
// lets get cost details of single manager
Console.WriteLine("Lets get cost details of single manager");
PrintCostDetails(mng1);
// Lets check cost details of list of employees
Console.WriteLine("Lets check cost details of list of manager");
foreach (var item in new List<iemployee>{mng1, mng2, mng3})
{
PrintCostDetails(item);
}
Console.ReadLine();
}
private static void PrintCostDetails(IEmployee item)
{
Console.WriteLine("Cost: {0}, Count Portal: {1}, Count Print: {2}, Count Training: {3}, Emp ID: {4}",
item.GetCost(),
item.GetSubscriptionCount(SubscriptionType.Portal),
item.GetSubscriptionCount(SubscriptionType.Print),
item.GetSubscriptionCount(SubscriptionType.Training),
item.EmployeeId);
}
#region Subscriptions gets
// This region has cummy methods to add subscritions to employees
// This is just for demo, in real world applications should never have such code
private static Subscription GetPluralSightSubscription()
{
Subscription sub = new Subscription
{
Name = "Pluralsight",
SubscriptionType = SubscriptionType.Portal,
Cost = 30
};
return sub;
}
private static Subscription GetLyndaSubscription()
{
Subscription sub = new Subscription
{
Name = "Lynda.com",
SubscriptionType = SubscriptionType.Portal,
Cost = 20
};
return sub;
}
private static Subscription GetMSDNSubscription()
{
Subscription sub = new Subscription
{
Name = "MSDN Magazine",
SubscriptionType = SubscriptionType.Print,
Cost = 10
};
return sub;
}
private static Subscription GetTrainingSubscription()
{
Subscription sub = new Subscription
{
Name = "Patterns Training",
SubscriptionType = SubscriptionType.Training,
Cost = 300
};
return sub;
}
#endregion
}
上面显示的大部分代码都涉及设置对象以及连接对象以将订阅分配给员工并创建员工经理关系。但我们感兴趣的代码封装在 PrintCostDetails
函数中。
private static void PrintCostDetails(IEmployee item)
{
Console.WriteLine("Cost: {0}, Count Portal: {1}, Count Print: {2}, Count Training: {3}, Emp ID: {4}",
item.GetCost(),
item.GetSubscriptionCount(SubscriptionType.Portal),
item.GetSubscriptionCount(SubscriptionType.Print),
item.GetSubscriptionCount(SubscriptionType.Training),
item.EmployeeId);
}
我们将看到这段代码如何保持不变,它将无缝地打印 Employee
和 Manager
的订阅成本。在内部,我们的 Composite
类将处理与组合对象相关的所有业务逻辑。让我们运行代码,看看输出。
在这个示例中,我们只展示了 1 级层次结构,但该设计已准备好处理 N 级层次结构。Composite
类和 Client
类需要稍作更改,我们就可以使用此代码处理任何级别的层次结构。
看点
在本文中,我们探讨了组合模式的基础知识以及如何使用此模式有效地管理树形结构和嵌套集合中的树形数据。我们使用了一个相当牵强的例子来演示此模式。本文是从绝对初学者的角度编写的。希望这能提供一些信息。
历史
- 2017年7月21日:初稿