23种设计模式-工厂方法模式

  1. 女娲造人的故事

女娲造人的故事大家都非常熟悉,我们通过这一场景来学习工厂模式(例子很好,就是内容有点胡扯)。首先,女娲采集黄土捏成人型,然后放到八卦炉中烧制,虽然工艺过程没错,但是意外总会发生:
第一次烤泥人,时间过短,然后白人诞生了;第二次烤,时间过长,黑人诞生了;第三次,时间不长不短,黄种人诞生了。
在面向对象的思维中,万物皆对象,是对象我们就可以通过软件设计来实现。首先对过程进行分析,该过程涉及三个对象:女娲,八卦炉,三种不同肤色的人。女娲可以使用
场景类来表示,八卦炉类似于一个工厂,负责制造生产产品,三种不同肤色的人,他们都是同一个接口下的不同实现类,类图8-1如下:

8-1

类图比较简单,AbstractHumanFactory是一个抽象类,定义了一个八卦炉具有的整体功能,HumanFactory为实现类,完成具体的任务——创建人类;Human接口是人类的总称,其三个实现类分别为三类人种;代码如下:

//人类接口
public interface Human {
    void getColor();
}
public class WhiteHuman implements Human {
    @Override
    public void getColor() {
        System.out.println("白人");
    }
}
public class BlackHuman implements Human {
    @Override
    public void getColor() {
        System.out.println("黑人");
    }
}
public class YellowHuman implements Human {
    @Override
    public void getColor() {
        System.out.println("黄种人");
    }
}
//工厂类
public abstract class AbstractHumanFactory {
    public abstract <T extends Human> T createHuman(Class<T> c);
}
public class HumanFactory extends AbstractHumanFactory {
    @Override
    public <T extends Human> T createHuman(Class<T> c) {
        Human human = null;
        try {
            human = (T)Class.forName(c.getName()).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return (T) human;
    }
}
//场景
public class NvWa {
    public static void main(String[] args) {
        AbstractHumanFactory humanFactory = new HumanFactory();
        Human blackHuman = humanFactory.createHuman(BlackHuman.class);
        blackHuman.getColor();
        Human whiteHuman = humanFactory.createHuman(WhiteHuman.class);
        whiteHuman.getColor();
        Human yellowHuman = humanFactory.createHuman(YellowHuman.class);
        yellowHuman.getColor();
    }
}

以上就是工厂模式。

  1. 工厂模式定义

工厂模式使用的频率非常高,在我们日常的开发中总能见到他的身影。其定义为:Define an interface for creating an object,but let subclasses decide with class to instantiate. Factory Method lets a class defer instantiation to subclasses(定义一个工厂用于创建对象的接口,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到其子类)

工厂方法模式的通用类图8-2所示:


8-2

在工厂方法模式中,抽象产品Product负责定义产品的共性,实现对事物最抽象的定义:Creator 为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成的。工厂方法模式的变种较多,我们来看一个比较实用的通用源码。

//抽象产品
public interface Product {
    void method();
}
//具体产品
public class ConcreteProduct implements Product{
    @Override
    public void method() {
        System.out.println("do something");
    }
}
//抽象工厂
public abstract class Creator {
    public abstract <T extends Product> T FactoryMethod(Class<T> clz);
}
//工厂实现
public class ConcreteCreator extends Creator{
    @Override
    public <T extends Product> T FactoryMethod(Class<T> clz) {
        Product obj = null;
        try {
            obj = (Product) Class.forName(clz.getName()).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return (T) obj;
    }
}
//场景类
public class Client {
    public static void main(String[] args) {
        Creator creator = new ConcreteCreator();
        Product product = creator.FactoryMethod(Concreteproduct.class);
        product.method();
    }
}
  1. 工厂方法模式的应用

3.1 工厂方法模式的优点
首先,良好的封装性,代码结构清晰。一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。
其次,工厂方法模块的扩展性非常优秀。在增加产品类的情况下,只要适当的修改具体的工厂类或扩展一个工厂实现类,就可以完成“拥抱变化”。例如在我们的例子中,需要增加一个棕色人种,只需要增加一个brownHuman类,工厂类不用任何修改就可以完成系统扩展。
再次,屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,他只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。因为产品类的实例工作是由工厂负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的。在数据库开发中,大家应该能够深刻的体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从Mysql切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是sql语句是标准语句),其他的都不需要修改,这是工厂模式灵活性的一个直接案例。
最后,工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则(最少知识原则),我不需要的就不去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!(六大原则指导思想,设计模式是六大原则思想的具体实现)

3.2 工厂方法模式的使用场景
首先,工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重考虑是否要增加一个工厂类进行管理,增加代码的复杂度。
其次,需要灵活的、可扩展的框架时,可以考虑采用工厂模式。万物皆对象,那万物也就皆产品类,例如需要设计一个邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP,我们就可以把这三中连接方式作为产品类,定义一个接口如IConnectMail,然后定义对邮件的操作方法,用不同的方法是想三个具体的产品类(也就是连接方式)再定义一个工厂类,按照不同的传入条件,选择不同的连接方式。如此设计,可以做到完美的扩展,如某些邮件服务器提供webservice接口,我们只需要增加一个产品类就可以了。
再次,工厂方法模式可以用在异构项目中,例如通过webservice与一个非java的项目交付,虽然webservice号称可以做到异构系统的同构化,但是在实践的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。(webservice的接口做得不多,这个意思是通过定义一个标识接口,让外围系统产生的对象都实现这个标识接口去统一管理吗

  1. 工厂方法模式的扩展

工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面将介绍4种扩展。(就我自己理解的一种就是可以把工厂类做成单例的,一般工厂会涉及很多配置信息,这个类就比较重,做成单例的就可以减少创建销毁的开销
4.1 缩小为简单工厂模式
我们这样考虑一个问题:一个模块仅需要一个工厂类,没必要把他产生出来,使用静态的方法就好了,根据这一要求,我们把上例中的AbstractHumanFactory修改一下,类图8-3:

8-3

在类图中可以看到,去掉了抽象工厂类,同时把createHuman方法设置为静态类型,简化了类的创建过程,变更的源码仅仅是HumanFactory和NvWa类,HumanFactory代码如下:

public class HumanFactory {
    public static <T extends Human> T createHuman(Class<T> clz){
        Human human = null;
        try {
            human = (T)Class.forName(c.getName()).newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return (T) human;
    }
}

这个比较简单,就是去掉继承抽象类,将createHuman方法改为类方法,对于不需要工厂类有其他扩展的时候用这个方法可以简化代码,该模式就是工厂方法模式的弱化,因为简单,所以叫简单工厂模式(Simple Factory Pattern),也叫做静态工厂模式。在实际项目中,采用该方法的案例还是比较多的,其缺点是工厂类的扩展比较困难,不符合开闭原则,但他任然是一个非常实用的设计模式。
4.2 升级为多个工厂类
当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰。例如,一个产品类有5个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new一个对象,并对对象设置一定得初始值)方法都不相同,如果写在一个工厂方法中,势必会导致该方法巨大无比,那该怎么办?(这种创建多种实例并且初始值不同,常规思路就是加判断,在分到对应多的具体实现方法,但是这样每增加一种产品类就得修改工厂类,这就不符合开闭原则,对修改关闭
考虑到需要结果清晰,我们就为每个产品 定义一个创造者,然后由调用者自己去选择与哪个工厂方法联系。(这种处理方式就是对修改关闭,对扩展开放,新增一种产品类,就去增加对应的工厂类)。我们还是以女娲造人为例,每个人种都有一个固定的八卦炉,分别造出黑人,白人,黄种人,修改后的类图8-4:

8-4

这种模式的代码如下(这里并没有体现出需要这样去分工厂类的必要性,只是作为举例):

//工厂类
public abstract class AbstractHumanFactory {
    public abstract Human createHuman();
}
public class BlackHumanFactory extends AbstractHumanFactory {
    @Override
   public abstract Human createHuman(){
        return new BlackHumanFactory();
    }
}
...

注意:抽象方法中已经不需要传递相关的参数了,因为每一个具体的工厂都已经非常明确自己的职责了:创建自己负责的对象。但是这种就看有没有必要去用工厂管理了,要是没必要可能会增加过多的工厂导致代码更复杂了
每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定得影响。为什么这么说呢?如果要扩展一个产品类就需要创建一个对应的工厂类,这就增加了扩展的难度。因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。
当然,在复杂的应用中一般采用多工厂的方法,然后在增加一个协调类,避免调用者与各个子工厂交流,协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。(在这里突然想到了反射的一些用处,以前只知道有反射这个东西,但是为什么要有这种方式,仅仅是为了增加创建实例或是调用实例的方法的方式吗?按我以前的思维方式就是做if..else或是switch...case等,每增加一个选项就需要去增加这块选择的代码,比如我新增一个产品类,我如果去做if判断类型是否符合条件然后创建实例,但是用反射的方式,我根本不需要一直去维护判断的逻辑,完全可以根据传的相关参数去用反射去创建对象,这就增加了代码的扩展性,那么spring框架可能也是这样的逻辑,根据bean中的类的全量名去用反射的方式创建实例
4.3 代替单例模式
我们是不是可以采用工厂方法模式实现单例模式的功能呢?单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中产生一个对象,类图8-5:

8-5

Singleton定义了一个private的无参构造函数,目的是不允许通过new的方式创建一个对象,代码如下:

//单例类
public class Singleton {
    private Singleton(){
    }
    public void doSomething(){
    }
}
public class SingletonFactory {
    private static Singleton singleton;
    static {
        try {
            Class clz = Class.forName(Singleton.class.getName());
            Constructor constructor  = clz.getDeclaredConstructor();
            constructor.setAccessible(true);
            singleton = (Singleton) constructor.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    public Singleton getSingleton(){
        return singleton;
    }
}

通过获得类构造器,然后设置访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一。当然其他类也可以通过反射的方式建立一个单例对象,确实如此,但是一个项目或团队是由章程和规范的,何况已经提供了一个获得单例对象的方法,为什么还要重新创建一个新对象呢。(按这个方式去思考,我们常说spirng中默认bean是单例的,其实并不是真的单例类,你也可以自己手动去创建,但是我们一般不会这么做,那么在bean的配置中可以通过设置去创建单例或是多例,实际上也是这样的逻辑去做的,通过beanFactory去管理的bean,这块之后可以去通过看spring框架的源码去学习一下。以上都是本人通过学习设计模式想到的一些,不知道是否正确,但是提供了一种思考思路。可以想见,像spring这些优秀的框架,其中的设计模式的实际运用随处可见,学完设计模式可以带着这些设计理念去阅读spring源码,一定会非常有帮助
4.4 延迟初始化
何为延迟初始化(Lazy initialization)?一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用。延迟初始化是工厂方法模式的一个扩展应用(听名字,应该是一个典型的空间换时间的思路,对象消费完毕并不是立刻释放,而是持有一段时间,下次在需要就直接使用,省去了创建销毁的开销,但是持有时会占用内存),其通用类图8-6:

8-6

ProductFactory负责产品类对象的创建工作,并且通过prMap变量产生一个缓存,对需要再次被重用的对象保留(看完类图发现,这个好像就是最基础的缓存原型吧,用map将最近使用的对象存起来,取的时候先去map中找,找不到在去创建,然后将新创建的实例维护到map中,如果map中已经存满(一般缓存肯定会有一个上限,要不然内存很容易就不够用了,动不动就内存溢出,那谁顶的了,应用肯定会出问题),就将比较久远的对象移除,在将新的对象保存到map中(这就是一个缓存策略的问题了)),代码如下:

public class ProductFactory {
    public static final Map<String,Product> prMap = new HashMap<>();
    public static synchronized Product creataProduct(String type) throws Exception{
        Product product = null;
        if(prMap.containsKey(type)){
            product = prMap.get(type);
        }else{
           if("Product".equals(type)){
               product = new Concreteproduct();
           }else{
               //这里应该是创建其他实现类对应的实例,就不写了
           }
            prMap.put(type,product);
        }
        return product;
    }
}

延迟加载框架是可以扩展的,例如限制某一个产品类的最大的实例化数量,可以通过判断Map中已有对象的数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大的连接数量,该数量就是内存中最大实例化的数量。(优秀的设计模式,到处都有体现
延迟加载还可以用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。

  1. 最佳实践

工厂方法模式在项目中使用的非常频繁,以至于很多代码中都包含工厂模式。该模式几乎尽人皆知,孰能生巧,熟练掌握该模式,多思考工厂方法如何应用,而且工厂方法模式可以和其他模式混合使用(例如模板方法模式,单例模式,原型模式等),变化出无穷的优秀设计,这也正是软件设计和开发的乐趣所在。(确实,就学习这一模式都可以让我想到很多平时常用的框架中的一些设计方式和基础的思考方向,多学习总是可以触类旁通的

内容来自《设计模式之禅》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容