设计模式专家

java工厂模式详解

2018-12-06  本文已影响51人  冯玉然

  这两天代码中同事用到了java工厂模式,所以度娘搜索了下然后自己理解了之后记录在此,希望也可以帮助面对网上各种文档、文章很难理解的像我一样的新手。

  说到设计模式,想必大家都不陌生,基本上每一个java的初学者都听说过,说实话,结合笔者这一年多的写业务代码的经验来说,平时可能真的用不到这些设计模式或者用到了某个设计模式但是不自知,但是,一旦在代码中用到了某些设计模式的话,首先会让自己的代码扩展性很好,而且别人看你的代码很吊,很专业,哈哈哈,设计模式共有23种,详情可以百度,今天只介绍其中三种与工厂模式有关的。
ps:本文中画的图均是示意图,并非严格UML图,只是为了方便读者理解。

简单工厂模式

简单工厂模式.jpg

  也叫工厂模式,这种模式平时在代码中偶尔自己会用到(虽然极少),也是工厂模式最简单的一种形态,也非常容易理解,简单来说,就是“当你需要某个对象的时候,通常你是new出来的,但是这样代码耦合度太高,因此我们通过一个工厂来生产出来这个对象”,这里工厂生产出来的产品就是我们所需要的那个对象,当然要注意如果是最简单的一个类,你直接使用new对象跟使用工厂生产出来对象其实区别是不大的,举个栗子:


public class A {
    public A() {
    }
}

public class Factory {
    public static A AFactory() {
        return new A();
    }
}
public class Main {
    public static void main(String[] args) {
        A a1 = new A(); // 直接new
        A a2 = Factory.AFactory(); // 通过工厂方法获取
    }
}

  从上面可以看到,在main方法里使用两种不同的方法创建A对象,通过工厂模式创建的虽然完成了对A的解耦,但是又新增了对Factory类的依赖,因此完全没什么必要这么干,但是,当类的结构稍微复杂一点的时候,就会派上用场了,下面来举个栗子:

  以前几天刚碰到的代码中的例子来讲解吧,在一个java后台项目中需要集成支付功能,而支付功能又分为了支付宝支付、微信支付、银联支付三种,并且以后还有可能会扩展百度钱包之类的东西,因此就想到了设计一个支付Pay接口,类中需要commitPayData()、pay()两个抽象方法(为啥是这两个方法,我之后会写关于集成支付的文章,欢迎关注), 然后设计三个类 AliPay、WxPay、UnionPay来实现Pay接口并分别实现两个抽象方法。如此一来,当需要哪个支付服务时就new哪个类,在这种情况下我们上面说的简单工厂模式就派上用场了,因为如果直接new的话代码中势必要在if/else中写 new AliPay()、new WxPay()、new UnionPay(),这样的话会对这三个类产生耦合,但是简单工厂模式就会巧妙的多了,我们新建一个PayFactory类,在这个类里面写一个静态方法getPayObj(),代码如下(省略了具体的支付代码):

public interface Pay {

    Object commitPayData(String str); // 返回值及参数是随手写的,应根据实际情况来

    boolean pay(Object obj); // 返回值及参数是随手写的,应根据实际情况来
}
public class AliPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

}
public class WxPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

}
public class UnionPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

}
public class PayFactory {
    public static final String ALI_PAY = "ali";
    public static final String WX_PAY = "wx";
    public static final String UNION_PAY = "union";

    /**
     * 
     * @param payMethod 支付方式
     * @return
     */
    public static Pay getPayObj(String payMethod) {
        Pay pay = null;
        if (payMethod.equals(ALI_PAY)) {
            pay = new AliPay();
        } else if (payMethod.equals(WX_PAY)) {
            pay = new WxPay();
        } else if (payMethod.equals(UNION_PAY)) {
            pay = new UnionPay();
        } else {
            // 其他支付方式不支持,可在此记录错误日志
        }
        return pay;
    }
}
public class Main {
    public static void main(String[] args) {
        Pay pay = PayFactory.getPayObj("ali");
        // 可调用pay的方法完成支付功能
        pay.commitPayData("");
        pay.pay("");
    }
}

  在Main类中想要调用某个支付服务时只需要调用工厂类中的工程方法getPayObj()即可,这样一来的话如果以后要扩展增加其他支付功能的话只需要继续实现Pay接口并且在工厂类的工厂方法中多加一个else if即可。

  上面写的demo这么使用简单工厂模式是没问题的,但是在真正的使用过程中这么写的缺点就是往往在扩展的时候还得修改工厂方法(加一个else if),有时候难免会遗忘或者会带来修改上的麻烦,因此,我们还有一种另外一种方法能在扩展功能的时候不用修改工厂类,最大限度的实现解耦,代码也有了更好的可扩展性,这种方法也算是简单工厂模式的一种延伸使用,用起来也很高大上。

  这种方法的思路是:我们需要在不更改工厂方法的前提下,又能动态的扩展服务,因此我们可以在第一次使用的时候做一个生成缓存的操作(这里是第一次使用的时候获取到缓存,当然也可以在项目启动时立即生成缓存,各有优劣),把我们所有种类的支付服务都放到一个Map<String,Pay>里,这个Map就是我们的缓存了,然后获取缓存再调用的时候获取到这个Map然后调用get(key)方法获取到支付服务对象,其实也就是我们上面写的工厂方法类改成一个缓存服务类,在该类中没有工厂方法了,取而代之的是一个获取到所有支付服务对象然后存到Map中的方法。整体思路如上所述,难点在于我们怎么获取到所有的支付服务,在这里我们使用到了自定义注解,使用注解来做一个标记功能,标记出来所有的支付功能,简单来说就是 带这个注解的类就是支付服务类,都要实现Pay接口,以后再扩展的时候可以也加上这个注解并实现Pay接口即可。

Pay类不变还是上面的,AliPay、WxPay、UnionPay也不变只是加上了注解

public interface Pay {

    Object commitPayData(String str); // 返回值及参数是随手写的,应根据实际情况来

    boolean pay(Object obj); // 返回值及参数是随手写的,应根据实际情况来

}
@PayService(channel = "ali")
public class AliPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

    @Override
    public String toString() {
        return "我是aliPay";
    }

}
@PayService(channel = "union")
public class UnionPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

    @Override
    public String toString() {
        return "我是unionPay";
    }

}
@PayService(channel = "wx")
public class WxPay implements Pay {

    @Override
    public Object commitPayData(String str) {
        // 省略业务、功能代码
        return null;
    }

    @Override
    public boolean pay(Object obj) {
        // 省略业务、功能代码
        return false;
    }

    @Override
    public String toString() {
        return "我是wxPay";
    }

}

自定义的注解


/**
 * @Target - 注解使用在类、接口上,
 * @Retention - 注解会存在与运行期
 * 
 * @author fengyr
 *
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PayService {
    // 注解的属性的设置类似于方法,之所以是String[]是因为这样的话可以不同的key关键字可以对应同一个支付服务
    public String[] channel();
}

payFactory从生产工厂变成了设置、获取缓存

public class PayFactory {

    private static PayFactory payFactory = new PayFactory(); // 保证单例

    private Map<String, Pay> payCahe;

    /**
     * 用这个来保证单例:初始化成员变量时已经给payFactory赋值了;若类的构造方法特别复杂的话则应使用双重校验方法实现单例
     * 
     * @return
     */
    public static PayFactory getInstance() {
        return payFactory;
    }

    /**
     * 把所有支付服务对象放进去缓存Map,这里的思路是通过包名及父类class对象Pay.class获取到包下所有我们需要的class对象,
     * 得到class对象后可以通过类对象从而得到支付服务对象;要注意防止多线程向map中重复存放元素 ;如果是使用@PostConstruct
     * 注解或者其他方法来让该方法只在项目初始化时执行一次的话,则可以不用考虑这里的多线程并发的问题了
     */
    private synchronized void setPayCache() {
        if (payCahe != null && !payCahe.isEmpty()) {
            return;
        }
        payCahe = new HashMap<String, Pay>();
        // 通过包名及目标类对象获取到该包下所有的支付服务类对象,注意下面这步只是获取到了该包下的Pay以及Pay的子类,但是我们只需要Pay的子类,因此还要做过滤
        Set<Class<Pay>> clazzs = PackageUtil.getPackageClasses("cn.com.payTest.payService.impl", Pay.class);
        for (Class<Pay> clazz : clazzs) {
            PayService payService = clazz.getAnnotation(PayService.class);
            if (payService == null) {
                // 过滤掉没有注解的
                continue;
            }
            String[] payNames = payService.channel();
            for (String name : payNames) {
                try {
                    // 放到缓存去
                    payCahe.put(name, clazz.getConstructor().newInstance());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }

    public Pay getPay(String channel) {
        if (payCahe == null || payCahe.isEmpty()) {
            this.setPayCache();
        }
        return payCahe.get(channel);
    }

}

最后是工具类

public class PackageUtil {

    /**
     * 根据传入的包名及类对象来扫描出来该包下所有的包含子包的满足目标泛型T的类对象并返回
     * 
     * @param pack
     * @param clazz
     * @return
     */
    public static <T> Set<Class<T>> getPackageClasses(String pack, Class<T> clazz) {
        Set<Class<T>> clazzs = new HashSet<Class<T>>();
        // 是否循环搜索子包
        boolean recursive = true;
        // 包名字
        String packageName = pack;
        // 包名对应的路径名称
        String packageDirName = packageName.replace('.', '/');
        // 保存目标package下的所有目录
        Enumeration<URL> dirs;
        try {
            // 获取目标包下所有目录
            dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            // 遍历所有目录
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                // 得到一个URL的协议
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    // System.out.println("file类型的扫描");
                    // 对字符串进行URL解码
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    // 找到该目录下所有的class<T>
                    findClassInPackageByFile(packageName, filePath, recursive, clazzs, clazz);
                } else if ("jar".equals(protocol)) {
                    // jar包不出处理,一般使用时也是扫描自己写的代码,也不会扫描到jar包
                    System.out.println("jar类型的扫描");
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return clazzs;
    }


    /**
     * 在package对应的路径下找到所有的class
     * 
     * @param packageName package名称
     * @param filePath package对应的路径
     * @param recursive 是否查找子package,final是为了在内部类中调用
     * @param clazzs 找到class以后存放的集合
     */
    public static <T> void findClassInPackageByFile(String packageName, String filePath, final boolean recursive, Set<Class<T>> clazzs, Class<T> clazz) {
        File dir = new File(filePath);
        if (!dir.exists() || !dir.isDirectory()) {
            // 目录不存在或者该目录不是一个文件夹都不行
            return;
        }
        // 在给定的目录下找到所有的文件,并且进行条件过滤
        File[] dirFiles = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                boolean acceptDir = recursive && file.isDirectory();// 接受dir目录,既接受文件夹中还有一个文件夹
                boolean acceptClass = file.getName().endsWith("class");// 接受class文件
                return acceptDir || acceptClass;
            }
        });

        for (File file : dirFiles) {
            if (file.isDirectory()) {
                // 如果是文件夹则继续调用该方法:递归思想
                findClassInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, clazzs, clazz);
            } else {
                // 去掉class文件的.class后缀
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    // 使用类加载器得到对象
                    Class<?> clazzz = Thread.currentThread().getContextClassLoader().loadClass(packageName + "." + className);
                    if (clazz.isAssignableFrom(clazzz)) {
                        // 当clazz是clazzz的父类或者两者相同的时候返回true,既根据泛型T过滤掉了不需要的类对象,我们需要的只是T或者T的子类
                        clazzs.add((Class<T>) clazzz);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在main方法中跑一下试试看,看能否获取到:

public class RunClass {
    public static void main(String[] args) {
        payAction("wx");
        payAction("union");
        payAction("ali");
    }

    public static void payAction(String channel) {
        Pay pay = PayFactory.getInstance().getPay(channel);
        System.out.println(pay);
    }
}

结果成功啦,打印如下语句:

我是wxPay
我是unionPay
我是aliPay

工厂方法模式

工厂方法模式.jpg

  上面虽然说了很多工厂模式,但是其实只是简单的工厂模式而已,但是根据笔者浅薄的经验来说是最常用的。现在要介绍一下简单工厂模式的进阶版:工厂方法模式。简单工厂模式是一个工厂根据不同的条件生产出来不同的产品(所需对象),是一个工厂类对应多个不同的产品类,而工厂方法模式则是有多个工厂类,也有多个产品类,然后每个工厂类。产品类一一对应
  这样做的好处是在新增一种产品时就不需要像简单工厂模式中一样再改动工厂类了,而是新增一个工厂类并实现/继承接口/抽象类,这样比简单工厂模式解耦的更加彻底一点了。当然,麻烦的地方在于要新增的东西比较多,可能工作量会大一些。这种模式有了上面简单工厂模式的例子后,应该很容易理解的,笔者就不做代码的展示了,若有疑问欢迎留言~

抽象工厂模式

抽象工厂模式.jpg

  最后一种工厂模式则是结构最为复杂的抽象工厂模式,笔者其实也没有在实际工作中遇到过适用这种模式的例子,因此也不贴代码拉,跟大家分享下我对这个模式的理解:上面的示意图如果不好理解的话,可以尝试更加具象化的理解,假设一件完整的产品指的是一部手机,它由A(屏幕)、B(外壳)、C(cpu)三部分组成,A1、A2、A3分别是电容屏、led、oled ;B1、B2、B3分别是塑料外壳、玻璃外壳、金属外壳 ;C1、C2、C3分别是arm、三星、inter ;那么上面的三个工厂可以分别生产不同的组合的手机;这样做的好处也是很明显的,想生产不同组合的手机时就新增一个工厂类并实现、继承工厂接口、抽象类;想新增不同种类的外壳、cpu、屏幕时只要新增类并实现相应接口就行了,并且耦合关系也比较轻量。这种模式的缺点可能是结构太复杂了吧,不过正是由于结构的复杂我们才会采用这种模式的,所以呢。。。。目前我还没用到过,等以后有什么新的想法了再跟大家分享这种模式的优劣性吧~

小结

  上面的三种模式的介绍主要是我自己的理解以及自己写的代码、画的图,所以难免有些疏漏、错误之处,希望大家能指出来,我们一起进步!笔者只是一名刚入行的小小的码畜,欢迎大家多多交流,可加948184604 QQ群交流,有疑问也可留言!

上一篇下一篇

猜你喜欢

热点阅读