设计模式(0)简单工厂模式
0 设计模式基础
0.0 设计模式的定义
先来看一下设计模式常见的书面定义:
- 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
- 设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的、特定问题的解决方案。
设计模式更多的是一种实际应用中经验的基类和总结,并得到了多数人的认可和验证,经过更规范的整理和分类及命名,成为了一种众所周知的知识体系。
0.1 设计模式的分类
一般情况下说到的设计模式都是指Gof著作中所讲述的23中经典设计模式。
-
创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
-
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
-
行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)、访问者模式。
1 面向接口编程
上学的时候经常跟室友一起玩儿War3,我一直比较钟情于暗夜精灵族,暗夜精灵有4个灰常厉害的英雄,每个英雄都会有4个技能。刚开始学习面向对象思想的时候,我已经知道“英雄”可以定义成一个类,假设每个英雄出场时候都要秀一下自己华丽的四个技能,我们在构造函数中传递英雄名称,构造不同的英雄对象。
/// <summary> /// 英雄类 /// </summary> public class Hero { private readonly string _name; // 英雄名称 /// <summary> /// 构造函数 /// </summary> /// <param name="name">英雄名称</param> public Hero(string name) { _name = name; } /// <summary> /// 秀出自己的技能 /// </summary> public void ShowSkills() { switch (_name) { case "DH": Console.WriteLine("我是恶魔猎手,我会法力燃烧、献祭、闪避和变身。"); break; case "WD": Console.WriteLine("我是守望者,我会暗影突袭、刀阵、闪烁和复仇天神。"); break; case "KOG": Console.WriteLine("我是丛林守护者,我会缠绕根须、荆棘光环、自然之力和宁静。"); break; case "POM": Console.WriteLine("我是月亮女祭司,我会召唤猫头鹰,灼热之箭,强击光环和流星雨。"); break; default: break; } } }
英雄轮流秀出的自己的技能(排名不分先后)
class Program { static void Main(string[] args) { Hero dh = new Hero("DH"); dh.ShowSkills(); Hero wd = new Hero("WD"); wd.ShowSkills(); Hero kog = new Hero("KOG"); kog.ShowSkills(); Hero pom = new Hero("POM"); pom.ShowSkills(); } }
输出结果
上面的代码存在的问题主要有2个:
- 如果某天我不再玩儿暗夜精灵了,改玩儿别的种族,Hero类中的ShowSkills方法就要完全修改一遍。
- 秀出自己的技能只是一个最基本的行为,英雄还具有攻击、移动、释放技能等行为,每个英雄攻击力、移动速度、释放技能耗蓝却又都各不相同。
这里就涉及到了面向接口编程的重要原则。
通常接口用来定义实现类的外观,提取实现类共有的行为定义。接口类似于一种契约,根据外部应用的需要,约定了实现类应该实现的功能,而具体内部如何实现,应由具体的实现类控制,同时具体的实现类除了要实现接口规定的行为外,还可以根据需要实现自己独有的行为,也就是说实现类的功能应包含但不仅限于接口定义的功能。
由于外部调用和内部实现被接口隔离开了,外部调用只通过接口调用,也就是说只要接口不变,内部具体实现的变化就不会对外部调用产生任何影响。这样使用接口的好处就很明显了,当我们增加其他英雄的时候只需要增加一个实现英雄接口的具体实现类既可,对原有已实现的不分不会造成任何影响。提现了接口“封装隔离”的思想。
定义英雄接口:
/// <summary> /// 英雄接口定义 /// </summary> public interface IHero { /// <summary> /// 秀技能 /// </summary> void ShowSkills(); }
不同英雄的具体实现:
/// <summary> /// 恶魔猎手 /// </summary> public class DH : IHero { /// <summary> /// 秀出自己的技能 /// </summary> public void ShowSkills() { Console.WriteLine("我是恶魔猎手,我会法力燃烧、献祭、闪避和变身。"); } }
/// <summary> /// 守望者 /// </summary> public class WD : IHero { /// <summary> /// 秀出自己的技能 /// </summary> public void ShowSkills() { Console.WriteLine("我是守望者,我会暗影突袭、刀阵、闪烁和复仇天神。"); } }
/// <summary> /// 丛林守护者 /// </summary> public class KOG : IHero { /// <summary> /// 秀出自己的技能 /// </summary> public void ShowSkills() { Console.WriteLine("我是丛林守护者,我会缠绕根须、荆棘光环、自然之力和宁静。"); } }
/// <summary> /// 月亮女祭司 /// </summary> public class POM : IHero { /// <summary> /// 秀出自己的技能 /// </summary> public void ShowSkills() { Console.WriteLine("我是月亮女祭司,我会召唤猫头鹰,灼热之箭,强击光环和流星雨。"); } }
修改外部调用部分,同样每个英雄都正确的秀出了自己的技能。
IHero dh = new DH(); dh.ShowSkills(); IHero wd = new WD(); wd.ShowSkills(); IHero kog = new KOG(); kog.ShowSkills(); IHero pom = new POM(); pom.ShowSkills(); Console.ReadLine();
到这里我们就会发现了新的问题,上面说过接口的作用的就是封装隔离,外部调用只知道接口的存在,不应该依赖于具体的实现类。而接口又是没有办法通过new关键字进行实现话的,如何解决这个矛盾?
2 简单工厂模式
简单工厂模式并不包含在上述23种经典设计模式之中,也有人说简单工厂并不能算得上一个设计模式。不管怎么说,简单工厂却是实实在在的简单易用,还记得当年还在学校的时候,由于没有经过实际项目的历练,在编码经验不足的情况下,去试着了解设计模式的时候,大部分设计模式是无法深入理解的,也就只有看到简单工厂模式的时候会惊呼:“原来还可以这样”抑或“原来我一直用的就是简单工厂”。
简单来说,简单工厂就是通过定义一个工厂类,这个工厂类提供了一个创建具体实例的功能,外部调用只需要告诉工厂需要什么类型的实例,工厂负责创建这个实例,外部调用无需关心其具体实现,从而达到真正的接口隔离的目的。
简单工厂类中创建具体实例的方法一般定义为静态方法,从而可以避免在外部调用的时候再new简单工厂的对象,因此简单工厂模式一般也被成为静态工厂模式。
我们定义一个简单工厂类。
/// <summary> /// 简单工厂类 /// </summary> public class Factory { /// <summary> /// 创建英雄的静态方法 /// </summary> /// <param name="heroName">英雄名称</param> /// <returns></returns> public static IHero CreateHero(string heroName) { switch (heroName) { case "DH": return new DH(); case "WD": return new WD(); case "KOG": return new KOG(); case "POM": return new POM(); default: return null; } } }
然后外部调用的时候通过简单工厂方法创建各个英雄的实例。
IHero dh = Factory.CreateHero("DH"); dh.ShowSkills(); IHero wd = Factory.CreateHero("WD"); wd.ShowSkills(); IHero kog = Factory.CreateHero("KOG"); kog.ShowSkills(); IHero pom = Factory.CreateHero("POM"); pom.ShowSkills();
初识简单工厂模式的时候,最大的疑问就是,这个简单工厂类,无非就是把原来在外部调用时创建具体英雄类实例的代码挪了过去,在简单工厂类里面还是一样需要通过new指定具体的类来进行接口的实例化啊,而且还白白多了一个简单工厂类,意义何在?
其实简单工厂类最大的意义还是起到接口隔离的作用,看到隔离这个词,就肯定有隔离的双方(封装体),我们多增加一个简单工厂类,表面上看是多了一个类,并没有减少任何代码,也没有对代码进行大的更改,只是一个英雄实例化代码的位置移动,简单工厂模式的精髓恰恰就是这个具体类实例化代码位置的移动,我们知道,使用接口的目的就是不让外部调用知道封装体内部的实现,在使用简单工厂类之前,我们创建英雄实例的代码很明显的是位于外部调用部分的,这样其实就是没有隔离,由于简单工厂类位于封装体内部的,工厂类是可以知道具体的实现细节的。使用简单工厂类后,相当于这个封装提对外只公开了一个IHero接口及一个工厂类创建英雄的方法给外部调用,这样隔离就很明确了,只是一段代码位置的移动,从设计上来讲,已经发生了本质的变化。
3 简单工厂的几点建议
- 工厂方法静化
简单工厂类中创建实例的方法,应为静态方法。
- 实例创建配置文件化
实例创建应尽量通过配置文件及反射机制,动态创建,达到能根据某个值,自动判断并创建对应类的实例的目的,这样就可以将庞大的swith语句块消除,同时,实例化部分的修改,只需要修改配置文件即可。
- 简单工厂模块化
一个简单工厂可以定义多个创建实例的静态方法,建议按照不同的功能模块,创建不同的工厂类。因为简单工厂类是一个模块封装提的一部分。
4 简单工厂模式的优点
- 封装
能够非常简单快捷的实现模块的组件化,组件通过对外公开接口,实现面向接口编程
- 解耦
实现了外部调用和具体实现的解耦,增强了系统的健壮性和易维护性。