简单工厂
我们在了解简单工厂之前,先回顾一下java中的接口
我们先来看看几个问题
什么是接口?
在java中,接口是一种特殊的抽象类,跟一般的抽象类相比,接口里面所有的方法都是抽象方法,接口里面所有的属性都是常量。也就是说,接口里面只有方法的定义,没有任何方法实现。(在这里只是考虑jdk8之前的版本接口,至于jdk8及以后版本接口default和static修饰的方法可以带有方法体,感兴趣的同学可以去找相关博客)
在工作中 所谓的"接口",并不仅仅指的是java中定义的interface,小到一个功能模块,大到一个系统,都可以称之为一个接口。
接口用来干什么?
通常来说,接口用于定义实现类的外观,也就是实现类的行为定义,规范一个实现当前接口的类最起码需要有什么功能。实现类除了实现接口规定的功能之外,还可以根据需要来实现其他的功能,所以说实现类的功能包含但不仅限于接口所约束的功能。
接口的思想是什么?
根据接口的作用和用途,浓缩下来,接口的思想就是“封装隔离”
通常提到的封装是对数据的封装,但是这里指的封装是指“对被隔离体的行为的封装”,或是"对被隔离体的职责的封装"
隔离则指的是外部调用与内部实现相隔离,外部调用只能通过接口来进行调用,外部调用者是不知道内部的具体实现方式的,也就是说外部调用和内部实现是被接口隔离开的。
比如我做了一个金钱支付的接口,对外只暴露了接口名和pay这个支付方法,外部在调用只能通过我接口创建的对象.pay()这个方法来获取功能,但是外部并不知道我金钱支付方法内部具体的逻辑(在这里我就是将支付这个内部逻辑封装成了对外的pay方法,内部实现对外部调用者隔离了)
使用接口的好处是什么?
由于外部调用和内部实现被接口隔离开了,那么只要接口不变,内部实现的变化就不会影响到外部应用,从而使系统更加灵活,具有更好的扩展性和可维护性。
使用场景
我们看下使用场景
public interface Fruit {
public void test();
}
public class Orange implements Fruit {
@Override
public void test() {
System.out.println("我是橘子");
}
}
我们现在有一个Fruit水果接口,有一个实现类Orange
我们想一下,在刚学习java的时候,我们是怎么使用接口来创建我们的实现类的
Fruit fruit = new Orange();
我刚学习java的时候,我是这么通过接口创建实现类的,
可是客户端在创建Fruit对象的时候,即知道了接口Fruit又知道了实现类Orange,这显然不符合接口"封装隔离"的思想。实现类应该和接口是隔开的,也就是说,客户端在调用的时候可以知道接口Fruit但是不可以知道实现类Orange。但是如果我们在创建Fruit对象的时候把 new Orange()去掉以后发现我们无法得到Fruit接口对象了,现在问题就是客户端只知道接口,却不知道实现类,所以拿不到对象,无法使用接口,该怎么办呢?
解决方案就是使用简单工厂
解决方案
分析上面的问题,虽然不能让模块外面知道模块内部的具体实现,但是模块内部是可以的,所以我们只要在模块内部创建一个专门负责来创建对象实例这个类就好了
打个比方,就是原来我要吃苹果,我自己去苹果园去摘(相当于new Apple()),自己摘苹果的时候,是需要了解具体怎么摘苹果的(比如构造器是有参构造器,你还得了解构造器参数是什么意思),是有一定的学习成本的。现在用了简单工厂模式,相当于我们对接了一个中介,这个中介专门是摘苹果的,我们想要吃苹果了,直接跟中介说我要苹果,中介直接把苹果给你,你不用了解具体怎么摘苹果,如果哪一天摘苹果方式换了,你依旧对接的是中介,中介去学习新的摘苹果方式就好了,而客户只需要持有对中介的引用就好了。
我们把这个中介称为工厂,暂叫它Factory
这样一来,客户端就可以通过Factory来获取所需要的接口对象,然后调用接口的方法实现对应的功能,而且客户端也不用再关心具体的实现了。
上图是简单工厂模式的基本组成
为了节省篇幅我们复用上面的Fruit接口和实现类Orange类
public class Apple implements Fruit {
@Override
public void test() {
System.out.println("我是苹果");
}
}
再定义一个实现类苹果类
然后我们写一个工厂类
public interface FruitConstant {
int APPLE = 1;
int ORANGE = 2;
}
public class FruitFactory {
public static Fruit getFruit(int fruitType) {
if (FruitConstant.APPLE == fruitType) {
return new Apple();
} else if (FruitConstant.ORANGE == fruitType) {
return new Orange();
}
return null;
}
}
我们工厂写好了,我们在使用的时候,只需要调用我们写的这个FruitFactory
的getFruit方法就好了
public class TestFruitFactory {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getFruit(FruitConstant.APPLE);
fruit.test();
fruit = FruitFactory.getFruit(FruitConstant.ORANGE);
fruit.test();
}
}
运行结果
我是苹果
我是橘子
这样看,是不是就是将我们客户端和我们Fruit内部实现类的实现隔离开了
可是这样的代码还是有问题的,因为会违反了开闭原则
开闭原则
软件应该实现对扩展开放,对修改关闭。
什么意思呢,我们拿代码具体
我们创建一个香蕉类
public class Banana implements Fruit {
@Override
public void test() {
System.out.println("我是香蕉");
}
}
上述代码,我们如果在工厂中要增加了一个Banana香蕉类,代码应该如何改变
在上面创建Banana类没有问题,属于扩展的范畴
但是下面我们要修改封装好的FruitConstant和我们FruitFactory中的getFruit方法
首先常量FruitConstant里面要增加一个对香蕉的常量
public interface FruitConstant {
int APPLE = 1;
int ORANGE = 2;
int BANANA = 3;
}
public class FruitFactory {
public static Fruit getFruit(int fruitType) {
if (FruitConstant.APPLE == fruitType) {
return new Apple();
} else if (FruitConstant.ORANGE == fruitType) {
return new Orange();
} else if (FruitConstant.BANANA == fruitType) {
return new Banana();
}
return null;
}
}
而且还需要将getFruit方法内部加一条对香蕉的else if分支
我们对原来已经封装好的getFruit方法进行了修改,这就违背了开闭原则,因为我们对已经封装好的类修改了。
我们如何去改变这种情况
可配置的简单工厂
我们可以通过配置文件的方式来决定工厂创建哪个Fruit的实例
我在E盘下创建了一个fruit-config.properties的文件
文件内容为键值对类型如下
fruitImpl=factory.Apple
因为我Apple类的全限定名为factory.Apple,在配置文件中的属性一定要为类的全限定名才可以
然后我们修改我们的工厂类中的getFruit方法
public static Fruit getFruit() {
Properties properties = new Properties();
InputStream in = null;
Fruit fruit = null;
try {
// 如果配置文件在项目中也可以使用Class内的getResourceAsStream方法取
// 此处因为我的配置文件不在项目下,所以我使用当前方法取的流
in = new FileInputStream(new File("E:\\fruit-config.properties"));
properties.load(in);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
try {
// 因为我在配置文件中的键用的fruitImpl所以在这里面拿也是fruitImpl
String className = properties.getProperty("fruitImpl");
Class clazz = Class.forName(className);
fruit = (Fruit) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return fruit;
}
我们使用工厂创建Fruit
public class TestFruitFactory {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getFruit();
fruit.test();
}
}
结果
我是苹果
这样的话,我们再使用这个工厂就很简单了,我们要创建Apple类,就在配置文件中配置 好苹果的全限定名,如果我们想创建橘子
fruitImpl=factory.Orange
只需要修改我们外部的配置文件即可
其他什么都不用做修改
再次跑我们的TestFruitFactory的main方法
我是橘子
到这里我们可配置的简单工厂就完成了,当然这只是最简单的一种静态配置的方式,还可以在取配置的时候从数据库里面拿你类的全限定名等等很多种方式,我在这里只介绍一种
总结
简单工厂的本质是“选择实现”,我们注意到我们实现已经是做好了的,工厂只是替我们去完成创建哪个类返回给我们。客户端选择,工厂去创建对应的实现类,从而实现了客户端和实现之间的解耦。这样一来,具体实现发生了变化,就不用变动客户端,这个变化会被简单工厂吸收和屏蔽掉。
如果想要完全屏蔽掉内部实现,让外部只能通过接口来操作封装体,我们可以使用简单工厂模式来实现。
如果想要把对外创建对象的职责集中管理起来,可以使用简单工厂模式,可以把对外创建对象的职责统一到一个工厂里面,从而实现集中管理和控制。