编辑推荐: |
本文主要介绍设计模式之引出模式,希望对您有所帮助。
本文来源于网络,由火龙果软件Linda编辑,推荐。 |
|
一、引出模式
场景:
我们在组装电脑时,通常会选择一系列的配件,比如CPU、硬盘、内存等。这里只讨论CPU和主板。
在选择CPU时,也会有它的品牌、型号、针脚数目等。
在选择主板时,也会有它的品牌、芯片组等。
在装机之前还有考虑这些配件之间的兼容性,也就是说,里面选择的各个配件之间是有关联的。
对于装机人来说,他只知道要组装电脑,需要相应的配件,但是具体用什么配件,还是客户说了算。
也就是说装机人只负责组装,而客户负责选择装机所需要的具体配件。
那我们使用程序来实现上面的过程吧!
对于客户,它需要选择自己需要的CPU和主板,然后告诉装机人自己的选择,接下来就等着装机人组装电脑。
对于装机人,它只知道要CPU和主板,而不知道具体需要什么类型的CPU和主板,我们可以将这定义成接口,很明显,我们可以用上简单工厂或工厂方法模式。
为简单演示,这里就是用简单工厂实现吧!
class Program { static void Main(string[] args) { Factory facroty = new Factory(); ICPU cpu = facroty.GetCpu(1); IMainboard mainboard = facroty.GetMainboard(1); cpu.Calculate(); mainboard.InstallCPU();
Console.ReadKey();
}
}
#region 简单工厂
public class Factory
{
/// <summary>
/// 选择CPU
/// </summary>
/// <param name="cpuType"></param>
/// <returns></returns>
public ICPU GetCpu(int cpuType)
{
ICPU cpu = null;
if (cpuType == 1)
{
cpu = new InterCpu(1000);
}
else if (cpuType == 2)
{
cpu = new AMDCpu(500);
}
else
{
cpu = new InterCpu(1000);
}
return cpu;
}
/// <summary>
/// 选择主板
/// </summary>
/// <returns></returns>
public IMainboard GetMainboard(int mainboardType)
{
IMainboard mainboard = null;
if (mainboardType == 1)
{
mainboard = new GAMMianborad(1000);
}
else if (mainboardType == 2)
{
mainboard = new MSIMianborad(500);
}
else
{
mainboard = new GAMMianborad(1000);
}
return mainboard;
}
}
#endregion
#region 产品
/// <summary>
/// CPU接口
/// </summary>
public interface ICPU
{
/// <summary>
/// 运算功能
/// </summary>
void Calculate();
}
/// <summary>
/// 主板接口
/// </summary>
public interface IMainboard
{
/// <summary>
/// 安装CPU
/// </summary>
void InstallCPU();
}
/// <summary>
/// InterCpu
/// </summary>
public class InterCpu : ICPU
{
/// <summary>
/// 初始化CPU针脚
/// </summary>
private int pins = 0;
/// <summary>
/// 构造函数初始化针脚数目
/// </summary>
/// <param name="pins"></param>
public InterCpu(int pins)
{
this.pins = pins;
}
public void Calculate()
{
Console.WriteLine("使用InterCpu,针脚数目是{0}", pins);
}
}
/// <summary>
/// AMDCPU
/// </summary>
public class AMDCpu : ICPU
{
/// <summary>
/// 初始化CPU针脚
/// </summary>
private int pins = 0;
/// <summary>
/// 构造函数初始化针脚数目
/// </summary>
/// <param name="pins"></param>
public AMDCpu(int pins)
{
this.pins = pins;
}
public void Calculate()
{
Console.WriteLine("使用AMDCpu,针脚数目是{0}", pins);
}
}
/// <summary>
/// 技嘉主板
/// </summary>
public class GAMMianborad : IMainboard
{
/// <summary>
/// CPU插槽孔数
/// </summary>
private int cpuHoles = 0;
/// <summary>
/// 初始化CPU插槽孔数
/// </summary>
/// <param name="cpuHoles"></param>
public GAMMianborad(int cpuHoles)
{
this.cpuHoles = cpuHoles;
} public void InstallCPU()
{
Console.WriteLine("使用技嘉主板,CPU插槽孔数是{0}", cpuHoles);
}
} /// <summary>
/// 微星主板
/// </summary>
public class MSIMianborad : IMainboard
{
/// <summary>
/// CPU插槽孔数
/// </summary>
private int cpuHoles = 0; /// <summary>
/// 初始化CPU插槽孔数
/// </summary>
/// <param name="cpuHoles"></param>
public MSIMianborad(int cpuHoles)
{
this.cpuHoles = cpuHoles;
} public void InstallCPU()
{
Console.WriteLine("使用微星主板,CPU插槽孔数是{0}", cpuHoles);
}
}
#endregio
|
结果:
好了,看了上面的代码,在运行下,不是解决了这个问题了?
那我们把客户端代码稍微改一下,比如这样
class Program { static void Main(string[] args) { Factory facroty = new Factory(); ICPU cpu = facroty.GetCpu(1); //注意这里改变喽 IMainboard mainboard = facroty.GetMainboard(2); cpu.Calculate(); mainboard.InstallCPU();
Console.ReadKey();
}
} |
在看下结果:
好了,看出问题了吗?客户端选择的CPU针脚数是1000,而选择的主板的CPU插槽只有500针,压根就无法进行组装。
小结下,简单工厂解决了:对于装机人,只知道CPU和主板的接口,而不知道具体实现的问题,但还有一个问题没解决?
那就是这些CPU对象和主板对象其实是有关系的,是需要相互匹配的。而简单工厂,并没有维护这种关联关系。
二、认识模式
1.模式定义
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。(通过引出模式模式部分的讲解,相信大家对这句话有了足够的理解)
2.解决思路
仔细分析上面问题,其实有两个问题点:
一个是只知道所需要的一系列对象的接口,而不知道具体实现;
另一个是这一系列对象是相关或者相互依赖的,也就是说既要创建接口对象,还要约束它们之间的关系。
这里的问题与工厂方法或简单工厂解决的问题是有很大不同的,工厂方法或简单工厂关注的是单个产品对象的创建,比如创建CPU的工厂方法,他就只关心如何创建CPU对象。
这里要解决的是,要创建一系列的产品对象,而且这一系列对象是构建新的对象所需要的组成部分,也就是这一系列被创建的对象相互之间是有约束的。
解决这个问题的一个解决方案就是抽象工厂。
在这个模式中,会定义一个抽象工厂,在里面虚拟地创建客户端需要的这一系列对象,所谓虚拟的就是定义创建这些对象的抽象方法,并不去实现,然后由具体的抽象工厂子类来提供这一系列对象的创建。
这样就可以为同一个抽象工厂提供很多不同的实现,那么创建的这一系列对象也就不同了。也就是说,抽象工厂在这里起一个约束作用,并提供所有子类的一个统一外观,来让客户端使用。
3.模式结构(请使用鼠标右键——在新标签页打开链接)
AbstractFactory:抽象工厂,定义创建一系列产品对象的接口,通俗点就是定义生产线,在本例中约束了装机方案需要的标准配件。
Scheme1:具体的工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建。在本例中是有AMDCPU和GAMainboard主板组成的装机方案。
ICPU:定义一类产品的接口,在本例中就是定义CPU的接口。
InterCPU:具体的产品实现的对象,通常在具体工厂里面,会选择具体的产品实现对象,来创建符合抽象工厂定义的方法返回的产品类型。
ComputerEngineer:在这里充当装机人的角色,具体的装机工作交给他,其实就是对客户端的进一步封装,使的客户端代码用起来更少,更方便。
4.示例代码
class Program { static void Main(string[] args) { //创建装机工程师 ComputerEngineer enginner = new ComputerEngineer();
//客户选择装机方案
AbstractFactory schema = new Schema1(); //开始组装电脑
enginner.MakeComputer(schema);
Console.ReadKey();
}
}
#region 组装人
/// <summary>
/// 组装人
/// </summary>
public class ComputerEngineer
{
private ICPU cpu = null;
private IMailboard mailboard = null;
public void MakeComputer(AbstractFactory schema)
{
//1.准备装机所需配件
this.PrepareHardwares(schema);
//2.组装电脑
//3.测试电脑
//4.交付客户
}
/// <summary>
/// 准备装机所需配件
/// </summary>
/// <param name="schema"></param>
private void PrepareHardwares(AbstractFactory schema)
{
this.cpu = schema.CreateCPU();
this.mailboard = schema.CreateMailboard();
this.cpu.Calculate();
this.mailboard.InstallCpu();
}
}
#endregion
#region CPU产品
/// <summary>
/// CPU
/// </summary>
public interface ICPU
{
void Calculate();
}
public class InterCPU : ICPU
{
private int pins = 0;
public InterCPU(int pins)
{
this.pins = pins;
}
public void Calculate()
{
Console.WriteLine("IntelCPU针脚有" + this.pins);
}
}
public class AMDCPU : ICPU
{
private int pins = 0;
public AMDCPU(int pins)
{
this.pins = pins;
}
public void Calculate()
{
Console.WriteLine("AMDCPU针脚有" + this.pins);
}
}
#endregion
#region 主板产品
public interface IMailboard
{
void InstallCpu();
}
public class MSIMailboard : IMailboard
{
private int cpuHoles = 0;
public MSIMailboard(int cpuHoles)
{
this.cpuHoles = cpuHoles;
}
public void InstallCpu()
{
Console.WriteLine("MSI主板针座" + this.cpuHoles);
}
}
public class GAMailboard : IMailboard
{
private int cpuHoles = 0;
public GAMailboard(int cpuHoles)
{
this.cpuHoles = cpuHoles;
}
public void InstallCpu()
{
Console.WriteLine("GA主板针座" + this.cpuHoles);
}
}
#endregion
#region 工厂
/// <summary>
/// 抽象工厂
/// </summary>
public interface AbstractFactory
{
ICPU CreateCPU();
IMailboard CreateMailboard();
}
public class Schema1 : AbstractFactory
{
public ICPU CreateCPU()
{
return new AMDCPU(2048);
}
public IMailboard CreateMailboard()
{
return new GAMailboard(2048);
}
}
public class Schema2 : AbstractFactory
{
public ICPU CreateCPU()
{
return new InterCPU(1024);
}
public IMailboard CreateMailboard()
{
return new MSIMailboard(1024);
}
}
#endregion
|
三、理解模式
1.抽象工厂的功能
抽象工厂的功能是为一系列相关对象或相互依赖的对象创建一个接口,注意这个接口内的方法不是随意定义的,而是一系列相关或相互依赖的方法。
抽象工厂其实是一个产品系列,也就是产品簇。上面例子中的抽象工厂就可以看成电脑簇,每个不同的装机方案,就是一种具体的电脑系列。
2.实现成接口
AbstrctFactory通常会实现成为接口,当然,如果需要为这个产品簇提供公共的功能,也可以将AbstrctFactory实现成抽象类,但一般不会这么做。
3.使用工厂方法
AbstrctFactory定义了创建产品所需要的接口,具体的实现是在具体实现类里面,在实现类里面就需要选择多种更具体的实现。所以AbstrctFactory定义的创建产品的方法看成是工厂方法,也就是说使用工厂方法来实现抽象工厂。
4.模式优点
1)分离接口和实现
客户端使用抽象工厂来创建需要的对象,而客户端根本就不知道具体的实现是谁,客户端知只是面对产品接口编程而已。就是说,客户端从具体的产品实现中解耦了。
2)使得切换产品簇变得容易
因为一个具体的工厂实现代表一个产品簇,比如例子中的Scheme1和Scheme2,客户端选用不同的工厂实现,就相当于是在切换不同的产品簇。
5.模式缺点
1)不要容易扩展新的产品
如果需要给整个产品簇添加一个新的产品,那么就要修改抽象工厂,这样就会导致修改所有的工厂实现类。
2)容易造成类层次复杂,增加代码的复杂性。
6.模式的选用
1)如果希望一个系统独立于它的产品创建、组合和表示时。就是说,希望一个系统只知道产品的接口,而不关心实现的时候。
2)如果一个系统有多个产品系列中的一个来配置时。换句话说,就是可以动态切换产品簇时。
3)如果要强调一系列相关产品的接口,以便联合使用它们的时候。
7.模式的总结
抽象工厂的本质是选择产品簇的实现。
工厂方法是选择单个产品的实现,虽然一个类里面可以有多个工厂方法,当时这些方法之间一般是没有联系的,即使看起来有联系。
但是抽象工厂着重的就是为一个产品簇选择实现,定义在抽象工厂里面的方法通常是有联系的,它们都是产品的某一部分或者是相互依赖的。
下一篇,准备是来学习下三种工厂模式之间的区别和联系,以及一些扩展。
设计模式之三种工厂的区别与联系(五)
前面已经学习简单工厂、工厂方法以及抽象工厂,本篇学习的是探究这三种工厂之间的区别与联系。
在面向对象的设计中,对象的管理是其核心所在。其中,对象的创建是对象管理的第一步。对象的创建非常简单,在C#中,只需要使用new操作符调用对象的构造函数即可。因此,管理对象最重要的是掌握创建对象的时机。
我们首先从对象的特征来看。代表抽象意义的类型,有接口和抽象类,但它们是不能创建对象实例的,这就意味着我们要创建的对象都是与具体的对象类型有关的。也就是说,创建对象的过程必将涉及设计中的实现细节,从而导致创建者与具体的被创建者之间的耦合增强。
从而就引出了工厂三剑客了!
简单工厂:提供一个创建对象实例的功能,而无需关心其具体的实现。
在这里,被创建的对象可以是接口、抽象类,也可以是具体的类。
通常情况下是用来创建接口的,但是也可以用来创建抽象类,甚至是一个具体的类实例。
简单工厂的本质在于选择实现,实现是已经做好了的。就算实现再简单,也要由具体的实现类来实现,而不是在简单工厂里面来实现。简单工厂的目的在于为客户端来选择相应的实现,从而使得客户端和实现之间解耦。
工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
首先,定义中的接口,不是编程里面的Interface,它是一个抽象的概念,你可以理解为“方法”。也就是说,定义一个用于创建对象的方法。这下,会对定义明白很多了。
准确地说,工厂方法模式是将创建对象实例的责任,转移到了工厂类中,并利用抽象的原理,让实例化行为延迟到具体工厂类。
工厂方法模式主要功能是让父类在不知道具体实现的情况下,完成自身的功能调用;而具体的实现延迟到子类来实现。
工厂方法模式的本质是延迟到子类来选择实现。
工厂方法模式中的工厂方法,在真正实现的时候,一般是先选择使用哪一个具体的产品实现对象,然后创建这个具体产品对象的实例,最后就可以返回回去了。
值得一提的是,引入工厂对象并不是简单地为产品创建相应的工厂,而是要划分各个模块不同的职责,将工厂对象的创建方法放在合适的地方。最佳的方案就是将创建工厂对象的职责集中起来,放到一个模块中,而不是在需要创建产品时,才创建工厂对象。
抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂主要的两个问题点:一个是只知道所需要的一系列对象的接口,而不知道具体实现;
另一个是这一系列对象是相关或者相互依赖的,也就是说既要创建接口对象,还要约束它们之间的关系。
抽象工厂的功能是为一系列相关对象或相互依赖的对象创建一个接口,注意这个接口内的方法不是随意定义的,而是一系列相关或相互依赖的方法。
抽象工厂的本质是选择产品簇的实现。
抽象工厂着重的就是为一个产品簇选择实现,定义在抽象工厂里面的方法通常是有联系的,它们都是产品的某一部分或者是相互依赖的
三种工厂的区别与联系
简单工厂和抽象工厂模式
简单工厂是用来选择实现的,可以选择任意接口的实现。一个简单工厂的可以有多个用于选择并创建对象的方法,多个方法创建的对象可以是有关系的也可以是没关系的。
抽象工厂模式是用来创建产品簇的实现的,也就是说一般抽象工厂里面有多个用于选择并创建对象的方法,但是这些方法所创建的对象之间通常是有关系的,这些被创建的对象通常是构成一个产品簇所需要的部件对象。
所以从某种意义上来说,简单工厂和抽象工厂是类似的,如果抽象工厂退化成为只有一个实现,不分层次,注意不分层次,那就相当于简单工厂了。
简单工厂和工厂方法模式
简单工厂和工厂也是类似的。
工厂方法的本质也是用来选择实现的,与简单工厂的区别在于工厂方法是把选择具体实现的功能延迟到了子类。
如果把工厂方法选择的实现方法父类直接实现,那就等同于简单工厂。
工厂方法模式和抽象工厂模式
工厂方法模式一般针对单独的产品对象的创建,虽然一个类里面可以有多个工厂方法,但是这些方法之间一般是没有联系的。
抽象工厂模式注重产品簇对象的创建,定义在抽象工厂里面的方法通常是有联系的,它们都是产品的某一部分或者相互依赖。
当抽象工厂的产品簇只有一个产品时,抽象工厂也就退化成工厂了。
在抽象工厂的实现中,还可以使用工厂方法来提供抽象工厂的具体实现,也就是说它们可以组合。
设计模式之初识IoC/DI(六)
本篇和大家一起学习IoC和DI即控制反转和依赖注入。
当然听上去这词语非常的专业,真不知道是怎么组出来的,看上去难归看上去难,但稍微理解一下也就这么回事了。
首先我们要明白IoC/DI干嘛用的,不然别人问“你知道IoC/DI吗?”,“知道啊!”,“那干嘛用的?”,呵呵,就郁闷了,其实IoC/DI的作用就两个字——解耦,说一千道一万,还是离不开这两个字。所以,IoC/DI的作用就是解耦。
至于为什么要解耦,解耦在面向对象开发中的重要性在这里就不做细说了。
下面我们来认识一下它们的真面目吧!
要理解IoC/DI两个概念,就必须搞清楚如下问题:
1.参与者都有谁?
2.依赖:谁依赖于谁?为什么需要依赖?
3.注入:谁注入谁?到底注入了什么?
4.控制反转:谁控制谁?控制什么?为什么叫反转?有没有正向呢?正向又是什么?
5.依赖注入和控制反转是同一概念吗?
下面来简要回答一下上述问题,把这些问题搞明白了,也就明白了IoC/DI。
1.参与者都有谁:一般有三方参与,一个是某个对象,一个是IoC/DI容器,还有就是某个对象的外部资源。
注:某个对象指任意一个普通的对象,IoC/DI容器简单说就是用来实现IoC/DI功能的一个框架程序,比如现在有许多IoC/DI框架(Unity、Spring.NET、Autofac),对象的外部资源指对象需要的,但是从对象外部获取的,都统称资源,比如对象需要其他对象。
2.谁依赖于谁:当然是某个对象依赖于IoC/DI容器
3.为什么需要依赖:因为对象需要IoC/DI的容器来提供对象需要的外部资源。
4.谁注入谁:IoC/DI的容器注入某个对象
5.到底注入了什么:就是注入某个对象所需要的外部资源,说完整点就是IoC/DI的容器讲某个对象所需要的外部资源注入了该对象。
6.谁控制谁:IoC/DI的容器来控制对象
7.控制什么:主要是控制对象实例的创建
8.为什么叫反转:反转是相对与正向而言的。
9.正向又是什么:通常情况下的应用程序,如果在A里使用C,一般我们会直接创建C的对象,也就是说,在A类中主动获取所需要的外部资源,这种情况被成为正向的,那什么是反向呢?就是A类不再主动去获取C,而是被动等待,等待IoC/DI的容器获取一个C的实例,然后反向地注入到A类中。
先看没有IoC/DI的时候,常规A类使用C的示意图
当有了IoC/DI的容器后,A类不再主动去创建C了,而是被动等待,等待IoC/DI的容器获取C的实例,然后反向注入到A类中。
依赖注入和控制反转是同一概念吗?
根据上面讲述,依赖注入和控制反转是对同一件事情的不同描述。依赖注入使用应用程序的角度去描述,我们可以把依赖注入描述的完整点:应用程序依赖容器创建并注入它所需要的外部资源。控制反转是从容器的角度去描述的,描述的完整点就是:容器控制应用程序,由容器反向的向应用程序注入其所需要的外部资源。
好了,通过上述学习,大家都应该对IoC/DI有了基本了解,下面我们再来正式认识下依赖注入和控制反转。
依赖注入中,依赖关系就像被注入的液体,我们可以在任何时候将依赖关系注入到模块中,而不只局限于编译是绑定。这种依赖关系是通过注入的方式完成的,就意味着我们可以随时更新,因为注入的液体与模块本身并无直接关联,举个不恰当的比喻,我们难道天生就要和针筒打交道吗?
实现依赖注入的前提是面向接口编程,辅助的技术可以使用反射技术。
MF.将依赖注入的形式分为三种:构造函数注入、设置方法注入和接口注入。
接口注入是通过定义接口约束的方式实现依赖注入,会给容器带来设计的限制。
而构造函数注入与设置方法(可以是属性)注入则是使用类的构造函数以及自定义的setter方法或属性进行依赖注入(关于这两点,可以参考第一篇《设计模式之UML类图的常见关系(一)》中的聚合和组合关系)。
在这里我们也把第一篇聚合和组合关系类图拿到了。
下面这个是使用Setter方法/属性实现依赖注入的。说白了就是定义一个Setter方法/属性,在该方法/属性内将对象关联起来。
下面这个是构造函数的依赖注入,就是将Setter方法/属性换成了构造函数,但是请仔细看看两幅图片的代码和注入,什么时候使用Setter方法/属性注入,什么时候使用构造函数注入,当时在开发过程中,通常是使用构造函数进行依赖注入的。
还有一个就是接口注入。
这是我认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:
public interface InjectFinder { void injectFinder(MovieFinder finder); } |
接下来,ServiceUser必须实现这个接口:
class MovieLister: InjectFinder { private MovieFinder finder; public void injectFinder(MovieFinder finder) { this.finder = finder; } } |
为什么现在介绍IoC/DI呢?
想想,前面我们已经学习了三种工厂,那能不能将工厂和IoC/DI联系起来呢?好了,下面就自己想想!
小结:IoC/DI对编程带来的最大改变不是在代码上,而是在思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动了,被动的等待IoC/DI容器来创建并注入它所需要的资源。
|