设计模式一の设计模式详解
一、设计模式定义
1、单一职责原则(SRP)
阐述:简述:一个类应该只有一个发生变化的原因。(如有多个职责,应该分离)
如果没有变化的征兆,那么应用SRP原则或者其它原则,都是不明智的。
2、开放-封闭原则(OCP)
阐述:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
开闭原则的重要性:
- 开闭原则对测试的影响
开闭原则可是保持原有的测试代码仍然能够正常运行,我们只需要对扩展的代码进行测试就可以了。
- 开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。
- 开闭原则可以提高可维护性
- 面向对象开发的要求
如何使用开闭原则:
- 抽象约束
第一,通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
第三,抽象层尽量保持稳定,一旦确定即不允许修改。
- 元数据(metadata)控制模块行为
元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
Spring容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)
- 制定项目章程
在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
- 封装变化
对变化的封装包含两层含义:
第一,将相同的变化封装到一个接口或者抽象类中;
第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
3、里氏替换原则(LSP)
阐述:子类型(subtype)必须能够替换掉它们的基类型(basetype)
如要既要遵守契约,又要遵守LSP?
从CRectangle类和CSquare类,提取出公共部分,做为一个基类。比如CShape类。
CRectangle和CSquare都继承自CShape类。
4、依赖倒置原则(DIP)
阐述:面向接口编程
表现:
- 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
如何做到抽象依赖:
- 构造函数传递依赖对象——构造函数注入
- Setter方法传递依赖对象——Setter依赖注入
- 接口声明依赖对象——接口注入
5、接口隔离原则(ISP)
阐述:不同应用场景的接口要分开定义
表现:
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
6、迪米特原则(LKP)
阐述:一个对象应该对其他对象有最少的了解。迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)。
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。
含义:只和朋友交流
int main() { //创建工厂 IFactory * pFactory = NULL; IUser * pUser = NULL; IDepartment * pDepartment = NULL; int choise; cout<<"选择数据库: "; cin>>choise; switch(choise) { case 1: pFactory= new SqlServerFactory(); //创建SqlServer访问的工厂 break; case 2: pFactory = new AccessFactory(); //创建Access访问的工厂 break; } //一致的操作 pUser = pFactory->CreateUser(); pDepartment= pFactory->CreateDepartment(); pUser->Insert(); pUser->GetUser(); pDepartment->Insert(); pDepartment->GetDepartment(); return 0; }
适用性:
在以下情况使用Build模式:
1 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
2 当构造过程必须允许被构造的对象有不同的表示时。
3 Builder模式要解决的也正是这样的问题:
当我们要创建的对象很复杂的时候(通常是由很多其他的对象组合而成),
我们要复杂对象的创建过程和这个对象的表示(展示)分离开来,
这样做的好处就是通过一步步的进行复杂对象的构建,
由于在每一步的构造过程中可以引入参数,使得经过相同的步骤创建最后得到的对象的展示不一样。
在书中第一个例子RTF文档阅读器的实现中,可以看到文档RTFReader支持。
将一个“复杂对象的构建算法”与它的“部件及组装方式”分离,使得构件算法和组装方式可以独立应对变化;复用同样的构建算法可以创建不同的表示,不同的构建过程可以复用相同的部件组装方式。
例子:C#中 StringBuild就是一个很好的简单的例子
1: //Client同时充当了Director()的角色 2: StringBuilder builder = new StringBuilder(); 3: builder.Append("happyhippy"); 4: builder.Append(".cnblogs"); 5: builder.Append(".com"); 6: //返回string对象:happyhippy.cnblogs.com 7: builder.ToString();
3、factory method(工厂方法)
意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。即使一个类的实例化延迟到子类中
别名:虚拟构造器(Virtual Constructor)
适用:
在以下情况下可以使用工厂方法模式:
一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易 扩展。
将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
例子:
namespace FactoryMethod { /// <summary> /// 抽象产品类 /// </summary> public interface ICoat { void ShowCoat(); } } IFactory.cs namespace FactoryMethod { /// <summary> /// 抽象工厂类,定义产品的接口 /// </summary> public interface IFactory { ICoat CreateCoat(); } } FashionCoat.cs using System; namespace FactoryMethod { /// <summary> /// 具体产品类,时尚上衣类 /// </summary> public class FashionCoat :ICoat { public void ShowCoat() { Console.WriteLine("这件是时尚上衣"); } } } BusinessCoat.cs using System; namespace FactoryMethod { /// <summary> /// 具体产品类,商务上衣类 /// </summary> public class BusinessCoat :ICoat { public void ShowCoat() { Console.WriteLine("这件是商务上衣"); } } } FashionFactory.cs namespace FactoryMethod { /// <summary> /// 具体工厂类,用于创建时尚上衣 /// </summary> public class FashionFactory :IFactory { public ICoat CreateCoat() { return new FashionCoat(); } } } BusinessFactory.cs namespace FactoryMethod { /// <summary> /// 具体工厂类:用于创建商务上衣类 /// </summary> public class BusinessFactory : IFactory { public ICoat CreateCoat() { return new BusinessCoat(); } } } App.config <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="FactoryName" value="FashionFactory"/> </appSettings> </configuration> Program.cs using System; using System.Configuration; using System.Reflection; namespace FactoryMethod { class Client { static void Main(string[] args) { //BusinessFactory factory = new BusinessFactory(); //为了方便以后修改,将工厂类的类名写在应用程序配置文件中 string factoryName = ConfigurationManager.AppSettings["FactoryName"]; IFactory factory = (IFactory)Assembly.Load("FactoryMethod").CreateInstance("FactoryMethod." + factoryName); ICoat coat = factory.CreateCoat(); //显示你要的上衣 coat.ShowCoat(); Console.ReadLine(); } } }
4、prototype(原型)
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
结构:
我定义了一个场景,一个人开这一辆车在一条公路上。现在这件事是确定的,但不确定的有几点:1、人:姓名,性别,年龄;2车:什么牌子的;3公路:公路名字,长度,类型(柏油还是土路)。现在我们一个个实现。 先来实现人,定义一个抽象类,AbstractDriver,具体实现男性(Man)和女性(Women) public abstract class AbstractDriver { public AbstractDriver() { // // TODO: 在此处添加构造函数逻辑 // } public string name; public string sex; public int age; public abstract string drive(); public abstract AbstractDriver Clone(); } public class Man:AbstractDriver { public Man(string strName,int intAge) { sex = "Male"; name = strName; age = intAge; } public override string drive() { return name + " is drive"; } public override AbstractDriver Clone() { return (AbstractDriver)this.MemberwiseClone(); } } public class Women:AbstractDriver { public Women(string strName,int intAge) { sex = "Female"; name = strName; age = intAge; } public override string drive() { return name + " is drive"; } public override AbstractDriver Clone() { return (AbstractDriver)this.MemberwiseClone(); } } 注意:抽象代码中有一个Clone的方法,个人认为这个方法是原型模式的一个基础,因为前面讲了原型模式是通过拷贝自身来创建新的对象。 下面我们再来实现公路和汽车 公路: public abstract class AbstractRoad { public AbstractRoad() { // // TODO: 在此处添加构造函数逻辑 // } public string Type; public string RoadName; public int RoadLong; public abstract AbstractRoad Clone(); } public class Bituminous:AbstractRoad //柏油路 { public Bituminous(string strName,int intLong) { RoadName = strName; RoadLong = intLong; Type = "Bituminous"; } public override AbstractRoad Clone() { return (AbstractRoad)this.MemberwiseClone(); } } public class Cement:AbstractRoad //水泥路 { public Cement(string strName,int intLong) { RoadName = strName; RoadLong = intLong; Type = "Cement"; } public override AbstractRoad Clone() { return (AbstractRoad)this.MemberwiseClone(); } } 汽车: public abstract class AbstractCar { public AbstractCar() { // // TODO: 在此处添加构造函数逻辑 // } public string OilBox; public string Wheel; public string Body; public abstract string Run(); public abstract string Stop(); public abstract AbstractCar Clone(); } public class BMWCar:AbstractCar { public BMWCar() { OilBox = "BMW's OilBox"; Wheel = "BMW's Wheel"; Body = "BMW's body"; } public override string Run() { return "BMW is running"; } public override string Stop() { return "BMW is stoped"; } public override AbstractCar Clone() { return (AbstractCar)this.MemberwiseClone(); } } public class BORACar:AbstractCar { public BORACar() { OilBox = "BORA's OilBox"; Wheel = "BORA's Wheel"; Body = "BORA's Body"; } public override string Run() { return "BORA is running"; } public override string Stop() { return "BORA is stoped"; } public override AbstractCar Clone() { return (AbstractCar)this.MemberwiseClone(); } } public class VolvoCar:AbstractCar { public VolvoCar() { OilBox = "Volvo's OilBox"; Wheel = "Volvo's Wheel"; Body = "Volvo's Body"; } public override string Run() { return "Volvo is running"; } public override string Stop() { return "Volvo is stoped"; } public override AbstractCar Clone() { return (AbstractCar)this.MemberwiseClone(); } } 然后我们再来看看场景,我们定义一个Manage类,在这个场景中有一个人,一辆车和一条公路,代码实现如下: class Manage { public AbstractCar Car; public AbstractDriver Driver; public AbstractRoad Road; public void Run(AbstractCar car,AbstractDriver driver,AbstractRoad road) { Car = car.Clone(); Driver = driver.Clone(); Road = road.Clone(); } } 可以看到,在这个代码中,场景只是依赖于那几个抽象的类来实现的。最后我们再来实现一下客户代码,比如我现在要一辆Volvo车,一个叫“Anli”的女司机,在一条叫“Road1”、长1000的柏油路上。 static void Main(string[] args) { Manage game = new Manage(); game.Run(new VolvoCar(),new Women("Anli",18),new Bituminous("Road1",1000)); Console.Write("CarRun:" + game.Car.Run() + "\n"); Console.Write("DriverName:" + game.Driver.name + "\n"); Console.Write("DriverSex:" + game.Driver.sex + "\n"); Console.Write("RoadName:" + game.Road.RoadName + "\n"); Console.Write("RoadType:" + game.Road.Type + "\n"); Console.Write("CarStop:" + game.Car.Stop() + "\n"); Console.Read(); } 运行的结果是: CarRun:Volvo is running DriverName:Anli DriverSex:Female RoadName:Road1 RoadType:Bituminous CarStop:Volvo is stoped
现在我们再来看看原型模式的几个要点:
1、Prototype模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些“易变类”拥有“稳定的接口”。
2、Prototype模式对于“如何创建易变类的实体对象”采用“原型克隆”的方法来实现,它使得我们可以非常灵活地动态创建“拥有某些稳定接口”的新对象——所需工作仅仅是注册一个新类的对象(即原型),然后在任何需要的地方不断地Clone。
3、Prototype模式中的Clone方法可以利用Object类的MemberwiseClone()或者序列化来实现深拷贝。
5、singleton(单件)
意图:保证类有且仅有一个实例,并提供一个访问它的全局访问点
此处没有过多的解释,程序只运行一个实例的例子很多,如工具管理的类,主程序等
单例模式的实现:
#的独特语言特性决定了C#拥有实现Singleton模式的独特方法。这里不再赘述原因,给出几个结果: 方法一: 下面是利用.NET Framework平台优势实现Singleton模式的代码: sealed class Singleton { private Singleton(); public static readonly Singleton Instance=new Singleton(); } 这使得代码减少了许多,同时也解决了线程问题带来的性能上损失。那么它又是怎样工作的呢? 注意到,Singleton类被声明为sealed,以此保证它自己不会被继承,其次没有了Instance的方法,将原来_instance成员变量变成public readonly,并在声明时被初始化。通过这些改变,我们确实得到了Singleton的模式,原因是在JIT的处理过程中,如果类中的static属性被任何方法使用时,.NET Framework将对这个属性进行初始化,于是在初始化Instance属性的同时Singleton类实例得以创建和装载。而私有的构造函数和readonly(只读)保证了Singleton不会被再次实例化,这正是Singleton设计模式的意图。 (摘自:http://www.cnblogs.com/huqingyu/archive/2004/07/09/22721.aspx ) 不过这也带来了一些问题,比如无法继承,实例在程序一运行就被初始化,无法实现延迟初始化等。 方法二: 既然方法一存在问题,我们还有其它办法。 public sealed class Singleton { Singleton() { } public static Singleton GetInstance() { return Nested.instance; } class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); } }
六、结构型模式
结构型模式涉及到如何组合类和对象以或者更大的结构
1、adapter模式
意图:把一个类的接口变换成客户端所期待的另一种接口, Adapter模式使原本因接口不匹配(或者不兼容)而无法在一起工作的两个类能够在一起工作
比如:多种手机,每一种机型都自带有从电器,有一天自带充电器坏了,而且市场没有这类型充电器可买了。怎么办?万能充电器就可以解决。这个万能充电器就是适配器。
分类:
共有两类适配器模式:1.类的适配器模式(采用继承实现)2.对象适配器(采用对象组合方式实现)
1)类适配器模式 ——适配器继承自已实现的类(一般多重继承)。
Adapter与Adaptee是继承关系
1、用一个具体的Adapter类和Target进行匹配。结果是当我们想要一个匹配一个类以及所有它的子类时,类Adapter将不能胜任工作
2、使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子集
3、仅仅引入一个对象,并不需要额外的指针以间接取得adaptee
2)对象适配器模式—— 适配器容纳一个它包裹的类的实例。在这种情况下,适配器调用被包裹对象的物理实体。
Adapter与Adaptee是委托关系
1、允许一个Adapter与多个Adaptee同时工作。Adapter也可以一次给所有的Adaptee添加功能
2、使用重定义Adaptee的行为比较困难
无论哪种适配器,它的宗旨都是:保留现有类所提供的服务,向客户提供接口,以满足客户的期望。
即在不改变原有系统的基础上,提供新的接口服务。
适用:
1 • 你想使用一个已经存在的类,而它的接口不符合你的需求。
2 • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
3 •(仅适用于对象Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。即仅仅引入一个对象,并不需要额外的指针以间接取得adaptee。
举例:
/// <summary> /// 定义客户端期待的接口 /// </summary> public class Target { /// <summary> /// 使用virtual修饰以便子类可以重写 /// </summary> public virtual void Request() { Console.WriteLine("This is a common request"); } } /// <summary> /// 定义需要适配的类 /// </summary> public class Adaptee { public void SpecificRequest() { Console.WriteLine("This is a special request."); } } /// <summary> /// 定义适配器 /// </summary> public class Adapter:Target { // 建立一个私有的Adeptee对象 private Adaptee adaptee = new Adaptee(); /// <summary> /// 通过重写,表面上调用Request()方法,变成了实际调用SpecificRequest() /// </summary> public override void Request() { adaptee.SpecificRequest(); } } 客户端: class Program { static void Main(string[] args) { // 对客户端来说,调用的就是Target的Request() Target target = new Adapter(); target.Request(); Console.Read(); } }
举例:
类适配器
using System; /// 这里以插座和插头的例子来诠释适配器模式 /// 现在我们买的电器插头是2个孔,但是我们买的插座只有3个孔的 /// 这是我们想把电器插在插座上的话就需要一个电适配器 namespace 设计模式之适配器模式 { /// <summary> /// 客户端,客户想要把2个孔的插头 转变成三个孔的插头,这个转变交给适配器就好 /// 既然适配器需要完成这个功能,所以它必须同时具体2个孔插头和三个孔插头的特征 /// </summary> class Client { static void Main(string[] args) { // 现在客户端可以通过电适配要使用2个孔的插头了 IThreeHole threehole = new PowerAdapter(); threehole.Request(); Console.ReadLine(); } } /// <summary> /// 三个孔的插头,也就是适配器模式中的目标角色 /// </summary> public interface IThreeHole { void Request(); } /// <summary> /// 两个孔的插头,源角色——需要适配的类 /// </summary> public abstract class TwoHole { public void SpecificRequest() { Console.WriteLine("我是两个孔的插头"); } } /// <summary> /// 适配器类,接口要放在类的后面 /// 适配器类提供了三个孔插头的行为,但其本质是调用两个孔插头的方法 /// </summary> public class PowerAdapter:TwoHole,IThreeHole { /// <summary> /// 实现三个孔插头接口方法 /// </summary> public void Request() { // 调用两个孔插头方法 this.SpecificRequest(); } } }
对象适配器
namespace 对象的适配器模式 { class Client { static void Main(string[] args) { // 现在客户端可以通过电适配要使用2个孔的插头了 ThreeHole threehole = new PowerAdapter(); threehole.Request(); Console.ReadLine(); } } /// <summary> /// 三个孔的插头,也就是适配器模式中的目标(Target)角色 /// </summary> public class ThreeHole { // 客户端需要的方法 public virtual void Request() { // 可以把一般实现放在这里 } } /// <summary> /// 两个孔的插头,源角色——需要适配的类 /// </summary> public class TwoHole { public void SpecificRequest() { Console.WriteLine("我是两个孔的插头"); } } /// <summary> /// 适配器类,这里适配器类没有TwoHole类, /// 而是引用了TwoHole对象,所以是对象的适配器模式的实现 /// </summary> public class PowerAdapter : ThreeHole { // 引用两个孔插头的实例,从而将客户端与TwoHole联系起来 public TwoHole twoholeAdaptee = new TwoHole(); /// <summary> /// 实现三个孔插头接口方法 /// </summary> public override void Request() { twoholeAdaptee.SpecificRequest(); } } }
桥梁模式(bridge模式):桥梁模式与对象适配器类似,但是桥梁模式的出发点不同:桥梁模式目的是将接口部分和实现部分分离,从而对它们可以较为容易也相对独立的加以改变。而对象适配器模式则意味着改变一个已有对象的接口
装饰器模式(decorator模式):装饰模式增强了其他对象的功能而同时又不改变它的接口。因此装饰模式对应用的透明性比适配器更好。结果是decorator模式支持递归组合,而纯粹使用适配器是不可能实现这一点的。
Facade(外观模式):适配器模式的重点是改变一个单独类的API。Facade的目的是给由许多对象构成的整个子系统,提供更为简洁的接口。而适配器模式就是封装一个单独类,适配器模式经常用在需要第三方API协同工作的场合,设法把你的代码与第三方库隔离开来。
适配器模式与外观模式都是对现相存系统的封装。但这两种模式的意图完全不同,前者使现存系统与正在设计的系统协同工作而后者则为现存系统提供一个更为方便的访问接口。简单地说,适配器模式为事后设计,而外观模式则必须事前设计,因为系统依靠于外观。总之,适配器模式没有引入新的接口,而外观模式则定义了一个全新的接口。
代理模式(Proxy )在不改变它的接口的条件下,为另一个对象定义了一个代理。
装饰者模式,适配器模式,外观模式三者之间的区别:
装饰者模式的话,它并不会改变接口,而是将一个一个的接口进行装饰,也就是添加新的功能。
适配器模式是将一个接口通过适配来间接转换为另一个接口。
外观模式的话,其主要是提供一个整洁的一致的接口给客户端。