一、引言
马上找工作了,自己做了一份简历,发现很多知识都忘得一干二净,所以最近准备做一个关于设计模式方面的专题,想通过自己写一些文章加深对设计模式的理解,也欢迎大家来阅读,同时给予批评指正。
闲话少说,我们进入正题吧,设计模式主要的功能就是实现代码的可扩展性,在现在面向对象开发过程中应用已经非常广泛了,同时它也是一个高级程序员应该具有的基本素养,在面试环节会经常被问到,今天我们就来聊一聊设计模式中最著名的工厂模式吧!
二、工厂模式分类
1)简单工厂模式(Simple Factory)
2)工厂方法模式(Factory Method)
3)抽象工厂模式(Abstract Factory)
这三个工厂模式由上到下抽象层次逐渐增加,代码的可扩展性也逐渐增加,但是我们不必完全抛弃前两种设计模式,具体看我们的业务需求。
三、场景布置
比如我们现在做一款坦克大战的游戏,游戏中自然就有一系列Tank的对象,游戏开始后,玩游戏的人控制着自己的tank打敌方的tank。
四、简单工厂
public class ChinaTank {
public void shoot(){
System.out.println("ChinaTank is shooting....");
}
}
这是具体的一个ChinaTank类的实现,我们打算根据这个具体的Tank来作为主程序的Tank,游戏中有很多Tank
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
ChinaTank t1 = new ChinaTank();
ChinaTank t2 = new ChinaTank();
ChinaTank t3 = new ChinaTank();
t1.shoot();
t2.shoot();
t3.shoot();
}
}
这是在利用简单工厂前我们的游戏主代码实现,程序启动后就能看到一个中国的Tank在shooting,那么我们遇到了什么问题呢?就功能来讲Client完全
没有任何问题,但是游戏者逐渐感到厌烦了,因为游戏中的Tank实在太单一,我们现在想在游戏中添加多个Tank的种类,现在应该怎么做呢?首先我
兴致盎然的在系统中写了一个AmericaTank的类
public class AmericaTank {
public void shoot(){
System.out.println("AmericaTank is shooting....");
}
}
写完后我就想把这个AmericaTank类对象嵌入到Client中,以替换当前的ChinaTank类,可是我突然发现只有通过修改原来的源代码为
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
AmericaTank t1 = new AmericaTank();
AmericaTank t2 = new AmericaTank();
AmericaTank t3 = new AmericaTank();
t1.shoot();
t2.shoot();
t3.shoot();
}
}
才可以实现替换游戏中的ChinaTank类对象,这显然不是我们想要的结果,因为我们不停的修改了3次Tank对象,有没有一种更好的方法来实现上述替换效果呢?
庆幸的是答案是肯定的,简单工厂可以做到,下面我们用简单工厂的方法来设计整个游戏。游戏中上面设计的两个ChinaTank类和AmericaTank类保留
,然后我们定义一个Tank接口(抽象类),同时让
ChinaTank和AmericaTank实现上述Tank接口,代码如下
public interface Tank {
public void shoot();
}
public class ChinaTank implements Tank{
public void shoot(){
System.out.println("ChinaTank is shooting....");
}
}
public class AmericaTank implements Tank{
public void shoot(){
System.out.println("AmericaTank is shooting....");
}
}
我们都知道多态的好处,利用Tank接口,我们的客户代码可以只和Tank接口打交道,而不和具体的ChinaTank类或者AmericaTank类打交道,
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
Tank t1 = new ChinaTank();
Tank t2 = new ChinaTank();
Tank t3 = new ChinaTank();
t1.shoot();
t2.shoot();
t3.shoot();
}
}
可是现在的Client代码还是有ChinaTank类啊,还是没有办法遵循开闭原则(实现新功能的时候只添加新的代码,而不修改原来的代码),其实要做到
不修改代码很简单
,我们只需要将代码中需要修改的部分去除掉,用不需要改变的代码来替换即可,上述Client中的ChinaTank正是我们的修改目标,
请注意:在我们平时的设计过程中,像
ChinaTank这样的统统都称为危险份子,就想MH370的机长,你不知道什么时候他会背叛人民,这样的危险
份子我们绝不允许它出现在代码中,就像这样的人我们绝对不允许他活在自由世界中一样。好了言归正传,我们需要用不可变的代码来代替Client中的
ChinaTank,怎么做到呢?下面我就添加了一个简单工厂类,它专门负责生产各种Tank
public class SimpleFactory {
public Tank getTank(int kind){
Tank t = null;
switch(kind){
case 1:
t = new ChinaTank();
break;
case 2:
t = new AmericaTank();
break;
}
return t;
}
}
有了这个简单工厂后,我们就可以利用这个工厂类来代替原来的游戏Client中的ChinaTank了
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
SimpleFactory sf = new SimpleFactory();
Tank t1 = sf.getTank(1);
Tank t2 = sf.getTank(1);
Tank t3 = sf.getTank(1);
t1.shoot();
t2.shoot();
t3.shoot();
}
}
修改后的Client代码没有了具体的ChinaTank,但是游戏运行起来后,仍然呈现的是一辆ChinaTank在shooting,需要我们注意的是sf.getTank(1)中的1
可以由游戏者自己选择,游戏可以记录下这一选择并从配置文件中读取,或者通过其它途径传入getTank作为参数,也就是说我们完全避免了Client的代码改动
总结:通过简单工厂,我们解决了不用修改Client中的主代码部分即可切换游戏中tank的种类,当然这一切都基于游戏实现已经设计好了若干tank类,并
从Tank接口继承
,简单工厂解决了对于Client而言的开闭原则的实现,即如果需要新增Tank,Client完全不用动,只需要改变SimpleFactory中的getTank即可,它实现了将Client中的可变部分抽取出来的效果,当我们新增其它Tank类的时候,可以很方便的找到对应的SimpleFactory,然后改变它,而不需要从庞大的工程中翻来找去哪些部位需要修改(因为除了Client要用Tank外,可能整个工程的其它部分也需要Tank)。
五、工厂方法
有了简单工厂我们就满足了吗?好吧到了给你一点打击的时候了,玩家重新玩了3天你新设计的游戏,要知道人是永远不知道满足的动物,它又开始厌倦了,为了迎合他的需求,程序员的老板发话了,说:“弯勇辉,限你两天时间在现有的游戏里添加新的一种Tank出来,要是完不成你知道你的后果的”。
他以为我会害怕他,但他没有想到的是我早已经在研究生的阶段练就了金刚不坏之身,对于这种针对我的言论我总是抱着呵呵的态度。现在我仍然是一名猿类,还没有进化完成,所以我只有听他的按时把工作完成。我打开原来的代码,仔细审视了一下,我发现用简单工厂的代码编写的Client端实在太好了,心里暗自高兴了一把,然后就开始动手了,我的打算是在游戏中再添加一个EnglishTank的类出来,然后把它嵌入整个游戏,首先我在工程里面添加了一个
EnglishTank
public class EnglishTank implements Tank{
public void shoot(){
System.out.println("EnglishTank is shooting....");
}
}
好了,第一步已经完成了,现在要做的就是把它嵌入到游戏中去,因为前面使用了简单工厂,Client里的代码根本不用变,我只需要改变SimpleFactory就可以了
public class SimpleFactory {
public Tank getTank(int kind){
Tank t = null;
switch(kind){
case 1:
t = new ChinaTank();
break;
case 2:
t = new AmericaTank();
break;
case 3:
t = new EnglishTank();
break;
}
return t;
}
}
好了,我的工作完成了,很简单吧,下次玩家玩游戏的时候他就可以有三种选择来决定游戏的tank到底是什么类型的。
高兴的过早了,我刚要把新完成的代码交给老板的时候,一个念头闪过我的脑海,因为我预测老板3天后还会再来找我,让我给游戏添加第4种tank出来
。我可不能总是在添加新的tank类后,再次回到SimpleFactory类中修改它的源码(刚刚你注意到了吗,我在将EnglishTank添加到游戏中的时候修改了
SimpleFactory的源码),对于SimpleFactory而言这违背了开闭原则。那么问题出在哪了呢?因为在简单工厂中我们只是简单的把生产对象的部分抽取了出来(程序中可能变化的部分)并放置到了一个我们起名为SimpleFactory的类中来,我们做的只是将可变的部分移了一个位置而已,对于整个程序而言,当有新的Tank出现的时候,我们还是避免不了对代码的修改。这貌似是一个不能避免的问题,但是我在这里还是要强调一下简单工厂的作用,它大大缩小了我们修改代码的范围,并一定程度上减少了代码修改的次数。
那有没有一种策略,在对Client端保持开闭原则的同时,也对工厂保持开闭原则,答案又是肯定的,下面我给出“工厂方法”策略
既然因为一个简单工厂生产各种Tank导致了该简单工厂的不稳定性,那我就让不同的工厂生产不同的Tank,这样问题不就解决了吗?是的,这就是所谓的“工厂方法”模式
关于Tank接口、ChinaTank、AmericaTank、EnglishTank的代码都不变,变化的是工厂的实现
首先我要定义一个抽象的工厂类,这种工厂不生产实际的Tank,它只是提供了生产Tank类型对象的方法
public interface TankFactory {
public Tank getTank();
}
然后对应每种具体的tank,我都定义一个具体的工厂类
public class ChinaTankFactory implements TankFactory{
@Override
public Tank getTank() {
return new ChinaTank();
}
}
public class AmericaTankFactory implements TankFactory{
@Override
public Tank getTank() {
return new AmericaTank();
}
}
public class EnglishTankFactory implements TankFactory{
@Override
public Tank getTank() {
return new EnglishTank();
}
}
三种工厂分别用于生产三种不同的Tank,并且都实现了TankFactory接口,这样以来我如果想要在游戏使用ChinaTank,则Client程序可以这样写:
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
TankFactory tf = new ChinaTankFactory();
Tank t1 = tf.getTank();
Tank t2 = tf.getTank();
Tank t3 = tf.getTank();
t1.shoot();
t2.shoot();
t3.shoot();
}
}
看到了吗?这样的工厂方法其实已经很完美了,因为它做到了两点,一、我们的Client中代码不用变(ChinaTankFactory对象可由配置文件指定,然后通过反射机制生成 tf 对象),Client中产生的tank都符合面向抽象编程的理念。二、当系统要加入新的tank种类的时候,我们不需要变更系统的任何一行代码,我们需要做的只是在系统中添加新的Tank类和生成该Tank的TankFactory。
六、抽象工厂
工厂方法是完美的,但只是对于生产一系列相同种类的产品来说,例如上一节中所描述的ChinaTank和AmericaTank,它们共同实现了Tank接口,这样才保证了在替换Tank种类的情况下(我们的Client面向Tank接口编程)Client端代码保持不变。然而现实生活中我们往往需要生成一系列的对象,这些不同类型的对象共同的组成了我们的系统,比如Tank游戏中的坦克和子弹,我们就暂且叫它们为一个产品族,对与我们的系统而言他们是不可或缺的一部分。而tank本身又分为ChinaTank和AmericaTank两种类别,我们暂且假设游戏中的子弹也分为两种不同的类别:ChinaBullet和AmericaBullet。当我们选择中国元素为游戏的主元素时,游戏中应该同时有ChinaTank和ChinaBullet,当选择了美国元素的时候游戏中应该有AmericaTank和AmericaBullet,如果利用上述的工厂方法设计当前系统,系统会为每种不同的坦克和子弹设计各自的工厂,这样容易产生工厂爆炸(系统完成后会有很多很多的工厂类),虽然满足了开闭原则,但这并不理想,事实上我们应该针对每种不同的主题创建一个工厂,比如ChinaFactory,它专门负责生产ChinaTank和ChinaBullet,AmericaFactory它专门负责生产AmericaTank和AmericaBullet,同时为了保证Client代码的不变性,我们的各个主题工厂和各个产品都应该面向接口:下面看代码吧(最精彩的部分来啦)
首先我们定义一个抽象的工厂
public interface AbstractFactory {
Tank getTank();
Bullet getBullet();
}
然后各个不同的主题工厂生产不同主题的产品族
public class ChinaFactory implements AbstractFactory{
@Override
public Tank getTank() {
return new ChinaTank();
}
@Override
public Bullet getBullet() {
return new ChinaBullet();
}
}
public class AmericaFactory {
@Override
public Tank getTank() {
return new AmericaTank();
}
@Override
public Bullet getBullet() {
return new AmericaBullet();
}
}
对应的Client端代码如下,此时Client要生产若干坦克和子弹出来
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
AbstractFactory af = new ChinaFactory();
Tank t1 = af.getTank();
Tank t2 = af.getTank();
Tank t3 = af.getTank();
Bullet b1 = af.getBullet();
Bullet b2 = af.getBullet();
Bullet b3 = af.getBullet();
}
}
跟据上述方式,当有新的产品族要加入系统时,只需要设计对应的产品类,这生产这些产品类(产品家族)的具体工厂(记得实现AbstractFactory哦)即可。
好了,总结一下就是抽象工厂面向的问题是产品族的生产问题,它是对工厂方法的改进(工厂方法设计系统的话产生太多的工厂类)
七、总结工厂模式
简单工厂解决了对客户端代码的不变动(开闭原则),同时避免了批量生产产品的多次new具体产品问题,当需要扩展时工厂内方法需要变动
工厂方法解决了工厂的变动问题,对与Client和工厂来说都不需要变动
抽象工厂面向产品族的生产问题,是对工厂方法的扩展,避免了工厂方法中工厂类爆炸的问题。