这篇文章是 设计模式 问答(第1篇)、(第2篇)和(第3篇)的后续。在这篇文章中,我们将介绍桥接模式、组合模式、外观模式、职责链模式、代理模式以及模板模式。
如果你还没有阅读我之前的文章,请从下面开始:
桥接模式能够将实现部分和抽象部分解耦。通过它,实现发生变化并不会影响到抽象,反之亦然。看看下图。开关是抽象部分,而电子设备是实现部分。开关可以连接到任何一个电子设备,因此开关是一个抽象的概念,而设备是实现部分。
图:抽象和实现
让我们尝试对这个开关和设备进行编码。第一部分,我们把实现和抽象分成两个不同的类。图“实现”展示了我们是如何实现接口“IEquipment”的“Start()”和“Stop()”方法的。我们实现了两个设备,一个是冰箱,另一个是电灯。
图:实现
第二部分是抽象。我们例子中的开关是抽象。它有一个“SetEquipment”方法,用来设置对象;“On”方法调用设备的“Start”方法,而“Off”调用“Stop”。
图:抽象
最终,我们看看客户端代码。你可以看到我们分别创建了实现对象和抽象对象。我们可以独立地使用它们。
图:桥接的客户端代码
GOF定义:一种简单和组合对象的树形数据结构
很多时候,对象以树形结构的方式组织,开发者必须理解叶子节点和分支节点的不同含义。这会使代码更加复杂,且容易导致错误。
如下例是一个简单对象的树形结构,其中customer是根对象,它有多个address对象,而每个address对象引用了多个phone对象。
图:通用程序
现在让我们假设你需要插入一个完整的对象树。示例代码会是下面所示的样子。代码遍历所有的customer,customer内的所有address,以及address内的所有phone。当循环执行时,会调用各自的更新方法,如下面代码所示。
foreach (Customer objCust in objCustomers) { objCust.UpDateCustomer(); foreach (Address oAdd in objCust.Addresses) { oAdd.UpdateAddress(); foreach (Phone ophone in oAdd.Phones) { ophone.UpDatePhone(); } } }
上面代码的问题在于每个对象的更新方法是变化的。对customer是“UpdateCustomer”,对address是“UpdateAddress”,而对phone是“UpdatePhone”。换句话说,处理根对象和它包含的叶子节点的方式不同。这会导致疑惑,并使你的应用程序容易出错。
如果我们可以统一对待根对象和叶子节点,那么代码就可以更清晰和优雅。在下面的代码中,你可以看到我们创建了一个接口(IBusinessObject),它强制所有类(就是customer,address和phone)使用这个共同的接口。由于这个共同的接口,所有的对象现在都有一个名称为“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; } }
强制address对象也实现这个接口:
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; } }
强制最后一个节点对象Phone实现接口。
public class Phone : IBusinessObject { public void Update() {} public bool isValid() {return true;} public void Add(object o) { // no implementaton } }
定义:装饰者模式动态地顺序添加行为,帮助我们在运行状态下改变对象的行为。
我们有需要在运行时动态地顺序添加行为的情形。“顺序”是一个需要重点注意的词。例如,考虑下面饭店销售面包早餐的场景。他们有4款重要的产品,订单可以是下面的组合方式:
换句话说,根据组合方式,订单处理方式和订单成本会动态的发生变化。
图:订单组合方式
下面是一个只有面包的简单订单,它有两个函数“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 ovenn”; 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:我们已经完成了最重要的步骤,就是创建装饰者。现在我们需要创建能够动态添加到装饰者中的动态行为对象。
下面是一个简单的鸡肉订单,它可以被添加到面包订单中,从而创建出一个不同的鸡肉+面包订单。鸡肉订单从订单装饰者类继承。
对这个对象的任何调用,都先执行鸡肉订单的自定义功能,然后再调用被包装对象的功能。例如,当调用Prepare函数时,它首先调用准备鸡肉的功能,然后执行被包装对象的准备功能。(译者:这里应该是作者说反了,先执行的是被包装类的方法)。
计算费用时,也是先添加鸡肉的费用,再计算被包装类的费用,并求和。
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 chickenn”; strPrepare = strPrepare + “Stuff in the bread”; return strPrepare; } public override double CalculateCost() { return base.CalculateCost() + 300.12; } } Same way we can also prepare order drinks. 同样的方法,我们准备饮料的订单。
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 freezern”; 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 frezer Serve in glass 210.42
外观模式处于子系统集合的顶端,使他们以一种统一的方式通讯。
图:外观模式与子系统
图“订单外观”展示了这样的一个实现。为了发出一个订单,我们需要和产品,支付以及发票类交互。因此,订单成为统一了产品、支付和发票类的一个外观。
图:订单外观
图“外观模式”展示了类“clsOrder”如何统一/使用“clsProduct”,“clsPayment”以及“clsInvoice”来实现“PlaceOrder”功能。
图:外观模式
当我们有一系列的逻辑处理器,来处理一系列的执行流程时,就需要使用职责链模式。让我们来理解一下它的意思。有些情况下,一条请求会被一系列的处理器处理。第一个处理器取出请求,它可能处理一部分,也可能不做处理。一旦处理结束,它把请求传递给链条中下一个处理器。一直持续下去,直到适当的处理器接收并完成整个处理流程。
图:职责链模式概念
让我们通过一个小的案例来理解这个概念。考虑图“简单案例”,我们有一些逻辑需要处理,需要经过3个处理流程。Process 1做一些处理,并传递给Process 2;Process 2做一些类似的处理后,传递给Process 3;最后完成整个处理流程。
图:简单案例
图“职责链模式类图”中,上述3个处理类都继承自同一个抽象父类。需要指出的一个重点是,每个处理流程都指向下一个将被调用的流程。在处理类中,我们聚合了另一个处理对象,叫做“objProcess”。对象“objProcess”指向下一个处理过程,它将在当前处理完成后被调用。
图:职责链模式类图
现在,我们已经定义了类,是时候在客户端调用这些类了。因此,我们为process1,process2和process3创建了所有的处理对象。通过“setProcess”方法,我们定义了处理对象的链表。你可以看到我们把process2链接到process1后面,把process3链接到process2后面。当这个链表建立完成后,我们运行处理流程。它按照链表的顺序依次执行每个处理流程。
图:职责链模式客户端代码
代理的本质是一个指向实际包含数据类的类,扮演一个接口的角色。这里的实际数据可能是一副很大的图像,或者是一个拥有大量数据、不易被复制的对象。因此你可以创建多个代理,指向这个包含大内存的对象,并对它施加操作。这样避免了对象的赋值,因此节省了内存。代理就是指向实际对象的引用。
图“代理和实际对象”展示了如何创建一个实际类所实现的接口。因此接口“IImageProxy”形成了代理,而类“clsActualImage”的实现就是实际对象。你可以在客户端代码中看到接口是如何指向实际对象的。
图:代理和实际对象
使用代理的优势是安全,避免大型对象的复制。通过传递代理而不是实际对象,从而避免了在客户端使用实际的代码。在客户端只使用代理,确保了更好的安全性。第二点是,当我们使用大型对象时,在网络或者其他领域移动这些对象会非常地耗内存。通过移动代理而不是大型对象,我们得到了更好的性能。
模板模式是一种行为模式。模板模式定义了一个主流程的模板,这个主流程模板包含子流程,以及子流程的执行顺序。然后,可以改变主流程的子流程,从而形成不同的行为。
定义:模板模式经常应用于在派生和特殊关系中,需要创建可扩展行为的场景。
例如,下面是一个格式化数据并写入到Oracle的一个简单流程。数据可能来源于多种源头,比如文件,SQL Server等。无论数据从哪里来,总体上的通用流程是,从数据源加载数据,解析数据,然后向Oracle写入数据。
现在我们可以通过重载“Load”和“Parse”子流程的实现,来改变通用流程,创建出从CSV文件加载数据的流程,或者从SQL Server加载数据的流程。
你可以从上图中看到,我们是如何修改“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(); } } The ‘SqlServerParser’ inherits from ‘GeneralParser’ and overrides the ‘Load’ and ‘Parse’ with SQL server implementation. “SqlServerParser”从“GeneralParser”继承,并重写了“Load”和“Parse”方法,提供了基于SQL server的实现。 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”从“GeneralParser”继承,并重写了“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”); } }
在客户端,你可以这样同时调用两个parser。
FileParser ObjFileParser = new FileParser(); ObjFileParser.Process(); Console.WriteLine(“———————–”); SqlServerParser ObjSqlParser = new SqlServerParser(); ObjSqlParser.Process(); Console.Read();
下面是两个parser的输出结果。
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
如果你是设计模式的新手,或者你不愿完整的阅读这篇文章,请看我们的免费视频 设计模式培训和问答 。
原文链接: codeproject 翻译:ImportNew.com -shenggordon
译文链接:[]