设计模式常见问题解答第四部分






3.84/5 (70投票s)
桥接模式、组合模式、外观模式、责任链模式、代理模式、模板模式
更新了装饰器模式、组合模式和模板模式的解释
设计模式常见问题解答 第 4 部分
引言
这篇常见问题解答文章是设计模式常见问题解答第 1、2、3 部分的延续。在本文中,我们将尝试理解桥接模式、组合模式、外观模式、责任链模式、代理模式和模板模式。
如果您尚未阅读我之前的部分,您可以随时从下方阅读
第 1 部分设计模式常见问题解答 -- 工厂模式、抽象工厂模式、建造者模式、原型模式、单例模式和命令模式
第 2 部分设计模式常见问题解答 -- 解释器模式、迭代器模式、中介者模式、备忘录模式和观察者模式
第 3 部分设计模式常见问题解答 -- 状态模式、策略模式、访问者模式、适配器模式和享元模式
你能解释一下桥接模式吗?
桥接模式有助于将抽象与实现分离。这样,如果实现发生变化,也不会影响抽象,反之亦然。请看图“抽象与实现”。开关是抽象,电子设备是实现。开关可以应用于任何电子设备,因此开关是抽象思维,而设备是实现。
图:- 抽象与实现
让我们尝试对同一个开关和设备示例进行编码。首先,我们将实现和抽象分成两个不同的类。图“实现”显示了我们如何创建一个具有“Start()”和“Stop()”方法的“IEquipment”接口。我们实现了两种设备,一种是冰箱,另一种是灯泡。
第二部分是抽象。在我们的示例中,开关是抽象。它有一个“SetEquipment”方法来设置对象。“On”方法调用设备的“Start”方法,“off”调用“stop”。
最后,我们看客户端代码。您可以看到我们分别创建了实现对象和抽象对象。我们可以以隔离的方式使用它们。
你能解释一下组合模式吗?
GOF definition :- A tree structure of simple and composite objects
很多时候,对象以树形结构组织,开发人员必须理解叶子对象和分支对象之间的区别。这会使代码更复杂,并可能导致错误。
例如,下面是一个简单的对象树结构,其中客户是主对象,它有许多地址对象,每个地址对象引用许多电话对象。
图:- 一般流程
现在,假设您想插入完整的对象树。示例代码如下所示。代码循环遍历所有客户、客户对象中的所有地址以及地址对象中的所有电话。在循环过程中,会调用相应的更新方法,如下面的代码片段所示。
foreach (Customer objCust in objCustomers) { objCust.UpDateCustomer(); foreach (Address oAdd in objCust.Addresses) { oAdd.UpdateAddress(); } foreach (Phone ophone in oAdd.Phones) { ophone.UpDatePhone(); } }
上述代码的问题在于更新词汇量因对象而异。对于客户是“UpdateCustomer”,对于地址是“UpdateAddress”,对于电话是“UpdatePhone”。换句话说,主对象和包含的叶节点被区别对待。这可能会导致混淆,并使您的应用程序容易出错。
如果我们能够统一对待主对象和叶子对象,代码可以更清晰整洁。您可以在下面的代码中看到,我们创建了一个接口 (IBusinessObject),它强制所有类,即客户、地址和电话,使用一个公共接口。由于公共接口,所有对象现在都有一个名为“Update”的方法。
foreach (IBusinessObject ICust in objCustomers) { ICust.Update(); foreach (IBusinessObject Iaddress in ((Customer)(ICust)).ChildObjects) { Iaddress.Update(); foreach (IBusinessObject iphone in ((Address)(Iaddress)).ChildObjects) { iphone.Update(); } } }
为了实现组合模式,首先创建一个接口,如下面的代码片段所示。
public interface IBusinessObject { void Update(); bool isValid(); void Add(object o); }
将此接口强制应用于所有根对象和叶/节点对象,如下所示。
public class Customer : IBusinessObject { private List<Address> _Addresses; public IEnumerable<Address> ChildObjects { get { return (IEnumerable<Address>)_Addresses; } } public void Add(object objAdd) { _Addresses.Add((Address) objAdd); } public void Update() { } public bool isValid() { return true; } }
同样对地址对象强制执行此接口。
public class Address : IBusinessObject { private List<Phone> _Phones; public IEnumerable<Phone> ChildObjects { get { return (IEnumerable<Phone>)_Phones.ToList<object>(); } } public void Add(object objPhone) { _Phones.Add((Phone)objPhone); } public void Update() { } public bool isValid() { return true; } }
对最后一个节点对象,即电话,强制执行此接口。
public class Phone : IBusinessObject { public void Update() {} public bool isValid() {return true;} public void Add(object o) { // no implementaton } }
你能解释一下装饰器模式吗?
Punch :- Decorator pattern adds dynamically stacked behavior thus helping us to change the behavior of the object on runtime.
有时我们希望在运行时动态地为类添加堆叠行为。请注意“堆叠”一词很重要。例如,考虑下面的情况,一家酒店出售面包餐。他们有四种重要产品,订单可以使用以下组合下达:
- 普通面包。
- 面包加鸡肉。
- 面包加饮料
- 面包加鸡肉和饮料。
换句话说,订单处理行为和订单成本会根据组合类型在运行时发生变化。
图:-
下面是一个只有面包的简单订单,它有两个函数“prepare”和“calculatecost”。我们希望根据客户的需求,在运行时动态地向这个基本面包订单添加新产品。
下面是一个简单的接口,每个订单都会有,即 Prepare 和 CalculateCost。
interface IOrder { string Prepare(); double CalculateCost(); }
基本产品是实现 Iorder 接口的面包。我们希望向面包订单添加新产品并更改整个订单的行为。
public class OrderBread : IOrder { public string Prepare() { string strPrepare=""; strPrepare = "Bake the bread in oven\n"; strPrepare = strPrepare + "Serve the bread"; return strPrepare; } public double CalculateCost() { return 200.30; } }
我们可以使用装饰器模式动态地修改面包订单。实现装饰器模式是一个 5 步过程。
步骤 1:- 创建一个装饰器类,它聚合我们需要动态添加行为的对象/接口。
abstract class OrderDecorator : IOrder { protected IOrder Order; . . . . . . . . . . . . . . . }
这个装饰器类将包含对象,并且对主对象的任何方法调用将首先调用所有包含的对象,然后调用主对象。
所以,例如,如果您调用 prepare 方法,这个装饰器类将调用所有包含对象的 prepare 方法,然后是最终的 prepare 方法。您可以看到装饰器介入后输出是如何变化的。
图:-
步骤 2:- 需要初始化包含的对象/接口指针。我们可以通过多种方式进行,对于下面的示例,我们将只公开一个简单的构造函数,并通过构造函数将对象传递给它来初始化包含的对象。
abstract class OrderDecorator : IOrder { protected IOrder Order; public OrderDecorator(IOrder oOrder) { Order = oOrder; } . . . . . }
步骤 3:- 我们将实现 Iorder 接口,并通过虚拟方法调用包含的对象方法。您可以看到我们创建了调用包含对象方法的虚拟方法。
abstract class OrderDecorator : IOrder { protected IOrder Order; public OrderDecorator(IOrder oOrder) { Order = oOrder; } public virtual string Prepare() { return Order.Prepare(); } public virtual double CalculateCost() { return Order.CalculateCost(); } }
步骤 4:- 我们完成了重要的步骤,即创建装饰器。现在我们需要创建动态行为对象,可以将其添加到装饰器中以在运行时更改对象行为。
下面是一个简单的鸡肉订单,可以添加到面包订单中,创建一个完全不同的订单,称为鸡肉+面包订单。鸡肉订单是通过继承 order decorator 类创建的。
对该对象的任何调用首先调用 order chicken 的自定义功能,然后调用包含对象的功能。例如,您可以看到当调用 prepare 函数时,它首先调用 prepare chicken 的功能,然后调用包含对象的功能。
calculate cost 还添加了鸡肉成本,然后调用包含的 order cost 来计算总计。
class OrderChicken : OrderDecorator { public OrderChicken(IOrder oOrder) : base(oOrder) { } public override string Prepare() { return base.Prepare() + PrepareChicken(); } private string PrepareChicken() { string strPrepare = ""; strPrepare = "\nGrill the chicken\n"; strPrepare = strPrepare + "Stuff in the bread"; return strPrepare; } public override double CalculateCost() { return base.CalculateCost() + 300.12; } }
同样,我们也可以准备饮料订单。
class OrderDrinks : OrderDecorator { public OrderDrinks(IOrder oOrder) : base(oOrder) { } public OrderDrinks() { } public override string Prepare() { return base.Prepare() + PrepareDrinks(); } private string PrepareDrinks() { string strPrepare = ""; strPrepare = "\nTake the drink from freezer\n"; strPrepare = strPrepare + "Serve in glass"; return strPrepare; } public override double CalculateCost() { return base.CalculateCost() + 10.12; } }
步骤 5:- 最后一步是查看装饰器模式的实际应用。因此,从客户端的角度,您可以编写类似这样的代码来创建一个面包订单。
IOrder Order =new OrderBread(); Console.WriteLine(Order.Prepare()); Order.CalculateCost().ToString();
下面是上面客户端调用将显示的输出。
Order 1 :- Simple Bread menu Bake the bread in oven Serve the bread 200.3
如果您希望创建包含鸡肉、饮料和面包的订单,下面的客户端代码将有助于您实现。
Order = new OrderDrinks(new OrderChicken(new OrderBread())); Order.Prepare(); Order.CalculateCost().ToString();
对于上面的代码,下面是结合了饮料 + 鸡肉 + 面包的输出。
Order 2 :- Drinks with chicken and bread Bake the bread in oven Serve the bread Grill the chicken Stuff in the bread Take the drink from freezer Serve in glass 510.54
换句话说,您现在可以将这些行为附加到主对象上,并在运行时更改对象行为。
下面是我们可以生成的不同订单组合,从而动态地改变订单的行为。
Order 1 :- Simple Bread menu Bake the bread in oven Serve the bread 200.3 Order 2 :- Drinks with chicken and bread Bake the bread in oven Serve the bread Grill the chicken Stuff in the bread Take the drink from freezer Serve in glass 510.54 Order 3 :- Chicken with bread Bake the bread in oven Serve the bread Grill the chicken Stuff in the bread 500.42 Order 4 :- drink with simple bread Bake the bread in oven Serve the bread Take the drink from freezer Serve in glass 210.42
(A) 你能解释一下外观模式吗?
外观模式位于一组子系统之上,并允许它们以统一的方式进行通信。
图“订单外观”显示了相同内容的实际实现。为了下订单,我们需要与产品、付款和发票类进行交互。因此,订单成为外观,它统一了产品、付款和发票类。
图“外观实战”显示了类“clsorder”如何统一/使用“clsproduct”、“clsproduct”和“clsInvoice”来实现“PlaceOrder”功能。
你能解释一下责任链模式 (COR) 吗?
责任链模式用于当我们有一系列需要由一系列处理程序逻辑处理的处理过程时。让我们理解一下它的含义。在某些情况下,一个请求由一系列处理程序处理。因此,请求被第一个处理程序处理,他可以处理其中的一部分,或者不能,一旦完成,他就将其传递给链中的下一个处理程序。这个过程一直持续到合适的处理程序接手并完成处理。
让我们通过一个小示例来理解这个概念。请看图“示例”,我们有一些逻辑需要处理。因此,它将经历三个处理系列。因此,过程 1 进行一些处理并将其传递给过程 2。过程 2 进行某种处理,然后将其传递给过程 3 以完成处理活动。
图“COR 类图”显示了三个过程类继承自同一个抽象类。需要注意的一个重要点是,每个过程都指向下一个将被调用的过程。因此,在过程类中,我们聚合了另一个过程对象,称为“objProcess”。对象“ObjProcess”指向此过程完成后应调用的下一个过程。
既然我们已经定义了类,那么是时候在客户端调用类了。因此,我们创建了所有过程对象,用于 process1、process2 和 process3。使用“setProcess”方法,我们定义了过程对象的链接列表。您可以看到我们将 process2 设置为 process1 的链接列表,并将 process2 设置为 process3。一旦建立此链接列表,我们就运行过程,该过程又根据定义的链接列表运行过程。
你能解释一下代理模式吗?
代理基本上是一个类,它充当指向实际类(包含数据)的接口。这个实际数据可能是一张大图片或一个非常大的对象数据,无法复制。因此,您可以创建多个代理指向占用大量内存的对象并执行操作。这避免了对象的重复,从而节省了内存。代理是指向实际对象的引用。
图“代理与实际对象”显示了我们如何创建一个由实际类实现的接口。因此,接口“IImageProxy”形成代理,而具有实现(即“clsActualImage”类)的类形成实际对象。您可以在客户端代码中看到接口是如何指向实际对象的。
使用代理的优点是安全性以及避免复制巨大尺寸的对象。与其发送代码,不如发送代理,从而避免在客户端安装实际代码的需要。仅在客户端有代理可以确保更高的安全性。第二点是,当对象很大时,在网络或其他域中移动这些大对象可能会非常消耗内存。因此,与其移动这些大对象,不如只移动代理,从而提高性能。
你能解释一下模板模式吗?
模板模式是一种行为模式。模板模式定义了一个主过程模板,该主过程模板包含子过程以及调用子过程的顺序。之后,可以更改主过程的子过程以生成不同的行为。
Punch :- Template pattern is used in scenarios where we want to create extendable behaviors in generalization and specialization relationship.
例如,下面是一个将数据格式化并加载到 Oracle 中的简单过程。数据可以来自各种来源,如文件、SQL Server 等。无论数据来自何处,整体通用过程是从源加载数据、解析数据,然后将其转储到 Oracle。
图:- 一般流程
现在我们可以修改通用过程来创建 CSV 文件加载过程或 SQL Server 加载过程,通过覆盖“Load”和“Parse”子过程实现。
图:- 模板思维过程
您可以看到,从上图可以看出,我们修改了“Load”和“Parse”子过程来生成 CSV 文件和 SQL Server 加载过程。“Dump”函数以及调用子过程的顺序在子进程中没有改变。
为了实现模板模式,我们需要遵循 4 个重要步骤:-
- 通过创建父抽象类来创建模板或主过程。
- 通过定义抽象方法和函数来创建子过程。
- 创建一个方法来定义子过程方法将被调用的顺序。此方法应定义为普通方法,以便子方法不能覆盖它。
- 最后创建子类,它们可以修改抽象方法或子过程以定义新的实现。
public abstract class GeneralParser { protected abstract void Load(); protected abstract void Parse(); protected virtual void Dump() { Console.WriteLine("Dump data in to oracle"); } public void Process() { Load(); Parse(); Dump(); } }
“SqlServerParser”继承自“GeneralParser”,并用 SQL Server 实现覆盖了“Load”和“Parse”。
public class SqlServerParser : GeneralParser { protected override void Load() { Console.WriteLine("Connect to SQL Server"); } protected override void Parse() { Console.WriteLine("Loop through the dataset"); } }
“FileParser”继承自 General parser,并用文件特定实现覆盖了“Load”和“Parse”方法。
public class FileParser : GeneralParser { protected override void Load() { Console.WriteLine("Load the data from the file"); } protected override void Parse() { Console.WriteLine("Parse the file data"); } }
现在您可以从客户端调用这两个解析器。
FileParser ObjFileParser = new FileParser(); ObjFileParser.Process(); Console.WriteLine("-----------------------"); SqlServerParser ObjSqlParser = new SqlServerParser(); ObjSqlParser.Process(); Console.Read();
下面显示了两个解析器的输出。
Load the data from the file Parse the file data Dump data in to oracle ----------------------- Connect to SQL Server Loop through the dataset Dump data in to oracle
如需进一步阅读,请观看以下面试准备视频和分步视频系列。
- C# 设计模式分步教程
- C# 面试问答
- ASP.NET MVC 面试题及答案
- Angular 面试题及答案
- 逐步学习Azure。
- SQL Server 分步教程
- C# is vs As 关键字
- C# throw vs throw ex
- C# 并发 vs 并行
- C# 抽象类与接口
- C# 字符串是不可变的
如果您是设计模式的新手,或者真的不想阅读完整的文章,请观看我们免费的 设计模式培训和面试问题/答案 视频。
带项目的设计模式
学习设计模式的最佳方法是做一个项目。因此,本教程逐个模式地教授您,但如果您想通过项目方法学习设计模式,请 单击此链接 了解更多信息。