设计模式详解 - 入门指南
带有示例场景和代码的设计模式详细讨论
在上一篇文章中,我提供了设计模式和相关术语的概述... 除了承诺写另一篇文章(就是这篇)来解释标准设计模式以及示例场景和代码。
注意:本文的其余部分将从开发者的角度,通过示例解释一些标准实现设计模式。示例将使用C#代码... 我相信VB / Java / PHP或其他编程语言的读者也能理解这些示例(如果您希望将您喜欢的语言的翻译代码添加到本文,请随时与我联系)。
引言
设计模式(或者更具体地说,实现设计模式),在早期阶段,只是一组常用的代码块,开发者或应用程序设计者之间经常在没有特定术语的情况下进行交流。
那时,有四个人写了一本书,专门介绍这些常用的实现设计代码块,并使“设计模式”这个术语/词语流行起来。这四个人被称为“四人帮”。
之所以再次提及这段历史,很可能是因为您可能已经读过/听过100次了,那就是,本文将讨论的模式将来自“四人帮”的设计模式列表。
多年来,许多新的设计模式变得流行起来,要么是全新的模式,要么是四人书中提到的标准模式的变体。换句话说,本文无法讨论设计模式的完整列表。
了解设计模式
几乎所有的模式都可以被认为是面向对象编程的良好范例……这意味着实现设计模式需要良好的面向对象编程知识/经验。
接口和类
在面向对象编程中,我们学过类可以实现接口……而类最好这样做,因为接口提供了类应该在其代码中实现的结构/方法,就像一本规则手册。
这样,类的开发者就不会遗漏什么,而接口的创建者则有了一个结构化的机制,让类开发者遵守规则。
这些规则/接口可以成为许多类在同一库或应用程序中实现的通用规则。
与定义抽象的abstract
类类似,类可以继承并应用。
大多数设计模式将使用接口、抽象类和类来实现抽象、分离和规则。我们将看到更多。
一个实现模式
示例
interface ITelephone
{
void MakeCall(string PhoneNumber);
void Receivecall();
}
class Phone : ITelephone
{
public void MakeCall(string PhoneNumber)
{
//some implementation to make a call
}
public void Receivecall()
{
//some implementation to receive a call
}
}
在上面的示例中,ITelephone
接口定义了一个规则……实现类Phone
使用该规则。
现在,深入研究一下,当我们拥有更多类和接口时,构建类和接口的真正优势才能显现出来……如下所示
public interface ITelephone
{
void MakeCall(string PhoneNumber);
void Receivecall();
}
public interface ISMSDevice
{
void SendSms(string PhoneNumber, string Message);
void ReceiveSms();
}
public interface IMobileDevice
{
void SetDateTime();
void ChangeTimeZone();
void SetReminderAlert();
void GetContacts();
void AddContact();
}
class Phone : ITelephone
{
public void MakeCall(string PhoneNumber)
{
//some implementation to make a call
}
public void Receivecall()
{
//some implementation to receive a call
}
}
class MobilePhone : ITelephone, ISMSDevice
{
public void MakeCall(string PhoneNumber)
{
//some implementation to make a call
}
public void Receivecall()
{
//some implementation to receive a call
}
public void SendSms(string PhoneNumber, string Message)
{
// implementation
}
public void ReceiveSms()
{
// implementation
}
}
上面的代码显示了接口如何定义不同的规则,而类如何实现它们。
规则(由接口提供)和创建过程(由类提供)是隔离的……这样,相同的创建过程可以用来创建对象的各种表示……例如,手机和移动电话都实现了电话的核心功能。
上面的代码实现称为建造者模式……因为它为创建构建块奠定了一个模式。
使用建造者模式
//The usage class
public class PhoneServices
{
public void CallNumber(ITelephone Device)
{
Device.MakeCall("9278349082");
}
}
//The create and use class
class Myclass
{
public void RingAContact()
{
PhoneServices ph1 = new PhoneServices();
ITelephone phone = new MobilePhone();
ph1.CallNumber(phone);
}
}
PhoneServices
类实现了逻辑,可以在不同对象(Phone
或MobilePhone
实例,它们都是ITelephone
设备)上执行操作(如CallNumber()
),而无需编写重复的代码,因为这些对象实现了通用接口(ITelephone
)。
我将把PhoneServices
类称为“Usage
”类,因为它不实现设计,而只是使用它。这样,一个Client
类(我将称之为“创建和使用”类),MyClass
,可以创建Phone/Mobile Phone的实例并调用CallNumber()
方法。- 如果将来需要创建一种新型手机,例如PDA,那么可以开发相关的接口和实现……PDA类可以实现现有和新接口,而无需修改现有代码。
难道不应该尽量简单地编码,不使用太多接口/类,这样就不用费心维护所有这些东西了吗?
建造者模式或任何其他设计模式的想法,就是面向对象编程本身的理念。
这就像将人体分解为骨骼和肌肉……这样可以帮助不同的开发团队负责实现中的每个部分,例如制作骨骼(接口),然后将其与包裹在骨骼外的肌肉(类)分开……这样肌肉团队就不必了解人体的所有结构,而只需遵循骨骼中的连接点(接口定义)并添加肌肉。
换句话说,骨骼(接口)团队不会关心肌肉团队做了什么实现(类)……它只说明了必需的结构(接口定义)。
面向对象编程
当我们构建像人体这样复杂的东西时,我们不能期望团队中的每个人都能理解一切。从需求、设计到实现和测试,都需要进行拆分。
开发必须模块化(作为对象),以保持紧凑并划分人员的任务,让他们专注于正在开发的每一个细节。
还可以进一步设立血管团队、神经团队等,他们只关心自己范围内的设计……最终,一个创建者团队可以实现代码,该代码使用所有这些设计块的实例来构建完整的人体。创建者团队在技术上充当所有构建块的用户。
适配
让我们做一些粗略但有趣的现实场景假设。
假设我们发布了一个包含建造者模式示例代码的产品……许多公司购买了该产品并将其集成到真实的手机和移动设备中,以允许设备拨打/接听电话、发送短信等。
现在,一位笔记本电脑制造商请求我们提供软件,允许笔记本电脑通过其内置硬件提供电话服务……但笔记本电脑的硬件提供与手机或移动设备不同的通信端口或机制。我们希望使我们的库在此处有用,同时不更改现有代码,因为我们已经将相同的电话库卖给了手机公司。
我们通常会创建一个适配器类……该类将电话呼叫转换为基于笔记本电脑的通信……如下所示。
// The class in a laptop's library which does laptop communications
public class Laptop
{
public void OpenVoiceSocket()
{
}
public void SendVoiceData()
{
}
}
// The adaptor class that is added to our library in a separate class file.
public class LaptopAdaptor : ITelephone
{
private Laptop computer1 = new Laptop();
public void MakeCall(string number)
{
computer1.OpenVoiceSocket();
computer1.SendVoiceData();
}
public void Receivecall()
{
}
}
如上所示,这两个类可以属于完全不同的库,由不同的公司发布,一个由您(LaptopAdaptor
)发布,另一个由笔记本电脑公司发布。
适配是在LaptopAdaptor
类中完成的,该类将作为新类添加到我们的电话库中,而不会干扰现有代码。LaptopAdaptor
类通过调用笔记本电脑的通信方法,将电话操作转换为笔记本电脑的通信操作。
这称为适配器模式。
拆分实现并添加桥梁
现在,让我们以不同的方式更改一些旧代码。如果我们更改了之前的电话示例,使其如下所示……保留ITelephone
和ISmsDevice
接口,并重写所有其他内容。
public class GenericPhone : ITelephone
{
public void MakeCall(string PhoneNumber)
{
//implementation for making a call
}
public void Receivecall()
{
//implementation for receiving a call
}
}
public class GenericSMS : ISMSDevice
{
public void SendSms(string PhoneNumber, string Message)
{
}
public void ReceiveSms()
{
}
}
public interface IPhone
{
void MakePhoneCall(string PhoneNumber);
void SendMessage(string PhoneNumber, string Message);
}
public class MobilePhone : IPhone
{
private GenericPhone phone;
private GenericSMS smsDevice;
public MobilePhone(GenericPhone DeviceHandle1, GenericSMS DeviceHandle2)
{
this.phone = DeviceHandle1;
this.smsDevice = DeviceHandle2;
}
public void MakePhoneCall(string PhoneNumber)
{
phone.MakeCall(PhoneNumber);
}
public void SendMessage(string PhoneNumber, string Message)
{
smsDevice.SendSms(PhoneNumber, Message);
}
}
// Client code
static void MakePhoneCall()
{
IPhone userPhone = new MobilePhone(new GenericPhone(), new GenericSMS());
userPhone.MakePhoneCall("234234");
}
我们得出了不同的实现。在建造者模式中,我们将所有实现逻辑都保留在Phone
和MobilePhone
类中……实际上,MakeCall
和ReceiveCall
的实现是重复的。对于某些设计来说,这种重复是可以接受的。
为了避免重复并在一个地方轻松管理更改,我们可以将实现拆分到多个层中,如上面的代码所示……这表明如何将MakeCall()
的实现放在一个地方。但这只在项目很大且有很多操作需要abstract
时才需要。在小型项目中,过多的抽象层会使代码复杂且维护成本高。
上面的代码将一部分实现(例如,MakeCall
、SendSms
方法)分离到另一组类(GenericPhone
和GenericSmsDevice
)中。这不仅展示了设计规则和实现的隔离,还将实现部分分离为核心实现和操作实现。
现在,操作代码(例如MobilePhone
)与实现代码(例如GenericPhone
)脱钩……最后,通过在抽象的两侧建立一个联系,连接操作和实现的桥梁,上面的代码就成为了桥梁模式的一个示例。
桥梁模式与建造者模式相比的主要优点是,它将核心实现的更改与操作代码(桥梁类)隔离开来,避免了相互影响。
有一天,如果拨打电话的实现发生变化,或者需要进行一些额外的工作,例如在拨打电话之前格式化电话号码来修复错误,那么就不需要触及桥梁类……只需要修改和测试核心实现类。
创建中介
假设我们有一个场景,我们的公司还有一个团队开发了传真消息库和电子邮件库……我们想将它们整合起来,最终构建一个完整的通信系统。
假设不同的消息可以到达通信块实现中的同一个处理模块,然后根据消息是电子邮件请求、SMS请求还是传真请求,该模块会适当地处理/发送消息。
那么,我们可以将处理此的中心代码块称为中介……而整个系统则拥有中介模式……其他块,如电话模块、传真模块,可以采用不同的模式开发。
场景假设就到这里……它有点像下面的代码,假设FaxDevice
是一个在项目中引用的另一个库中的类实现。
public class Communicator
{
public void SendMessage(string Message, string PhoneNumber, string Mode)
{
switch (Mode.ToUpper())
{
case "FAX":
FaxDevice faxer1 = new FaxDevice();
faxer1.SendFax(Message, PhoneNumber);
break;
case "SMS":
MobilePhone phone1 = new MobilePhone();
phone1.SendMessage(PhoneNumber, Message);
}
}
}
// Usage code
Communicator commChannel = new Communicator();
commChannel.SendMessage("Fire in Deck 01A! ", "+11232342323452", "EMAIL");
上面的使用代码可以作为某些模块或库的一部分,这些模块或库不知道MobilePhone
类或FaxDevice
类的存在……但仍然可以通过中介在设备上发送消息。
中介模式在集成两个或多个系统,或者实现块(B1、B2、…Bn)时最有用,这些系统或块彼此独立,无法互相调用或感知对方。
例如,集成第三方库,或协调遗留系统与新系统代码等。
对不同的模式感到困惑?
如何记住哪个模式应该用接口实现,哪个应该用抽象类实现?
您注意到我在上面的示例中使用了switch
语句来实现中介模式吗?这是否使其更像拦截过滤器模式而不是中介模式?
好吧,这就是我喜欢设计模式的地方……
您实现的任何设计模式都可能包含其他模式……例如,您可以使用桥梁来实现中介模式,或使用适配器来实现桥梁等。
这取决于您的代码……没有规定建造者模式必须使用接口(您可以使用abstract
类代替接口),也没有规定您不应该使用拦截过滤器来实现您的中介。
用本文或其他地方找到的相同类型的代码来实现一个模式将不会很有趣……但当您理解一个中介模式是通过编写一个独立的层来调节两个独立块之间的通信时,并且桥梁模式是通过将操作代码(或执行工作的操作调用)与实际实现(实际实现代码)以及两边的抽象分离开来时,这会很有趣……就是这样。
我将在这里留下的模式和示例,但“四人帮”书籍以及软件社区中还有更多的设计模式。
不同的模式类别
还有一件事我们之前没有讨论。它们是模式类别。
一般来说,模式被分为三个类别……创建型、结构型和行为型模式。
我们讨论了建造者模式,它是一种创建型模式,因为它的逻辑更侧重于为构建块设置创建规则,而不将事物分层。
我们讨论了适配器模式和桥梁模式,它们属于结构型模式,因为它们将事物分层,从而为共享实现的类添加了结构。
我们讨论了中介模式,它是一种行为型模式,因为它更注重为一个类实现中介行为。
结论
还有许多其他模式被进一步归入不同类别,以便在决定何时使用哪种模式时更加轻松。
但是,会存在实现结构设计模式的代码,看起来像是使用了行为模式,等等。混合搭配模式来实现需求通常是必要的。
本文缺少许多图形表示,使用UML和块图,这本来可以让它更清晰、更美观。我将尽快添加它们。同时,您可以使用以下一些有趣的参考链接来获得更清晰的了解。
希望本文对您有所帮助……请留下您的评论、想法和反馈。
参考链接
- DoFactory: http://www.dofactory.com/Patterns/Patterns.aspx
- 最流行模式的快速描述:http://geekswithblogs.net/subodhnpushpak/archive/2009/09/18/the-23-gang-of-four-design-patterns-.-revisited.aspx
- 建造者模式: http://www.csharp-station.com/Articles/BuilderPattern.aspx
- 适配器模式: http://www.dotnetheaven.com/Uploadfile/rajeshvs/
AdapterPatternInCS02012006034414AM/AdapterPatternInCS.aspx - 桥梁模式: http://en.wikipedia.org/wiki/Bridge_pattern
- 中介模式: http://en.wikipedia.org/wiki/Mediator_pattern
历史
- 2010年11月17日:初始发布