设计模式之Factory

    |     2015年5月8日   |   设计模式   |     0 条评论   |    1483

工厂模式如此常用,是因为它相当于创建实例对象的 new。我们经常要根据类生成实例,如 A a = new A()。以后再写 new 时不妨多想一步:能不能改用工厂?虽然可能多写一点代码,但能换来更大的可扩展性和尽量少的修改量。

以类 Sample 为例,创建实例本来是 Sample sample = new Sample();。实际中创建时往往还要做初始化:赋值、查库等。参数简单时可以写成 new Sample(参数);初始化若是很长一段代码,塞进构造函数会很难看,也违背封装和分派:长代码应切割、封装,降低段与段之间的耦合。

一、为什么要把 new 换成工厂

创建实例和使用实例要分开:大量初始化工作应从 Sample 的构造函数里拆出去,这时就不能再用简单的 new Sample(参数)。若 Sample 有子类 MySample,按面向接口编程应把 Sample 抽象成接口,下面有 MySample 和 HisSample:

Sample mysample = new MySample();
Sample hissample = new HisSample();

项目深入后 Sample 还可能“生出很多儿子”,传统写法要对每个儿子 new 一次,甚至改以前的代码。一开始就用工厂,这些麻烦就没有了。

二、工厂方法

专门建一个生产 Sample 实例的工厂:

public class Factory {
    public static Sample creator(int which) {
        // getClass 产生 Sample,一般可使用动态类装载装入类
        if (which == 1)
            return new SampleA();
        else if (which == 2)
            return new SampleB();
        return null;
    }
}

程序里要实例化 Sample 时写成:

Sample sampleA = Factory.creator(1);

整段代码不再涉及 Sample 的具体子类,达到封装效果,也减少改错机会。通俗说法:具体事情做得越多越容易犯错;工厂把“怎么 new”藏起来,调用方只认接口。

使用工厂方法要注意几个角色:先定义产品接口(如 Sample),下面有实现类(如 SampleA),再有一个 factory 类用来生成产品。下图最右边是生产出的对象 Sample:

factory

三、反射实现工厂方法

用 Class.forName(className).newInstance() 按类名动态装入产品,换实现类时不必改工厂内部的 if/else。原文里接口方法、赋值语句漏了分号,下面按可编译写法补全:

package com.scxh.java.factory;
interface Fruit { // 水果接口
    public void eat(); // 吃水果
}
class Apple implements Fruit { // 定义苹果
    public void eat() {
        System.out.println("**吃苹果。");
    }
}
class Orange implements Fruit {
    public void eat() {
        System.out.println("**吃橘子。");
    }
}
class Factory {
    public static Fruit getInstance(String className) {
        Fruit fruit = null;
        try {
            fruit = (Fruit) Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fruit;
    }
}
public class FactoryDemo {
    public static void main(String args[]) { // 通过工厂取得接口实例,传入完整的包.类名称
        Fruit f = Factory.getInstance("com.scxh.java.factorydemo.Apple");
        if (f != null) { // 判断是否取得接口实例
            f.eat();
        }
    }
}

四、抽象工厂

工厂模式里有工厂方法(Factory Method)和抽象工厂(Abstract Factory)。差别在于创建对象的复杂程度。工厂方法只创建一种产品 Sample;若还有新产品接口 Sample2,且 Sample 有 SampleA / SampleB,Sample2 有 Sample2A / Sample2B,就把 Factory 做成抽象类,共同部分放抽象类,不同部分交给子类:

public abstract class Factory {
    public abstract Sample creator();
    public abstract Sample2 creator(String name);
}

public class SimpleFactory extends Factory {
    public Sample creator() {
        // ...
        return new SampleA();
    }
    public Sample2 creator(String name) {
        // ...
        return new Sample2A();
    }
}

public class BombFactory extends Factory {
    public Sample creator() {
        // ...
        return new SampleB();
    }
    public Sample2 creator(String name) {
        // ...
        return new Sample2B();
    }
}

两个工厂各自生产一套 Sample 和 Sample2。为什么不拆成两个工厂方法分别生产?关键是 SimpleFactory 里生产两种产品的方法有联系,要捆在一个类里——比如制造工艺都比较简单,所以叫 SimpleFactory。实际应用中工厂方法更多,而且常和动态类装入器一起用。

模式 产品族 典型用法
工厂方法 一种产品接口,多个实现 creator(which) 或 Class.forName 装入
抽象工厂 多组相关产品一起创建 同一工厂产出配套的 Sample + Sample2
简单工厂 静态方法集中 new 改产品时主要改工厂类

五、Jive 的 ForumFactory

前面 Singleton 里讨论过这个例子,这里看工厂部分。现在的 Jive 用数据库存放帖子;若改成文件系统,只要换 className:

private static String className = "com.jivesoftware.forum.database.DbForumFactory";

用自己开发的创建 forum 的方法代替 DbForumFactory 即可。

public abstract class ForumFactory {
    private static Object initLock = new Object();
    private static String className = "com.jivesoftware.forum.database.DbForumFactory";
    private static ForumFactory factory = null;

    public static ForumFactory getInstance(Authorization authorization) {
        if (authorization == null) {
            return null;
        }
        // 以下使用了 Singleton 单态模式
        if (factory == null) {
            synchronized (initLock) {
                if (factory == null) {
                    try {
                        // 动态装载类
                        Class c = Class.forName(className);
                        factory = (ForumFactory) c.newInstance();
                    } catch (Exception e) {
                        return null;
                    }
                }
            }
        }
        // 返回 proxy,用来限制授权对 forum 的访问
        return new ForumFactoryProxy(authorization, factory,
                factory.getPermissions(authorization));
    }

    // 真正创建 forum 的方法由继承 ForumFactory 的子类去完成
    public abstract Forum createForum(String name, String description)
            throws UnauthorizedException, ForumAlreadyExistsException;
}

这段代码一共用了三种模式:工厂、Singleton,以及 Proxy。Proxy 用来授权访问 forum:注册用户和游客权限不同,而且权限贯穿整个系统,用 proxy 类似网关。

六、Java 宠物店的 CatalogDAOFactory

public class CatalogDAOFactory {
    /**
     * 本方法指定一个特别的子类来实现 DAO 模式。
     * 具体子类定义是在 J2EE 的部署描述器中。
     */
    public static CatalogDAO getDAO() throws CatalogDAOSysException {
        CatalogDAO catDao = null;
        try {
            InitialContext ic = new InitialContext();
            // 动态装入 CATALOG_DAO_CLASS
            // 可以定义自己的 CATALOG_DAO_CLASS,从而在无需变更太多代码
            // 的前提下,完成系统的巨大变更。
            String className = (String) ic.lookup(JNDINames.CATALOG_DAO_CLASS);
            catDao = (CatalogDAO) Class.forName(className).newInstance();
        } catch (NamingException ne) {
            throw new CatalogDAOSysException(
                "CatalogDAOFactory.getDAO: NamingException while getting DAO type : \n"
                + ne.getMessage());
        } catch (Exception se) {
            throw new CatalogDAOSysException(
                "CatalogDAOFactory.getDAO: Exception while getting DAO type : \n"
                + se.getMessage());
        }
        return catDao;
    }
}

CatalogDAOFactory 是典型工厂方法。catDao 通过动态类装入器拿到具体实现子类,Java 宠物店里用来操作 catalog 数据库。用户可按数据库类型定制子类,把类名交给 CATALOG_DAO_CLASS 即可。

由此可见,工厂方法为系统提供了灵活的动态扩展:只要更换具体工厂方法,系统其他地方无需改动,功能就可以改头换面。

一句话总结:工厂把 new 和初始化从调用方拆走:一种产品用工厂方法(常配反射),一组相关产品用抽象工厂;换实现只改工厂或配置,不改业务代码。

转载请注明来源:设计模式之Factory
本文链接地址:https://ai.zhousir.top/?p=641
回复 取消