什么是设计模式
在软件开发的世界里,许多领域中的问题具有相似的特性。就像造房子一样,不管要盖一座购物中心,还是要盖一座假日酒店,它们之间都有基本上相似的工作步骤,都需要搭建梁柱,铺置房顶等工作。在做这些工作时都需要遵循某种特殊的技术要求,以使得房子的结构、承受能力达到合理,这些规则是前人经过精确的计算和失败的教训得来的。对于软件开发也一样,如果我们希望编写出来健壮、灵活的应用程序,也有必要进行精细的设计,并且可以通过遵循某种规则以达到这个目标。这些规则或者称之为技巧就是设计模式。
设计模式领域中有一本经典的著作:《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为“四人组(Gang of Four)”,而这本书也就被称为“四人组(或 GoF)”书。此书中介绍了23种设计模式,随着技术的不断发展,新的设计模式不断的被发现,但这23种设计模式是最值得学习和掌握的。 C#设计模式中将设计模式划分为以下5类:
- 接口型模式
- 职责型模式
- 构造型模式
- 操作型模式
- 扩展型模式
各类别中包含的模式:
类别 | 包含的设计模式 |
接口型 | 适配器模式,外观模式,合成模式,桥接模式 |
职责型 | 单件模式,观察者模式,中介者模式,代理模式,职责链模式,享元模式 |
构造型 | 生成器模式,工厂方式模式,抽象工厂模式,原型模式,备忘录模式 |
操作型 | 模板方法模式,状态模式,策略模式,命令模式,解释器模式 |
扩展型 | 装饰模式,迭代器模式,访问者模式 |
适配器模式
一. 现实问题
在进行项目开发时,经常会发生类之间的调用关第,有可能存在以下两种情况:
1. 我们已经定义了类调用关系,例如类需要通过调用实现了某个接口的类来实现特定功能,而这个功能第三方类已经实现了,第三方类的方法签名可能与客户期望的签名不太一致。
2.这些类提供的方法可能只是完成了我们所需要的大部分功能,我们还要在此基础上执行一点点额外的操作;
二. 解决方案
以上两种问题实际上都是客户需要对提供功能的服务类的调用,可以有以下几种解决方案:
1. 直接在客户类里实例化服务类,进行方法调用,并且根据需要在调用后进行额外的处理。
2. 创建一个中介,将方法调用传递给中介,由中介请求服务类并执行额外操作。
比较而言,第二种方式更为合理,因为通过中介类的引处,降低了客户类和服务类的耦合,在服务类发生变化时,可以不影响到客户类。
三. 概念定义
对于这种问题,可以使用适配器模式。适配器模式的目的在于:如果客户需要使用某个类的服务,而这项服务是这个类用一个不同的接口提供的,那么,可以使用适配器为客户提供一个期望的接口。
存在两种适配器的形式:接口适配器(类适配器)和对象适配器。
四. 项目示例
1. 接口适配器
客户类需要通过对某个接口的调用来实现某个功能,而这个功能已经被服务类实现了。可以引入适配器类,该类继承自服务类并实现了客户端要调用的接口。
例如现在期望实现用户登录的同时更新最后活动时间的功能,约定客户类需要调用某个接口的方法实现该操作。此时发现已有现成的类(服务类)实现了用户登录的功能,但没有实现更新活动时间的操作,因此可以创建一个接口适配器。类关系图如下所示:
客户类代码如下(只定义了相应的方法,未编写实现):
/// <summary> /// 客户类 /// </summary> class Client { public IUser User { get { throw new System.NotImplementedException(); } set { } } /// <summary> /// 用户登录 /// </summary> /// <param name="user">用户名称</param> /// <param name="lastActivityDate">最后活动日期</param> public void Login(string user, DateTime lastActivityDate) { User = new InterfaceAdapter(); User.Login(user, lastActivityDate); } }
接口定义如下:
interface IUser { void Login(string name,DateTime lastActivityDate); }
服务类定义如下:
/// <summary> /// 服务类 /// </summary> class Service { /// <summary> /// 用户登录 /// </summary> /// <param name="user">用户名称</param> public void UserLogin(string user) { throw new NotImplementedException(); } }
接口适配器定义如下:
class InterfaceAdapter : Service, IUser { /// <summary> /// 用户登录 /// </summary> /// <param name="name"></param> /// <param name="lastActivityDate"></param> public void Login(string name, DateTime lastActivityDate) { UserLogin(name); UpdateActivityDate(name, lastActivityDate); } private void UpdateActivityDate(string name, DateTime lastActivityDate) { throw new NotImplementedException(); } }
此时可以直接实例化Client类并完成登录调用,例如:
Client client = new Client(); client.Login("admin",DateTime.Now);
以上是接口适配器的示例,如果客户端不存在这样的一个接口,那么可以使用对象适配器。
2. 对象适配器
使用对象适配器时,需要创建客户类的一个子类,并重写相应的方法,在该适配器类中通过服务类完成相应的功能并执行其他操作,在执行客户类操作时可以通过适配器完成。但是需要注意把客户端相应的方法定义为virtual以能够重写。上述示例使用对象适配器的类关系图如下所示:
ObjectAdapter定义如下:
class ObjectAdapter:Client { internal Service Service { get; set; } public override void Login(string user, DateTime lastActivityDate) { Service.UserLogin(user); UpdateActivityDate(user, lastActivityDate); } private void UpdateActivityDate(string name, DateTime lastActivityDate) { throw new NotImplementedException(); } }
在执行用户登录时可以通过ObjectAdapter完成,例如:
Client client = new ObjectAdapter(); client.Login("admin",DateTime.Now);
和接口适配器相比,对象适配器更加脆弱,这主要是由于对象适配器没有引入接口,在服务类发生变化时,仍然需要修改客户端代码。