开个坑,接下来写完设计模式,其实这个东西一直很重要,但是自己却不重视,直到在很多地方吃了亏才想起来,我之前看起来设计模式有点“无所谓”,好像代码知道具体的技术和知识点就可以包打天下了。一个语言推出的各种各样的语法,特性都是根据代码的需求来的,而设计模式则是需求的规范化和标准化,所以对于一门语言来说,设计模式就是基石,不用设计模式的角度来看我们的代码,那对它一切的理解就是空中楼阁。
设计模式有六大法则,六大法则根据不同的项目场景就有了23种设计模式,分为创建型、结构型和行为型。一般国内很多人学设计模式都看《大话设计模式》(虽然里面有的例子太棒读了),我也是参照这本书,然后写一些笔记吧,权当自己学习的沉淀了。
所有的设计模式都是根据以下六个设计模式原则来的。
六大设计模式原则:
1.单一职责原则:就一个类而言,应该只有一个引起它变化的原因。
如果一个类中的职责(所要做的事情)越多,那么不同的职责的耦合度就越高。因此,一个职责的变化很可能削弱类完成其他职责的能力。
例如C#的设计中,枚举接口IEnumerable和IEnumerator一分为二,IEnumerable中负责返回一个IEnumerator的值,即符合单一职责原则,分成两个接口避免过多的耦合。
2.开放封闭原则:对于软件实体(类、模块、函数)来说,应当支持扩展而不支持封闭
代码中,编码人员必须对于代码的变化情况作出判断,确定哪些地方是需要留出扩展而不支持更改的。然后构造抽象来隔离变化。同样的,对于程序的改动也应该尽量采取增加新代码的方式,而不是更改现有的代码的方式。在下文中的简单工厂到抽象工厂就是这个原则的例子。
3.依赖反转原则:1.高层模块和底层模块不应该互相依赖,而是都依赖于抽象。
2.抽象不应该依赖于细节,细节应该依赖于抽象。
抽象在我的理解中是分层级的,虚函数——纯虚函数——抽象类——纯抽象类——接口,但是依赖反转原则里的抽象一般是说的接口,当然并不是说抽象类或者虚函数不能完成抽象的功能。
4.里氏替换原则:子类必须能够替换父类型。
对于类的继承来说,这一点非常常见,很多类继承的特性都是以这个原则为基石的,这个与上文中的依赖反转一起用有非常好的效果,即接口引用指向实现接口的类实例的地址进而调用接口的方法。
在C#中的“编译多态”也就是对抽象类和虚函数的重写,和里氏替换原则密不可分。
5.接口隔离原则:1.对于一个类不需要的接口就没必要继承
2.类之间的依赖应该建立在最小的接口上。使得逻辑块可以高内聚。
这个原则要求代码中尽量建立单一函数或功能的接口,而不是那些庞大臃肿的接口,如果一个类要实现多个接口也没关系,多继承就好了。在C#中我们看到的许多接口也都是内部较为简单的接口,例如ICloneable、IDisposable、ICompareable接口等,里面都只有一个函数的声明。
但是接口过多也会导致一个类继承的接口太过繁杂,这样会提高维护的难度。这一点需要在设计接口的时候好好把握。
6.迪米特原则:一个类对于它耦合或调用的类知道的最少
它也叫最少知识原则,一个类对其他类知道的内容越少(即调用其他类的函数或字段越少),那么对这个类的耦合也就越小。
我们看到在第三篇行为型的博客中,中介者模式就是迪米特原则的最好应用。
创建型模式
创建型模式顾名思义,也就是负责创建对象的模式,我们可以理解这种模式是对实例化的抽象。它帮助一个系统独立与如何创建组合和表示它的对象。它负责对创建的对象进行了封装,其他端口需要调用这个对象时,只需要关心如何使用,而不需要去关系这个对象怎么来的。
这是设计模式的第一篇博客,我把创建型的设计模式写完。
单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式常见于很多对象的需求,例如一个窗口对象只能生成一次,我们在开发中当然可以控制其他地方只调用一次,但是你不能保证每个用户都对一个生成窗口的按钮只点一次。所以单例模式的需求就应运而生。
如果只让一个类对象只生成一次的话,办法很多,比如,在new的时候用个布尔值进行限制就好了。在之前GC那篇的时候,讲到过非托管类型需要使用终结器Finalize自动销毁或者Dispose方法来手动销毁。同样的,既然生成的时候修改了那个布尔值,那么销毁的时候也要修改,频繁的这样做,会产生大量的重复代码。
在标准的C#单例模式中,有三个特点:
- 同过私有的构造函数防止外部通过new生成实例。
- 类的静态成员由于在类的编译就生成在专用的静态储存区中,所以一个类的静态成员只能有一个实例。所以使用了静态的实例和一个获得该实例的方法。
- 如果需要申请内存空间来生成实例,必须对多线程的安全性予以考虑,采用Lock关键字避免生成多个单例对象引起冲突。
代码如下:
class testClass
{
private static testClass instance = null;
private testClass()
{
Console.WriteLine("实例化了");
}
public static testClass getInstance()
{
if (instance == null)
{
lock(instance)
{
instance = new testClass();
}
}
return instance;
}
}
这里的一个字段和两个函数方法都不可或缺,注意构造函数是私有的,而公有的只有获得私有静态实例的getInstance实例。
我们用一个用例测试一下:
class Program
{
static void Main()
{
testClass test1 = testClass.getInstance();
testClass test2 = testClass.getInstance();
if (test1.Equals(test2))
{
Console.WriteLine("两次其实都生成了一个对象");
}
}
}
结果很显然:
简单工厂:将对象的创建和对象的使用分离
简单工厂适用于很多格式相似但是功能不同的类对象,例如一个计算器不同的运算或者同产品不同细节的品类的选择。一般这样类的实例可以用if-else或者switch语句来生成不同实例,但是简单工厂中通过一个类将这样繁杂的功能包装起来,对于调用实例者(在我们这里就是主函数)来说封装了实例的创建,将对象的创建和对象的使用分离了开来。
如下是一个计算器的调用,将具体操作符的选定被包装到的OperationFactory中的getOperation方法中:
public class OperationFactory
{
public Operation getOperation(string operatType)
{
Operation operater;
switch(operatType)
{
case "+":
operater = new OperationAdd();
break;
case "-":
operater = new OperationSub();
break;
case "*":
operater = new OperationMul();
break;
case "/":
operater = new OperationDiv();
break;
default:
operater = null;
break;
}
return operater;
}
}
public abstract class Operation
{
public abstract float Operat(float a, float b);
}
public class OperationAdd : Operation
{
public override float Operat(float a, float b)
{
return a + b;
}
}
public class OperationSub : Operation
{
public override float Operat(float a, float b)
{
return a - b;
}
}
public class OperationMul : Operation
{
public override float Operat(float a, float b)
{
return a * b;
}
}
public class OperationDiv : Operation
{
public override float Operat(float a, float b)
{
return a / b;
}
}
这样对于主函数来说,具体的Switch选择对它来说就是封闭不可见的,实现了前后端的分离,我们创建操作符的时候只需要传入操作符的Type,而不需要知道getOperation是如何实现的:
class Program
{
static void Main()
{
float a = Convert.ToSingle(Console.ReadLine());
float b = Convert.ToSingle(Console.ReadLine());
Operation operater = OperationFactory.getOperation(Console.ReadLine());
float result = operater.Operat(a, b);
Console.WriteLine("结果是" + result.ToString());
}
}
工厂模式:在简单工厂的前后分离上通过创建接口将类的实例化延迟至子类
在上文的简单工厂中,如果我们要对运算符进行扩展,例如添加一个运算符,我们除了对运算符类进行添加以外,还需要修改getOperation方法内部逻辑(在Switch语句里面加入新的内容),但这是不符合开闭原则的。
因此,工厂模式在简单工厂的前后分离的基础上将依赖的目标转为了抽象的接口。进而将操作的对象的实例化转到了子类中,在添加功能时,避免了对某一个函数中的逻辑进行修改,而是添加对接口的继承,这样做迎合了开闭原则。
我们将上文简答工厂的例子进行改变,原先的Operation和它的子类不变,增加一个IOperator接口,然后分别实现四个操作符的子类,然后修改OperationFactory中的方法,改为使用接口的里氏替换原则去实现一个Operator实例,主要的修改如下文:
public interface IOperator
{
Operation GetOperation();
}
public class getOperatAdd : IOperator
{
public Operation GetOperation()
{
return new OperationAdd();
}
}
public class getOperatSub : IOperator
{
public Operation GetOperation()
{
return new OperationSub();
}
}
public class getOperatMul : IOperator
{
public Operation GetOperation()
{
return new OperationMul();
}
}
public class getOperatDiv : IOperator
{
public Operation GetOperation()
{
return new OperationSub();
}
}
public class OperationFactory
{
public static Operation getOperation(IOperator operatType)
{
return operatType.GetOperation();
}
}
但是并不是说具体使用哪个操作符代码可以完全自己判断(这也不太现实),因为在创建指向实现接口的子类的实例的引用时仍让需要修改具体子类的对象。我们在主函数中仍然需要这样写:
class Program
{
static void Main()
{
float a = Convert.ToSingle(Console.ReadLine());
float b = Convert.ToSingle(Console.ReadLine());
Operation operater = OperationFactory.getOperation(new getOperatAdd());
//这里仍然需要自己指定
float result = operater.Operat(a, b);
Console.WriteLine("结果是" + result.ToString());
}
}
这样看起来比简单工厂的代码量要多得多,但是更好扩展了,以前的判断实例的生成全靠一个难以维护的Switch语句,而在工厂模式中添加功能只需要添加实现功能的类和继承IOperator的类就行了,剩下的交给OperationFactory去判断,代码的可扩展性大大增强了。
那么,我们试着扩展一个计算功能,增加一个取余:
public class getOperationYU : IOperator
{
public Operation GetOperation()
{
return new OperationYU();
}
}
public class OperationYU : Operation
{
public override float Operat(float a, float b)
{
return a % b;
}
}
如上,不需要关于Operation的任何代码,只需要在主函数(前端)修改new语句就好了:
Operation operater = OperationFactory.getOperation(new getOperationYU());
//这里仍然需要自己指定
抽象工厂:引入了产品族的概念,并使用一个创建一系列相关或相互依赖对象的接口来定义产品族。
既然工厂模式已经很好很强大了,那为什么需要抽象工厂呢,因为抽象工厂引入了产品族的概念,我们创建一个对象,并不止需要创建某一种对象,有时候需要生成一系列的对象,例如说一些汽车品牌,虽然很多汽车品牌名字不同,品质不同,但是都有主打特定人群的产品线。例如很多汽车厂家都有SUV和小轿车、电动汽车这三个产品,这三个产品构成了一个产品族。不同的汽车品牌对这个产品族有不同的实现。
在代码中,抽象工厂有以下几个模块:
- 抽象工厂:一般是一个抽象类或者接口,定义了有哪些产品族类别。
- 具体工厂:继承自抽象工厂,对应抽象工厂里产品族,返回对应了哪些产品族的具体产品实例。
- 抽象产品族:定义了一个类型的产品应该有哪些特征或者类别。
- 具体产品:根据抽象产品族来实现具体产品的类。
由于上面那个汽车的例子已经烂大街了,而我这辈子都可能不会买车,所以换个例子写一写。
既然我爱玩游戏,那就写个游戏公司的例子。对于3A游戏来说,枪车球这三个大类里面占了很大分量。我们今天主要说说枪和车这两个类别。对于枪(射击)和车(竞速)这两种游戏,不同公司开发了不同的竞品,例如育碧枪有彩虹六号、车有飙酷车神。索尼枪有神秘海域、车有GTS。EA枪有战地、车有极品飞车。微软枪有光环、车有极限竞速。
我们把枪和车看成两个产品族,不同的公司都对这两个产品族有各自的实现(开发不同类别的游戏),而这些公司生产的产品族又隶属于不同的产品族中。
那么,上文这句话,转换为抽象工厂模式就有如下模块:
抽象产品族:有枪战游戏和竞速游戏两种产品族:我们定义两个抽象类来实现它:
//抽象产品族:第一人称射击游戏
abstract class FPS
{
public string FPSGameName;
public abstract void FPSshow();
}
//抽象产品族:竞速游戏
abstract class Race
{
public string RaceGameName;
public abstract void RaceShow();
}
具体产品:我们两个产品族也有如下游戏,FPS中EA的战地、微软的光环。RACE中EA的极品飞车、微软的极限竞速,它们分别继承于以上两个产品族中:
//具体产品:第一人称射击游戏光环
class HALO : FPS
{
public HALO()
{
FPSGameName = "光环";
}
public override void FPSshow()
{
Console.WriteLine("这款FPS游戏的名字叫"+ FPSGameName);
}
}
//具体产品:第一人称射击游戏战地
class BattleField:FPS
{
public BattleField()
{
FPSGameName = "战地";
}
public override void FPSshow()
{
Console.WriteLine("这款FPS游戏的名字叫" + FPSGameName);
}
}
//具体产品,竞速游戏极品飞车
class NeedForSpeed : Race
{
public NeedForSpeed()
{
RaceGameName = "极品飞车";
}
public override void RaceShow()
{
Console.WriteLine("这款竞速游戏的名字叫" + RaceGameName);
}
}
//具体产品,竞速游戏极限竞速
class Froza : Race
{
public Froza()
{
RaceGameName = "极限竞速";
}
public override void RaceShow()
{
Console.WriteLine("这款竞速游戏的名字叫" + RaceGameName);
}
}
抽象工厂:定义了一个具体工厂中该有哪些产品族,由上我们知道是枪类和车类:
//3A级游戏抽象工厂,定义了这样的抽象工厂里有哪些产品族
interface ITripulAGame
{
FPS CreateFPS();
Race CreateRace();
}
具体工厂:继承自上面的抽象工厂,里面描述了具体生产(也就是new)哪些产品:
//具体工厂EA:生产战地系列和极品飞车系列
class EA : ITripulAGame
{
public FPS CreateFPS()
{
return new BattleField();
}
public Race CreateRace()
{
return new NeedForSpeed();
}
}
//具体工厂微软:生产光环系列和极限竞速系列
class Microsoft : ITripulAGame
{
public FPS CreateFPS()
{
return new HALO();
}
public Race CreateRace()
{
return new Froza();
}
}
以上就构成了抽象工厂的具体类别,这个抽象工厂有什么用呢,比如说有个游戏商店,他想要摆一批某游戏公司的3A游戏。那么有两个公司可以选,第一个是EA、第二个是微软。当然,可以一个个游戏类型摆上去(就是对某个族实例一个个去new),但是那样做远不如这样方便,我们在客户端(主函数)演示一下这样的代码:
class Program
{
static void Main()
{
ITripulAGame game = new EA();
FPS fpsGame = game.CreateFPS();
Race raceGame = game.CreateRace();
fpsGame.FPSshow();
raceGame.RaceShow();
}
}
输出为:
上面的代码看起来平平无奇,但是如果我们要切换到微软的公司的游戏上,只需要改一个地方(即具体工厂实例名),这样代码的灵活度就上了个台阶了:
class Program
{
static void Main()
{
TripulAGame game = new Microsoft();
//只改动了公司名字
FPS fpsGame = game.CreateFPS();
Race raceGame = game.CreateRace();
fpsGame.FPSshow();
raceGame.RaceShow();
}
}
输出的内容就彻底地发生了变化:
原型模式:用原型实例指定创建对象的种类,并且通过拷贝原型创建新的对象
原型模式的实现,可以通过Clone接口或者返回Object中的MemberwiseClone(浅引用表)来实现,但实际上,实现原型模式用序列化可以轻松的实现深拷贝。这个在之前的序列化那一篇已经写过了,这里就不展开写代码了。
建造者模式:将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
建造者模式适用于一个类内部完成实现比较复杂,但不同的对象需要产生一定细微的差别的情况,所有的对象都要实现同样的功能,但实现功能的细节有区别,那么我们可以将对象的创建和完善分离开来,然后针对不同细节创建不同的内容。
建造者模式需要一个指挥者,这个指挥者负责统一调用对象的方法来对对象的细节进行完善。
在下文的例子中,我们假设一个游戏玩家(Player类)需要生成不同体型的游戏人物(Person),不同的游戏人物的体格有区别(可以认为越构建身体的值越高人物越胖,因此护甲越高但是敏捷下降),通过IPerson标明了人物实现要构建哪些内容,通过IPerson接口实现了一个DefaultPerson(普通体格)类和一个ThinPerson(瘦子体格)类,它们内部的需要的Data不同,但是需要实现的功能是相似的(都需要头、身体、手脚):
public class Player
{
public int playerCode;
public Player(int code)
{
playerCode = code;
}
public void Build(float data)
{
Console.WriteLine("玩家编号"+playerCode+"创造的是" + data);
}
}
interface IPerson
{
void BuildHead();
void BuildBody();
void BuildArm();
void BuildLeg();
}
public class DefaultPerson:IPerson
{
private Player player;
public DefaultPerson(Player p)
{
player = p;
}
public virtual void BuildHead()
{
player.Build(1f);
}
public virtual void BuildBody()
{
player.Build(2f);
}
public virtual void BuildArm()
{
player.Build(0.5f);
}
public virtual void BuildLeg()
{
player.Build(0.5f);
}
}
public class ThinPerson : IPerson
{
private Player player;
public ThinPerson(Player p)
{
player = p;
}
public void BuildArm()
{
player.Build(0.5f);
}
public void BuildBody()
{
player.Build(1);
}
public void BuildHead()
{
player.Build(0.5f);
}
public void BuildLeg()
{
player.Build(0.5f);
}
}
而统一实现身体的代码,则通过指挥者类PersonDirector来实现:
class PersonDirector
{
public IPerson person;
public void CreatePerson()
{
person.BuildHead();
person.BuildBody();
person.BuildArm();
person.BuildLeg();
}
}
我们在前端的代码就非常简单了,我们尝试两个玩家创建一个普通身材一个瘦子的代码:
class Program
{
static void Main()
{
PersonDirector director = new PersonDirector();
Player playerA = new Player(1);
IPerson personA = new DefaultPerson(playerA);
director.person = personA;
director.CreatePerson();
Player playerB = new Player(2);
IPerson personB = new ThinPerson(playerB);
director.person = personB;
director.CreatePerson();
}
}
我们可以看到输出结果:
以上就是创造型的设计模式。
设计模式中的依赖注入
这一部分写于我三篇设计模式的博客发布完成以后,所以很多内容在第一章的里面没有体现(如果您需要看这一部分可能需要看完剩下章节的设计模式然后回过来),我想归纳一下依赖注入的一些问题,因为这个概念困扰我很久了,但是我没有找到太多好的资料来解决它,我觉得写完23个设计模式以后是时候归纳一下它了。我首先说说我的一些理解:
依赖注入是我们在代码结构迎合依赖倒置原则和里氏替换原则时的一种行为。
依赖倒置原则我们上面就说过了,通过依赖抽象来隔离模块之间的变化。里氏替换原则则是依赖倒置原则的实现的前提,通过使用接口的抽象对象来替代实现接口的具体对象,进而达成依赖倒置原则。那么可以认为,依赖注入则是我们使用接口的抽象对象这种行为的描述。(我甚至看到一些博客比较激进的说“有使用抽象类型的时候就一定存在依赖注入”,虽然比较激进但是我觉得讲得一语中的)
依赖注入的解释:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。
那么,我们的代码中使用依赖注入的情况在设计模式中有三种情况:
1:Setter注入:在客户类中,设置一个接口类型的数据成员,并设置一个Set方法作为注入点,这个Set方法接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员(里氏替换)。
我们在设计模式中能看到许多Setter注入的例子:
例如,在工厂模式中,OperationFactory里面的GetOperation方法里面参数IOperator接口对象,并传入实现IOperator的具体Operater运算符,就是相当于将依赖注入到了这个OperationFactory对象中,此时注入点为getOperation函数。
例如,在桥接模式中,实现抽象行为的Player的抽象基类保存一个抽象行为的基类Action的对象,并传入实现行为的具体对象(例如Fly和Walk对象)。那么,Action的依赖注入到了Player中,此时的注入点为setAction函数。
2:构造函数注入:是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。
构造函数注入与Setter注入的区别不大,只是将注入点从设定好的函数转移到了构造函数里,甚至如果二者对代码的影响不大是可以随意切换的。
但是由于构造注入只能在实例化客户类时注入一次,程序运行期间是没法改变一个客户类对象内的服务类实例的。
我们可以将上面的代码模块稍作修改即可改为构造函数注入,即第一个例子中的改为:
public class OperationFactory
{
private IOperator operatType;
public OperationFactory(IOperator operatType)
{
this.operatType = operatType;
}
public Operation GetOperator()
{
return operatType.GetOperation();
}
}
那么主函数中的调用也要相应的修改:
static void Main()
{
float a = Convert.ToSingle(Console.ReadLine());
float b = Convert.ToSingle(Console.ReadLine());
OperationFactory factory = new OperationFactory(new getOperatAdd());
//这里仍然需要自己指定
Operation operater = factory.GetOperator();
float result = operater.Operat(a, b);
Console.WriteLine("结果是" + result.ToString());
}
这样就将setter注入改为了构造函数注入的形式了,但是这样的缺点就是程序运行时所实现的对象不可更改。所以上文中的桥接模式是不太适合改为构造函数注入的,因为player的行为在游戏中随时需要改变,所以应该使用setter注入。
3:依赖获取:在系统中提供一个获取点(也称为容器),客户类仍然依赖服务类的接口。当客户类需要服务类时,从获取点主动取得指定的服务类,具体的服务类类型由获取点的配置决定。这样的获取点包装了服务类的接口,并将依赖转移到特殊类型上(例如字符串)。
依赖获取相较于上文中Player类和OperaterFactory中的注入点来说,将注入点换成了获取点,即为对抽象接口和需要获得接口中的对象的类(在我们的例子中这个类往往就是主函数)之间搭建一个中介者,这个中介者就是我们的获取点(这里描述获取点为中介者是因为二者在结构上比较相似)。
这样的获取点往往在设计模式中作为抽象类与客户端桥梁存在。这种注入方式在简单工厂模式和抽象工厂中常常有使用。
例如简单工厂中,一个类包装各种判断语句来返回我们代码需要的实例,客户端传入一个字符串,返回相应的运算符,那么整个代码的对象调用就转移到了字符串operatType上:
在抽象工厂中,我们也可以建立获取点来实现依赖获取,它相当于对我们的抽象工厂做了一层包装,这层包装将创建具体工厂的依赖转移到了字符串factoryName中,我们根据上面的3A游戏的例子来添加一个获取点TripulAGamesFactory:
class TripulAGamesFactory
{
public ITripulAGame getFactory(string factoryName)
{
if(factoryName== "Microsoft")
{
return new Microsoft();
}
else if(factoryName=="EA")
{
return new EA();
}
else
{
throw new Exception("未找到您需要的类");
}
}
}
这样我们的代码中具体创建哪个工厂实例都只需要修改字符串就行了,这样我们的代码依赖就存在于字符串上,在主函数中有这样的调用:
static void Main()
{
ITripulAGame game = TripulAGamesFactory.getFactory("EA");
//只改动了公司名字
FPS fpsGame = game.CreateFPS();
Race raceGame = game.CreateRace();
fpsGame.FPSshow();
raceGame.RaceShow();
}
如果我们改用其他的方式来存取字符串,例如XML表,那么这种功能还能更强大,能够做到仅仅修改配置文件就能修改整个代码行为。
但是这样看起来也是一种倒退,从抽象工厂支持开闭原则到现在在容器中依赖转移到字符串中,影响了开闭原则。我们在没有容器类的抽象工厂中,如果要添加新的具体工厂或是新的具体产品的时候,能够非常简单的调用,但是使用了依赖获取后反而不能这么做了,但是C#中的特殊用法完美补足了这一点:
使用反射实现依赖注入
C#中的反射通过字符串生成对象,一般我们创建出一个对象,都需要使用new关键字来生成特定的类,而反射只需要程序集以及类的字符串就可以生成。
我们只需要使用Assembly类生成特定的类型就好,我们上文中那个if-else可以使用反射修改成:
class TripulAGamesFactory
{
public static ITripulAGame getFactory(string factoryName)
{
ITripulAGame factory = Assembly.Load("CSharp学习").CreateInstance("CSharp学习." + factoryName + "Factory") as ITripulAGame;
if (factory != null)
{
return factory;
}
else
{
throw new Exception("输入有误");
}
}
}
这样无论我们的代码扩展多少抽象产品的实现都能仅仅通过字符串的更改来实现依赖注入,例如我们添加一个新的具体工厂和新的具体产品,也都只需要添加完毕之后更改反射中的字符串就行了。这样就完美地迎合了开闭原则。
反射机制的引入,降低了依赖注入结构的复杂度,使得依赖注入彻底符合OCP,并为通用依赖注入框架(如Spring.NET中的IoC部分、Unity等)的设计提供了可能性。
但是我们也需要知道,完美的迎合开闭原则并不代表所有地方都能任意的扩展,例如抽象工厂模式中的抽象工厂类中功能扩展仍然需要修改代码,任何设计模式都是针对某一种情况,所以也需要我们根据自己的需求来使用设计模式。