大话设计模式
设计模式概览
1.简单工厂模式
结构图
代码实现
public class Operation
{
private double _numberA = 0;
private double _numberB = 0;
public double NumberA
{
get { return _numberA; }
set { _numberA = value; }
}
public double NumberB
{
get { return _numberB; }
set { _numberB = value; }
}
public virtual double GetResult()
{
double result = 0;
return result;
}
}
2.策略模式
策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
结构图
CashContext类定义
客户端代码
策略与简单工厂结合
简单工厂模式我需要让客户端认识两个类,CashSuper和CashFactory,而策略模式与简单工厂结合的用法,客户端就只需要认识一个类CashContext就可以了。耦合更加降低。
改造后的CashContext
客户端代码
3.单一职责原则
一个类而言,应该仅有一个引起它变化的原因。
软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。
4.开放-封闭原则
这个原则其实是有两个特征,一个是说对于扩展是开放的,另一个是说对于更改是封闭的。开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
5.依赖倒转原则
- 高层模块不应该依赖底层模块。两个模块都应该依赖抽象。
- 抽象不应该依赖细节。细节应该依赖抽象
关于这个原则的理解:首先理解什么是高层模块,什么是底层模块,高层模块一般是指我们的业务逻辑,底层模块一般是指一些跟业务无关的通用功能。再来理解为什么两个都应该依赖抽象,例如,我们做的项目大多要访问数据库,所以我们就把访问数据库的代码写成了函数,每次做新项目时就去调用这些函数,这也就叫做高层模块依赖低层模块。但是我们要做新项目时,发现业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。我们希望能再次利用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起的,没办法复用这些高层模块,这就非常糟糕了。而如果不管高层模块还是低层模块,它们都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其他受到影响,这就使得无论高层模块还是低层模块都可以很容易地被复用。这才是最好的办法。为什么依赖了抽象的接口或抽象类,就不怕更改呢,这里涉及一个另一个原则:
里氏代换原则
子类型必须能够替换掉他们的父类型。它的白话翻译就是一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。
6.装饰模式
结构图
这个模式还可以变通:如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decorator类可以是ConcreteComponent的一个子类。同样道理,如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
代码实现
abstract class Component
{
public abstract void Operation();
}
class ConcreteComponent : Component
{
public override void Operation()
{
Console.WriteLine("具体对象的操作");
}
}
装饰模式总结:装饰模式是为已有功能动态地添加更多功能的一种方式。什么时候用它?
当系统需要新功能的时候,而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要,如果我们直接在主类里添加新的代码会增加了主类的复杂度。而装饰模式却提供了一个非常好的解决方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。
装饰模式的优点总结下来就是,把类中的装饰功能从类中搬移去除,这样可以简化原有的类。
7.代理模式
为其他对象提供一种代理以控制对这个对象的访问。
结构图
代码实现
//Subject类,定义了RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy
abstract class Subject
{
public abstract void Request();
}
//RealSubject类,定义Proxy所代表的真实实体
class RealSubject : Subject
{
public override void Request()
{
Console.WriteLine("真实的请求");
}
}
//Proxy类,保存一个引用使得代理可以访问实体,并提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体
class Proxy : Subject
{
RealSubject realSubject;
public override void Request()
{
if(realSubject == null)
{
realSubject = new RealSubject();
}
realSubject.Request();
}
}
//客户端代码
static void Main(string[] args)
{
Proxy proxy = new Proxy();
proxy.Request();
Console.Read();
}
代理模式的应用
- 第一种应用是远程代理,也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。典型的应用有WebService服务代理。
- 第二种应用是虚拟代理,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如说你打开一个很大的HTML网页时,里面可能有很多的文字和图片,但你还是可以很快打开它,此时你所看到的是所有的文字,但图片却是一张一张地下载后才能看到。那些未打开的图片框,就是通过虚拟代理来替代了真实的图片,此时代理存储了真实图片的路径和尺寸。
- 第三种应用是安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候。
- 第四种是智能指引,是指当调用真实的对象时,代理处理另外一些事。如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它;或当第一次引用一个持久对象时,将它装入内存;或在访问一个实际对象前,检查是否已经锁定它,以确保其他对象不能改变它。它们都是通过代理在访问一个对象时附加一些内务处理。
8.工厂方法模式
结构图(以前面计算的例子做示例)
代码实现
interface IFactory
{
Operation CreateOperation();
}
IFactory operFactory = new AddFactory();
Operation oper = operFactory.CreateOperation();
oper.NumberA = 1;
oper.NumberB = 2;
double result=oper.GetResult();
简单工厂vs工厂方法
简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。就像你的计算器,让客户端不用管该用哪个类的实例,只需要把'+’给工厂,工厂自动就给出了相应的实例,客户端只要去做运算就可以了,不同的实例会实现不同的运算。但问题也就在这里,如你所说,如果要加一个'求M数的N次方’的功能,我们是一定需要给运算工厂类的方法里加'Case’的分支条件的,修改原有的类,违背了开放-封闭原则。而对于工厂方法,则只需要增加此功能的运算类和相应的工厂类就可以了。工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。但缺点是由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量。
注意:工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是说,工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行。你想要加功能,本来是改工厂类的,而现在是修改客户端,这个问题可以通过反射解决。
9.原型模式
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。
结构图
代码实现
原型类
具体原型
客户端代码
注意:对于.NET而言,那个原型抽象类Prototype是用不着的,因为克隆实在是太常用了,所以.NET在System命名空间中提供了ICloneable接口,其中就是唯一的一个方法Clone,这样你就只需要实现这个接口就可以完成原型模式了。
原型模式要注意深复制和浅复制的问题。在一些特定场合,会经常涉及深复制或浅复制,比如说,数据集对象DataSet,它就有Clone方法和Copy方法,Clone方法用来复制DataSet的结构,但不复制DataSet的数据,实现了原型模式的浅复制。Copy方法不但复制结构,也复制数据,其实就是实现了原型模式的深复制
10.模板方法模式
定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类的可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
结构图
使用原则:当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
11.迪米特法则:
12.外观模式
结构图
代码实现
class SubSystemOne
{
public void MethodOne()
{
Console.WriteLine(" 子系统方法一");
}
}
class SubSystemTwo
{
public void MethodTwo()
{
Console.WriteLine(" 子系统方法二");
}
}
class SubSystemThree
{
public void MethodThree()
{
Console.WriteLine(" 子系统方法三");
}
}
class SubSystemFour
{
public void MethodFour()
{
Console.WriteLine(" 子系统方法四");
}
}
外观类
客户端
何时使用外观模式:
- 首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。
- 其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式使用时也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。
- 第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时用外观模式Facade也是非常合适的。你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。
13.建造者模式
如果你需要将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示的意图时,我们需要应用于一个设计模式,'建造者(Builder)模式’,又叫生成器模式。如果我们用了建造者模式,那么用户就只需指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需知道了。它主要是用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。
以下以建造小人做示例:
abstract class PersonBuilder
{
protected Graphics g;
protected Pen p;
public PersonBuilder(Graphics g,Pen p)
{
this.g = g;
this.p = p;
}
public abstract void BuildHead();
public abstract void BuildBody();
public abstract void BuildArmLeft();
public abstract void BuildArmRight();
public abstract void BuildLegLeft();
public abstract void BuildLegRight();
}
//我们需要建造一个瘦的小人,则让这个瘦子类去继承这个抽象类。当然,胖人或高个子其实都是用类似的代码去实现这个类就可以了
class PersonThinBuilder : PersonBuilder
{
public PersonThinBuilder(Graphics g,Pen p) : base(g,p){ }
public override void BuildHead()
{
g.DrawEllipse(p,50,20,30,30);
}
public override void BuildBody()
{
g.DrawRectangle(p,60,50,10,50);
}
public override void BuildArmLeft()
{
g.DrawLine(p,60,50,40,100);
}
public override void BuildArmRight()
{
g.DrawLine(p,70,50,90,100);
}
public override void BuildLegLeft()
{
g.DrawLine(p,60,100,45,150);
}
public override void BuildLegRight()
{
g.DrawLine(p,70,100,85,150);
}
}
指挥者
客户端
Pen p=new Pen(Color.Yellow);
PersonThinBuilder ptb = new PersonThinBuilder(pictureBox1.CreateGraphics(),p);
PersonDirector pdThin = new PersonDirector(ptb);
pdThin.CreatePerson();
PersonFatBuilder pfb = new PersonFatBuilder(pictureBox2.CreateGraphics(),p);
PersonDirector pdFat = new PersonDirector(pfb);
pdFat.CreatePerson();
示例结构图
建造者模式结构图
14.观察者模式-又叫做发布-订阅(Publish/Subscribe)模式
结构图
代码实现
//Subject类,可翻译为主题或抽象通知者,一般用一个抽象类或者一个接口实现。它把所有对观察者对象的引用保存在一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
abstract class Subject
{
private IList<Observer> observers = new List<Observer>();
//增加观察者
public void Attach(Observer observer)
{
observers.Add(observer);
}
//移除观察者
public void Detach(Observer observer)
{
observers.Remove(observer);
}
//通知
public void Notify()
{
foreach(Observer o in observers)
{
o.Update();
}
}
}
//ConcreteSubject类,叫做具体主题或具体通知者,将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个具体子类实现。
class ConcreteSubject : Subject
{
private string subjectState;
//具体被观察者状态
public string SubjectState
{
get { return subjectState; }
set { subjectState = value; }
}
}
//Observer类,抽象观察者,为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。
abstract class Observer
{
public abstract void Update();
}
//ConcreteObserver类,具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。具体观察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体子类实现。
class ConcreteObserver : Observer
{
private string name;
private string observerState;
private ConcreteSubject subject;
public ConcreteObserver(ConcreteSubject subject,string name)
{
this.subject = subject;
this.name = name;
}
public override void Update()
{
observerState = subject.SubjectState;
Console.WriteLine("观察者{0}的新状态是{1}",name,observerState);
}
public ConcreteSubject Subject
{
get { return subject; }
set { subject = value; }
}
}
//客户端代码
static void Main(string[] args)
{
ConcreteSubject s = new ConcreteSubject();
s.Attach(new ConcreteObserver(s,"X"));
s.Attach(new ConcreteObserver(s,"Y"));
s.Attach(new ConcreteObserver(s,"Z"));
s.SubjectState = "ABC";
s.Notify();
Console.Read();
}
//结果展示
观察者X的新状态是ABC
观察者Y的新状态是ABC
观察者Z的新状态是ABC
用观察者模式的动机是什么呢?
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便[DP]。而观察者模式的关键对象是主题Subject和观察者Observer,一个Subject可以有任意数目的依赖它的Observer,一旦Subject的状态发生了改变,所有的Observer都可以得到通知。Subject发出通知时并不需要知道谁是它的观察者,也就是说,具体观察者是谁,它根本不需要知道。而任何一个具体观察者不知道也不需要知道其他观察者的存在。
什么时候考虑使用观察者模式呢?
当一个对象的改变需要同时改变其他对象的时候。而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。
总的来讲,观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。
观察者模式的不足:有些观察者可能不是自己写的代码,可能是别人的类库,这种情况下的观察者是不可能实现Observer接口的,这时可以通过事件委托实现。即在通知者里定义一个事件,然后将观察者的更新方法添加到通知者的事件里即可。
15.抽象工厂模式
以数据库选择为示例的结构图
以数据库选择为示例的代码实现
interface IDepartment
{
void Insert(Department department);
Department GetDepartment(int id);
}
class SqlserverDepartment : IDepartment
{
public void Insert(Department department)
{
Console.WriteLine("在SQL Server中给Department表增加一条记录");
}
public Department GetDepartment(int id)
{
Console.WriteLine("在SQL Server中根据ID得到Department表一条记录");
return null;
}
}
class AccessDepartment : IDepartment
{
public void Insert(Department department)
{
Console.WriteLine("在Access中给Department表增加一条记录");
}
public Department GetDepartment(int id)
{
Console.WriteLine("在Access中根据ID得到Department表一条记录");
return null;
}
}
客户端
、
抽象工厂模式的优点
最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。
第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。例如上面的例子,客户端所认识的只有IUser和IDepartment,至于它是用SQL Server来实现还是Access来实现就不知道了。
抽象工厂模式的缺点
抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果你的需求来自增加功能,比如我们现在要增加项目表Project,那就至少要增加三个类,IProject、SqlserverProject、AccessProject,还需要更改IFactory、SqlserverFactory和AccessFactory才可以完全实现。
用简单工厂改进抽象工厂
抛弃了IFactory、SqlserverFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,由于事先设置了db的值(Sqlserver或Access),所以简单工厂的方法都不需要输入参数,这样在客户端就只需要DataAccess.CreateUser()和
DataAccess.CreateDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL Server或Access的字样,达到了解耦的目的。
结构图
代码实现
这个改进还不是很优秀,如果我需要增加Oracle数据库访问,本来抽象工厂只增加一个OracleFactory工厂类就可以了,现在就比较麻烦了,就需要在DataAccess类中每个方法的swicth中加case了。
用反射+抽象工厂(更好的方式,还可以利用依赖注入)
从这个角度上说,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
16.状态模式
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,那就没必要用'状态模式’了。
结构图
代码实现
abstract class State
{
public abstract void Handle(Context context);
}
状态模式消除庞大的条件分支语句,大的分支判断会使得它们难以修改和扩展,就像我们最早说的刻版印刷一样,任何改动和变化都是致命的。状态模式通过把各种状态转移逻辑分布到State的子类之间,来减少相互间的依赖,好比把整个版面改成了一个又一个的活字,此时就容易维护和扩展了。
什么时候使用状态模式?
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。另外如果业务需求某项业务有多个状态,通常都是一些枚举常量,状态的变化都是依靠大量的多分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,某一天客户需要更改需求,增加或减少业务状态或改变状态流程,对你来说都是不困难的事。
17.适配器模式
适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。
注意:在GoF的设计模式中,对适配器模式讲了两种类型,类适配器模式和对象适配器模式,由于类适配器模式通过多重继承对一个接口与另一个接口进行匹配,而C#、VB.NET、JAVA等语言都不支持多重继承(C++支持),也就是一个类只有一个父类,所以我们这里主要讲的是对象适配器。
结构图
代码实现
class Target
{
public virtual void Request()
{
Console.WriteLine("普通请求!");
}
}
class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("特殊请求!");
}
}
何时使用适配器模式
两个类所做的事情相同或相似,但是具有不同的接口时要使用它。客户代码可以统一调用同一接口就行了,这样应该可以更简单、更直接、更紧凑。
其实用适配器模式也是无奈之举,很有点'亡羊补牢’的感觉,没办法呀,是软件就有维护的一天,维护就有可能会因不同的开发人员、不同的产品、不同的厂家而造成功能类似而接口不同的情况,此时就是适配器模式大展拳脚的时候了。即我们通常是在软件开发后期或维护期再考虑使用它。
那有没有设计之初就需要考虑用适配器模式的时候?
当然有,比如公司设计一系统时考虑使用第三方开发组件,而这个组件的接口与我们自己的系统接口是不相同的,而我们也完全没有必要为了迎合它而改动自己的接口,此时尽管是在开发的设计阶段,也是可以考虑用适配器模式来解决接口不同的问题。
适配器模式在.Net的应用
在.NET中有一个类库已经实现的、非常重要的适配器,那就是DataAdapter。DataAdapter用作DataSet和数据源之间的适配器以便检索和保存数据。DataAdapter通过映射Fill(这更改了DataSet中的数据以便与数据源中的数据相匹配)和Update(这更改了数据源中的数据以便与DataSet中的数据相匹配)来提供这一适配器[MSDN]。由于数据源可能是来自SQL Server,可能来自Oracle,也可能来自Access、DB2,这些数据在组织上可能有不同之处,但我们希望得到统一的DataSet(实质是XML数据),此时用DataAdapter就是非常好的手段,我们不必关注不同数据库的数据细节,就可以灵活的使用数据。
适配器模式和代理模式的区别
适配器模式和代理模式看似类似,其实他们是不同的,代理模式的两个类是继承相同接口的,只是在一些场景下需要用代理去访问真实对象。而适配器里的两个类是功能相似,但是具有不同的接口。
18.备忘录模式
结构图
代码实现
备忘录模式的应用场合:
Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator可以根据保存的Memento信息还原到前一状态。例如当对象的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。
19.组合模式
结构图
代码实现
何时使用组合模式
当你发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。例如以前曾经用过的ASP.NET的TreeView控件就是典型的组合模式应用。还例如我们写过的自定义控件,也就是把一些基本的控件组合起来,通过编程写成一个定制的控件,比如用两个文本框和一个按钮就可以写一下自定义的登录框控件,实际上,所有的Web控件的基类都是System.Web.UI.Control,而Control基类中就有Add和Remove方法,这就是典型的组合模式的应用。
20.迭代器模式
本来这个模式还是有点意思的,不过现今来看迭代器模式实用价值远不如学习价值大了,MartinFlower甚至在自己的网站上提出撤销此模式。因为现在高级编程语言如C#、JAVA等本身已经把这个模式做在语言中了。
结构图
代码实现
当如果只有一种遍历方式时,可以直接定义一个ConcreteIterator而不需要实现抽象的Iterator,但是如果有多重遍历方式时,定义抽象的抽象的Iterator就很必要。例如上面的遍历是从头到尾,现在实现一个从尾到头的遍历:
这时你客户端只需要更改一个地方就可以实现反向遍历了:
//Iterator i = new ConcreteIterator(a);
Iterator i = new ConcreteIteratorDesc(a);
.NET的迭代器实现
IEumerator支持对非泛型集合的简单迭代接口
IEnumerable公开枚举数,该枚举数支持在非泛型集合上进行简单迭代
你会发现,这两个接口,特别是IEumerator要比我们刚才写的抽象类Iterator要简洁,但可实现的功能却一点不少,这其实也是对GoF的设计改良的结果。有了这个基础,你再来看你最熟悉的foreach in就很简单了:
这里用到了foreach in而在编译器里做了些什么呢?其实它做的是下面的工作
IEnumerator<string> e = a.GetEnumerator();
while(e.MoveNext())
{
Console.WriteLine("{0} 请买车票!",e.Current);
}
21.单例模式
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
结构图
代码实现
单例模式和类似.Net框架里的Math类什么区别:
它们之间的确很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的,比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。在运用中还得仔细分析再作决定用哪一种方式。
多线程的单例
多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用GetInstance()方法,会有可能造成创建多个实例的。这是可以通过加锁来处理。
上面这个方式每次调用GetInstance方法时都需要lock,不太好,下面是优化写法(也叫双重锁定)
静态初始化
C#与公共语言运行库也提供了一种'静态初始化’方法,这种方法不需要开发人员显式地编写线程安全代码,即可解决多线程环境下它是不安全的问题。
由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类,原先的单例模式处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。
22.合成/聚合原则
合成/聚合复用原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大。例如下面的结构图就是用错了继承:
改成用合成/聚合后:
23.桥接模式
'将抽象部分与它的实现部分分离’,还是不好理解,我的理解就是实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
也就是说,在发现我们需要多角度去分类实现对象,而只用继承会造成大量的类增加,不能满足开放-封闭原则时,就应该要考虑用桥接模式了。
结构图
代码实现
abstract class Implementor
{
public abstract void Operation();
}
class ConcreteImplementorA : Implementor
{
public override void Operation()
{
Console.WriteLine("具体实现A的方法执行");
}
}
class ConcreteImplementorB : Implementor
{
public override void Operation()
{
Console.WriteLine("具体实现B的方法执行");
}
}
class Abstraction
{
protected Implementor implementor;
public void SetImplementor(Implementor implementor)
{
this.implementor = implementor;
}
public virtual void Operation()
{
implementor.Operation();
}
}
class RefinedAbstraction : Abstraction
{
public override void Operation()
{
implementor.Operation();
}
}
static void Main(string[] args)
{
Abstraction ab = new RefinedAbstraction();
ab.SetImplementor(new ConcreteImplementorA());
ab.Operation();
ab.SetImplementor(new ConcreteImplementorB());
ab.Operation();
Console.Read();
}
24.命令模式
结构图
代码实现
abstract class Command
{
protected Receiver receiver;
public Command(Receiver receiver)
{
this.receiver = receiver;
}
abstract public void Execute();
}
class ConcreteCommand : Command
{
public ConcreteCommand(Receiver receiver) : base(receiver) { }
public override void Execute()
{
receiver.Action();
}
}
class Invoker
{
private Command command;
public void SetCommand(Command command)
{
this.command = command;
}
public void ExecuteCommand()
{
command.Execute();
}
}
class Receiver
{
public void Action()
{
Console.WriteLine("执行请求!");
}
}
static void Main(string[] args)
{
Receiver r = new Receiver();
Command c = new ConcreteCommand(r);
Invoker i = new Invoker();
i.SetCommand(c);
i.ExecuteCommand();
Console.Read();
}
命令模式的优点:
第一,它能较容易地设计一个命令队列;
第二,在需要的情况下,可以较容易地将命令记入日志;
第三,允许接收请求的一方决定是否要否决请求;
第四,可以容易地实现对请求的撤销和重做;
第五,由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易;
其实还有最关键的优点就是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。
25.职责链模式
结构图
代码实现
这当中最关键的是当客户提交一个请求时,请求是沿链传递直至有一个ConcreteHandler对象负责处理它。接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。结果是职责链可简化对象的相互连接,它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。由于是在客户端来定义链的结构,也就是说,我可以随时地增加或修改处理一个请求的结构。增强了给对象指派职责的灵活性。
职责链模式和撞他模式区别
状态模式与职责链模式的最大的不同是设置自己的下一级的问题上,状态模式是在类的设计阶段就定好的,不能在客户端改变,而职责链的下一级是在客户端自己来确定的。
26.中介者模式
结构图
代码实现
class ConcreteColleague1 : Colleague
{
public ConcreteColleague1(Mediator mediator): base(mediator) { }
public void Send(string message)
{
mediator.Send(message,this);
由于有了Mediator,使得ConcreteColleague1和ConcreteColleague2在发送消息和接收信息时其实是通过中介者来完成的,这就减少了它们之间的耦合度了。
中介者模式的优缺点
优点:中介者模式的优点首先是Mediator的出现减少了各个Colleague的耦合,使得可以独立地改变和复用各个Colleague类和Mediator。由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。
缺点:由于ConcreteMediator控制了集中化,于是就把交互复杂性变为了中介者的复杂性,这就使得中介者会变得比任何一个ConcreteColleague都复杂。
中介者模式的应用
用.NET写的Windows应用程序中的Form或Web网站程序的aspx就是典型的中介者。
比如下面用winform开发的计算器程序,它上面有菜单控件、文本控件、多个按钮控件和一个Form窗体,每个控件之间的通信都是通过谁来完成的?它们之间是否知道对方的存在?
由于每个控件的类代码都被封装了,所以它们的实例是不会知道其他控件对象的存在的,比如点击数字按钮要在文本框中显示数字,按照我以前的想法就应该要在Button类中编写给TextBox类实例的Text属性赋值的代码,造成两个类有耦合,这显然是非常不合理的。但实际情况是它们都有事件机制,而事件的执行都是在Form窗体的代码中完成,也就是说所有的控件的交互都是由Form窗体来作中介,操作各个对象,这的确是典型的中介者模式应用。
27.享元模式
结构图
代码实现
abstract class Flyweight
{
public abstract void Operation(int extrinsicstate);
}
class ConcreteFlyweight : Flyweight
{
public override void Operation(int extrinsicstate)
{
Console.WriteLine("具体Flyweight:"+extrinsicstate);
}
}
class UnsharedConcreteFlyweight : Flyweight
{
public override void Operation(int extrinsicstate)
{
Console.WriteLine("不共享的具体Flyweight:" + extrinsicstate);
}
}
内部状态与外部状态
享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。也就是说,享元模式Flyweight执行时所需的状态是有内部的也可能有外部的,内部状态存储于ConcreteFlyweight对象之中,而外部对象则应该考虑由客户端对象存储或计算,当调用Flyweight对象的操作时,将该状态传递给它。
享元模式应用
- 如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;
- 还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。
因为用了享元模式,所以有了共享对象,实例总数就大大减少了,如果共享的对象越多,存储节约也就越多,节约量随着共享状态的增多而增大。
实际上在.NET中,字符串string就是运用了Flyweight模式。举个例子吧。Object.ReferenceEquals(object objA,object objB)方法是用来确定objA与objB是否是相同的实例,返回值为bool值。
string titleA = "大话设计模式";
string titleB = "大话设计模式";
Console.WriteLine(Object.ReferenceEquals(titleA,titleB));
返回值竟然是True,这两个字符串是相同的实例,试想一下,如果每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存的开销会很大。所以如果第一次创建了字符串对象titleA,下次再创建相同的字符串titleB时只是把它的引用指向'大话设计模式’,这样就实现了'大话设计模式’在内存中的共享。
再比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大量的棋子对象,你分析一下,它们的内部状态和外部状态各是什么?
围棋和五子棋只有黑白两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。像围棋,一盘棋理论上有361个空位可以放棋子,那如果用常规的面向对象方式编程,每盘棋都可能有两三百个棋子对象产生,一台服务器就很难支持更多的玩家玩围棋游戏了,毕竟内存空间还是有限的。如果用了享元模式来处理棋子,那么棋子对象可以减少到只有两个实例。
28.解释器模式
解释器模式需要解决的是,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
例如我们经常使用的正则表达式就是解释器模式的应用,因为这个匹配字符的需求在软件的很多地方都会使用,而且行为之间都非常类似,过去的做法是针对特定的需求,编写特定的函数,比如判断Email、匹配电话号码等等,与其为每一个特定需求都写一个算法函数,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。
结构图
代码实现
abstract class AbstractExpression
{
public abstract void Interpret(Context context);
}
class TerminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
Console.WriteLine("终端解释器");
}
}
class NonterminalExpression : AbstractExpression
{
public override void Interpret(Context context)
{
Console.WriteLine("非终端解释器");
}
}
class Context
{
private string input;
public string Input
{
get { return input; }
set { input = value; }
}
private string output;
public string Output
{
get { return output; }
set { output = value; }
}
}
static void Main(string[] args)
{
Context context = new Context();
IList<AbstractExpression> list = new List<AbstractExpression>();
list.Add(new TerminalExpression());
list.Add(new NonterminalExpression());
list.Add(new TerminalExpression());
list.Add(new TerminalExpression());
foreach(AbstractExpression exp in list)
{
exp.Interpret(context);
}
Console.Read();
}
解释器模式看起来好像不难,但其实真正做起来应该还是很难的,就如同你开发了一个编程语言或脚本给自己或别人用。解释器模式就是用'迷你语言’来表现程序要解决的问题,以迷你语言写成'迷你程序’来表现具体的问题。通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。
解释器模式也有不足的,解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理。
29.访问者模式
下面以成功或者失败时,男人和女人的反应为例画出的结构图
代码实现
//成功
class Success : Action
{
public override void GetManConclusion(Man concreteElementA)
{
Console.WriteLine("{0}{1}时,背后多半有一个伟大的女人。",concreteElementA.GetType().Name,this.GetType().Name);
}
public override void GetWomanConclusion(Woman concreteElementB)
{
Console.WriteLine("{0}{1}时,背后大多有一个不成功的男人。",concreteElementB.GetType().Name,this.GetType().Name);
}
}
//失败
class Failing : Action
{
//与上面代码类同,省略
}
//恋爱
class Amativeness : Action
{
//与上面代码类同,省略
}
这个模式使用有个前提:如果人类的性别不止是男和女,而是可有多种性别,那就意味'状态’类中的抽象方法就不可能稳定了,每加一种类别,就需要在状态类和它的所有下属类中都增加一个方法,这就不符合开放-封闭原则。也就是说,访问者模式适用于数据结构相对稳定的系统。
真正结构图
访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式。事实上,我们很难找到数据结构不变化的情况,所以用访问者模式的机会也就不太多了。