常说的“依赖注入”是如何发展过来的呢?
工厂方法模式
工厂模式是常见的设计模式。
静态工厂
介绍
静态工厂的工厂类集中了所有实例的创建逻辑,你可以告诉工厂“我要个 A 类型的产品”,工厂就乖乖给了你个 A,是一种 valueOf 类型的指令。
代码
class SimpleFactoryImpl {
public static Product getInstance(ProductType productType) {
switch (productType) {
case TYPE_1:
return () -> System.out.println("Product Type1");
case TYPE_2:
return () -> System.out.println("Product Type2");
case TYPE_3:
return () -> System.out.println("Product Type3");
default:
return null;
}
}
}
interface Product {
void use();
}
enum ProductType {
TYPE_1, TYPE_2, TYPE_3
}
缺点
这样的“工厂”不利于扩展,如果我现在有个 TYPE_4 的 Product,还得修改工厂本身的代码,破坏了 OCP(开-闭原则,Open-Closed Principle)。
工厂方法
介绍
对于简单工厂存在的缺陷,工厂方法作了改进,将创建对象的逻辑抽象到一个接口内,使不同的实现类都实现工厂方法。
工厂方法常常用在“不想让调用者决定使用哪个实现”或者“调用者自己不知道要调用哪个实现”的情况。
当需要扩展工厂方法时,不需要修改原有的逻辑分支,只需要在新实现类中实现获取产品的方法即可。
代码
interface Product {
void use();
}
class Product1 implements Product {
public void use() {
System.out.println("Product Type1");
}
}
class Product2 implements Product {
public void use() {
System.out.println("Product Type2");
}
}
class Product3 implements Product {
public void use() {
System.out.println("Product Type3");
}
}
interface FactoryMethod {
Product getInstance();
}
class FactoryMethodImpl1 implements FactoryMethod {
public Product getInstance() {
return new Product1();
}
}
class FactoryMethodImpl2 implements FactoryMethod {
public Product getInstance() {
return new Product2();
}
}
class FactoryMethodImpl3 implements FactoryMethod {
public Product getInstance() {
return new Product3();
}
}
这样一来,当我利用多态去使用 FactoryMethod 类型的实例时,我不用关心实例具体的类型,我知道这些实例都可以 get 一个 Product 类型的 Instance,到时候 use 就完事儿了。
典型举例
集合框架 java.util.Collection 接口继承了 java.lang.Iterable 接口,后者的 iterator 方法即 Collection 接口的工厂方法,产品就是迭代器 java.util.Iterator。
我们在调用 Collection 类型的各种集合时不需要关心 iterator 方法具体返回的是什么类型的迭代器对象(比如 ArrayList 的 iterator 方法返回的是其内部类 java.util.ArrayList.Itr),只需要知道 Collection 都有一个 iterator 方法可以返回具有遍历 Collection 功能的对象就行了。
缺点
- 工厂方法的调用者不能决定调用的是哪个类型。
- 如果工厂需要生产多类产品,则工厂方法不能很好地满足。
针对第一点,与其说是缺点,不如说是初衷,工厂方法的初衷就是使调用者对工厂返回的类型无感知。
针对第二点,抽象工厂模式进行了改进,下一篇我们会介绍。
抽象工厂模式
抽象工厂用来处理一般工厂模式带来的扩展性问题。
首先解释“为什么在多种产品的情况下不新建一个 interface FactoryMethod2”
代码
为了解释这种情景,我们添加了一类产品 TrademarkSticker。
/**
* 产品
*/
interface Product {
void use();
}
/**
* 可被贴标
*/
interface CanBeStuck {
void setTrademark(String trademark);
}
/**
* 鼠标既是产品又可以被贴商标
*/
interface Mouse extends Product, CanBeStuck {
}
/**
* 贴标器
*/
interface TrademarkSticker {
void stick(CanBeStuck canBeStuck);
}
/**
* 既生产鼠标又生产贴标器的工厂
*/
interface Factory {
Mouse produceMouse();
TrademarkSticker produceTs();
}
class DellFactory implements Factory {
public Mouse produceMouse() {
DellMouse dellMouse = new DellMouse();
// 鼠标需要贴商标,生产鼠标需要贴标器的支持
produceTs().stick(dellMouse);
return dellMouse;
}
public TrademarkSticker produceTs() {
return new DellTrademarkSticker();
}
}
class HPFactory implements Factory {
public Mouse produceMouse() {
HPMouse hpMouse = new HPMouse();
// 鼠标需要贴商标,生产鼠标需要贴标器的支持
produceTs().stick(hpMouse);
return hpMouse;
}
public TrademarkSticker produceTs() {
return new HPTrademarkSticker();
}
}
class DellMouse implements Mouse {
public void use() {
System.out.println("using DELL mouse");
}
public void setTrademark(String trademark) {
System.out.println("trademark is set to " + trademark);
}
}
class HPMouse implements Mouse {
public void use() {
System.out.println("using HP mouse");
}
public void setTrademark(String trademark) {
System.out.println("trademark is set to " + trademark);
}
}
class DellTrademarkSticker implements TrademarkSticker {
public void stick(CanBeStuck canBeStuck) {
canBeStuck.setTrademark("DELL");
}
}
class HPTrademarkSticker implements TrademarkSticker {
public void stick(CanBeStuck canBeStuck) {
canBeStuck.setTrademark("HP");
}
}
原本可以通过两个 FactoryMethod 抽象出两种产品的工厂,但是由于产品间有相互依赖的关系,某个产品的生产需要依赖其它产品才能完成,这种情况如果使用 FactoryMethod 的话,会在产品中调工厂,代码耦合更严重。所以,我们才需要在工厂方法的基础上进行改造,形成上面抽象工厂的逻辑。
结构
缺点
但是抽象工厂也有缺陷,当我们需要增加新的产品线时(比如增加 Keyboard),在新增实现类后,还要修改所有 Factory 实现类中生产 Keyboard 的逻辑。
从工厂模式到依赖注入
Spring 的 Web 容器等 IOC 容器在 Servlet 容器启动期间对所“管辖”的组件间的依赖进行解析,帮助程序为不同的 Java Bean 注入所需要的依赖,程序员不需要关心他们的来源,只需要将配置文件编写好,这大大方便了 JavaEE 开发者们的开发工作。
Martin Fowler 大神有一篇 Inversion of Control Containers and the Dependency Injection pattern,即“控制反转型容器和依赖注入模式”,其中介绍了多种依赖注入的方式,是 IOC 的经典必读。
抽象工厂的问题
如上文所述,在抽象工厂模式中,如果要多增加一个产品线,会需要修改很多代码。
我们尝试模拟抽象工厂中的客户端调用工厂方法得到产品的场景:
public static void main(String[] args) {
Mouse mouse = new DellFactory().produceMouse();
mouse.use();
mouse = new HPFactory().produceMouse();
mouse.use();
}
结果如下:
现在添加一个产品线 Keyboard,我们需要增加以下代码:
interface Keyboard extends Product, CanBeStuck {
}
class DellKeyboard implements Keyboard {
public void use() {
System.out.println("using DELL keyboard");
}
public void setTrademark(String trademark) {
System.out.println("trademark is set to " + trademark);
}
}
class HPKeyboard implements Keyboard {
public void use() {
System.out.println("using HP keyboard");
}
public void setTrademark(String trademark) {
System.out.println("trademark is set to " + trademark);
}
}
并修改以下代码:
interface Factory {
...
Keyboard produceKeyboard();
}
class DellFactory implements Factory {
...
public Keyboard produceKeyboard() {
DellKeyboard dellKeyboard = new DellKeyboard();
produceTs().stick(dellKeyboard);
return dellKeyboard;
}
}
class HPFactory implements Factory {
...
public Keyboard produceKeyboard() {
HPKeyboard hpKeyboard = new HPKeyboard();
produceTs().stick(hpKeyboard);
return hpKeyboard;
}
}
可以预想,当代码量很大,业务比较复杂的情况下,这将会是件很恶心的事情。其实对于 Java 语言来说,利用反射去生成对象再方便好不过了。
优化抽象工厂
Java 提供反射机制,可以让程序获取代码运行时的信息,我们尝试结合 反射和配置文件 来优化抽象工厂。
为了方便展示重点,我们把原抽象工厂代码中体现产品联系的 Trademark 去掉,保留单纯的产品侧抽象:
interface Product {
void use();
}
interface Mouse extends Product {
}
interface Keyboard extends Product {
}
class DellMouse implements Mouse {
public void use() {
System.out.println("using DELL mouse");
}
}
class HPMouse implements Mouse {
public void use() {
System.out.println("using HP mouse");
}
}
class DellKeyboard implements Keyboard {
public void use() {
System.out.println("using DELL keyboard");
}
}
class HPKeyboard implements Keyboard {
public void use() {
System.out.println("using HP keyboard");
}
}
修改 Factory 为面向 JavaBean 的 BeanFactory:
interface BeanFactory {
Object getBean(String beanName) throws Exception;
}
class MyBeanFactory implements BeanFactory {
private Map<String, Object> beanMap;
private Document document;
public MyBeanFactory(String configFilePath) {
try {
SAXReader saxReader = new SAXReader();
this.beanMap = new HashMap<>();
this.document = saxReader.read(new File(configFilePath));
injectRecursively(this.document.getRootElement().elements("bean"));
} catch (Exception e) {
System.err.println("initialize bean factory error: " + e.getCause().getMessage());
}
}
/**
* initialize <bean>s
*/
private void injectRecursively(List<? extends Element> elements) throws Exception {
for (Element element : elements) {
// <bean>
String beanId = element.attribute("id").getValue();
String beanClassName = element.attribute("class").getValue();
if (beanMap.get(beanId) != null) {
continue;
}
Object bean = Class.forName(beanClassName).newInstance();
// <property>s
List properties = element.elements("property");
if (properties.size() != 0) {
for (Object prop : properties) {
Element subElement = (Element) prop;
String subBeanAttr = subElement.attribute("attr").getValue();
String subBeanId = subElement.attribute("ref").getValue();
Object initialized = this.beanMap.get(subBeanId);
if (initialized != null) {
// property 已被工厂持有
ReflectionUtils.setFieldValue(bean, subBeanAttr, initialized);
} else {
Element refed = findClassConfig(subBeanId);
ArrayList<Element> beanList = new ArrayList<>();
beanList.add(refed);
injectRecursively(beanList);
ReflectionUtils.setFieldValue(bean, subBeanAttr, beanMap.get(subBeanId));
}
}
}
beanMap.put(beanId, bean);
}
}
private Element findClassConfig(String beanId) {
Element element = null;
try {
element = (Element) this.document.selectSingleNode("/beans/bean[@id='" + beanId + "']");
} catch (Exception ignored) {
}
return element;
}
public Object getBean(String id) throws Exception {
return this.beanMap.get(id);
}
}
代码中使用了 DOM4j 解析 xml 文件,实现了一个简陋的利用反射技术使用 setter 进行注入的 BeanFactory。
使用工厂产品的类:
class User {
private Mouse mouse;
private Keyboard keyboard;
public void setMouse(Mouse mouse) {
this.mouse = mouse;
}
public void setKeyboard(Keyboard keyboard) {
this.keyboard = keyboard;
}
public void use() {
mouse.use();
keyboard.use();
}
}
配置文件 config.xml:
<beans>
<bean id="user" class="di.User">
<property attr="mouse" ref="dellMouse"/>
<property attr="keyboard" ref="hPKeyboard"/>
</bean>
<bean id="dellMouse" class="di.DellMouse"/>
<bean id="hPKeyboard" class="di.HPKeyboard"/>
</beans>
测试代码:
BeanFactory beanFactory = new MyBeanFactory(configPath);
User user = (User) beanFactory.getBean("user");
user.use();
我们在第二行打断点,查看 BeanFactory 持有的对象情况:
可以看到,工厂持有的 User 被注入了两个属性,且来自工厂本身(@943、@941)。
运行结果:
结构
控制被反转?
我们把创建对象比作炒菜,原先我们需要自己掌勺,亲力亲为,现在厨师(BeanFactory)帮我们把菜做好了,你尽管享用就行。
我们把抽象工厂模式和 DI 做下对比,发现抽象工厂在增加产品线时,需要修改代码,而依赖注入模式中的 BeanFactory 持有的 beans 是来源于配置文件的(还有扫描等非侵入的方式),程序端在使用时是感觉不到 Factories 的存在的(如 User),这样如果要再增加产品线,只需要在增加产品线定义(接口)和相应产品(实现类)后,增加配置文件即可(自动装配则更简单)。也就是说,原先需要我在代码中主动关注的“从哪个工厂拿对象”这件事,现在我不需要关心了,IOC 工厂都帮我都做好了,我要做的只是告诉工厂:“我要个 Mouse,要个 Keyboard,注入的话你用我暴露的 setter 吧”,原先掌控在我手里的“依赖获取方式”(即控制),现在委托给 BeanFactory 啦(即反转)。
Spring IoC 容器:
总结
本文实现了一个比较简单的 BeanFactory,还没有对资源等做抽象,依赖解析的过程也过于简单,实际上 Spring 等高级工厂的逻辑还有功能都更复杂强大的多。