工厂模式是一个大的概念,具体分为简单工厂、工厂方法模式和抽象工厂模式。之所以要用工厂模式,总体来说是为了不想将new对象的操作暴露给外界;也不想将创建对象的操作和操作对象的操作发生强耦合;同时最关键的是工厂模式实现了针对接口编程而不针对实现编程的原则。下面分别逐一介绍这三种模式。
1 简单工厂
“简单工厂,又叫做静态工厂,是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。”
这里需要明确的一点是:简单工厂并不是一种设计模式,反而比较像是一种编程习惯。这里依然拿《Head First 设计模式(中文版)》中的例子来说明。假如现在我有一个新需求:制作匹萨。如下:
Pizza orderPizza(String type) {
Pizza pizza = null;
if ("cheese".equals(type)) {
pizza = new CheesePizza();
} else if ("greek".equals(type)) {
pizza = new GreekPizza();
} else if ("pepperoni".equals(type)) {
pizza = new PepperoniPizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
首先是没有使用任何设计模式的写法。可以看到,该方法实现了制作披萨的操作。方法内的前8行是创建披萨对象的操作,后几行是拿到披萨对象后进行进一步加工(准备、烘烤、切块、包装)的操作。这是最普通的写法,同时也是最欠缺考虑的做法。我们在实际项目开发中,如果创建对象比较复杂,应尽量避免这种写法。这种写法的主要弊端在于创建对象的操作和操作对象的操作发生强耦合。如果我要新建一个披萨对象,那么得改动该方法;如果我对加工披萨的动作做修改,也需要改动该方法。
简单工厂就是将创建对象的操作单独提出来,放进一个类里的方法中,这就是简单工厂。如下:
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if ("cheese".equals(type)) {
pizza = new CheesePizza();
} else if ("greek".equals(type)) {
pizza = new GreekPizza();
} else if ("pepperoni".equals(type)) {
pizza = new PepperoniPizza();
}
return pizza;
}
}
SimplePizzaFactory类就是工厂类,createPizza方法就是创建披萨对象的方法。通过传进参数的不同,来生成不同的披萨对象。这样我们就可以通过调用工厂类的创建实例的方法来实现创建披萨的操作。如下:
Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
该方式成功将创建对象的动作和操作对象的动作解耦。如果我要新增一种披萨,只需要在工厂类里进行修改就行;如果我要修改操作披萨的逻辑,只需要在orderPizza方法中修改就行了。
但是,这种方式是否就完全解决问题了呢?代码的可维护性是否已经足够好?答案肯定是否定的(否则也不会有工厂方法模式和抽象工厂模式的出现了)。简单工厂的弊端在于不符合开放-关闭原则。开放-关闭原则指的是对扩展开放,对修改关闭。当有新的需求出现时,不应该去对现有代码进行修改,而是通过扩展的方式实现新的需求。实现开放-关闭原则的核心精髓在于继承,这点需要读者自行进行体会。如果现在有一种新的披萨要生产,那么势必要对工厂类的createPizza方法进行修改,这就是一个不好的实现,由此引出了工厂方法模式,该模式解决了这个问题。
2 工厂方法模式
“定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。”
工厂方法模式中会定义一个抽象类,通过继承、实现抽象方法的方式来解决实例化的问题。现在的需求发生了变动,现在有纽约和芝加哥两家披萨店需要制作披萨。具体代码如下:
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(String type);
}
createPizza方法是抽象基类中的抽象方法,子类实现该方法用来实现具体实例化的动作。如下则是两家披萨店的具体制做披萨的方法:
public class NYPizzaStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new NYStyleCheesePizza();
} else if ("veggie".equals(type)) {
return new NYStyleVeggiePizza();
} else if ("pepperoni".equals(type)) {
return new NYStylePepperoniPizza();
} else {
return null;
}
}
}
public class ChicagoPizzaStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
if ("cheese".equals(type)) {
return new ChicagoStyleCheesePizza();
} else if ("veggie".equals(type)) {
return new ChicagoStyleVeggiePizza();
} else if ("pepperoni".equals(type)) {
return new ChicagoStylePepperoniPizza();
} else {
return null;
}
}
}
由上可以看到,纽约披萨店制作纽约风味的披萨,芝加哥披萨店制作芝加哥风味的披萨。代码通过继承抽象类,实现抽象类中的抽象方法来实现的工厂方法模式。那为什么说这种方式就符合开放-关闭原则呢?假如说现在我有一家新的奥克兰披萨店,那么我不需要去动原有的代码逻辑,只需要去写一个新的奥克兰披萨店的子类,去实现抽象类的抽象方法createPizza就可以了。这也就是我前面所说的开放-关闭原则的核心在于继承的概念。
在看过我之前讲解的模板方法模式的文章的话,可能在此已经发现,工厂方法模式和模板方法模式的实现很相似,实际也确实是如此(没看过的可以在此点链接《设计模式之模板方法模式》)。可以理解为工厂方法是模板方法的一种特殊版本。在学过设计模式后,我由衷的感觉到:设计模式之间的区分,并不是简单的从代码的实现上来区分的,而是通过需求的不同、实现目的不同来区分的。模板方法模式解决的是重复代码的问题,而工厂方法模式解决的是对象实例化的问题,这也需要读者自行进行体会。
但是无论如何,我们都需要写一个判断语句去判断,到底执行的是纽约的披萨店还是芝加哥披萨店。这无可厚非,不能被避免。我们需要做的是:把注意力集中在设计中最有可能发生改变的地方,然后应用开放-关闭原则(如果一定要去掉判断语句,可以考虑使用反射来动态加载类)。
3 抽象工厂模式
“提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。”
抽象工厂模式是这三种模式中抽象层级最高的一种,可以理解为抽象工厂的再一级抽象。当有对象族的需求出现时,可以考虑使用抽象工厂模式。
那么什么是对象族呢?我一开始也不理解,包括查了很多例子后,慢慢有所感悟,在此说出自己的一些理解(如说的不对,请多指教)。我认为对象族指的是有多层维度的场景。比方说现在有一个汽车的例子。汽车可以分为国产车、美国车和日本车。而每种车之下又可分为轿车和跑车。即国产车分为国产轿车和国产跑车;美国车分为美国轿车和美国跑车;日本车分为日本轿车和日本跑车。如下图所示:
这里就有了两个维度,分别是汽车的产地和类型。这种场景下用抽象工厂模式更适合一些,但是这并不意味着另外两种方式不能使用,这需要多加注意。
回到我们制作披萨的需求中来,现在我们有了新的需求:生产披萨上的各种原料。例如:制作酱汁、奶酪以及辣香肠。这意味着纽约披萨会生产出纽约口味的酱汁、奶酪以及辣香肠;芝加哥披萨会生产出芝加哥口味的酱汁、奶酪以及辣香肠。这里也会有两个维度:披萨的原料和产地。此时我们需要一个抽象接口如下:
public interface PizzaIngredientFactory {
public Sauce createSauce();
public Cheese createCheese();
public Pepperoni createPepperoni();
}
该接口定义了生产这三种原料的接口方法。说一句题外话,在我们日常的项目开发中,使用接口还是抽象类往往没有严格的限制。这两者的区别是:接口不能实现方法,抽象类可以实现方法。但是从jdk8开始,接口也支持了实现方法的default语句。所以这两者直接的区别貌似只剩下了java不能多重继承类,而可以实现多个接口这一点了吧。其实接口和抽象类可以结合起来一起使用,即骨架类。这种模式的具体解释引用《Effective Java中文版》中所说:“通过对你导出的每个重要接口都提供一个抽象的骨架实现类(skeletal implementation)类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。”感兴趣的读者可以自行了解。
回到做披萨的例子中,接下来就是具体的原料工厂的实现了,如下图所示:
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
@Override
public Sauce createSauce() {
return new MarinaraSauce();
}
@Override
public Cheese createCheese() {
return new ReggianoCheese();
}
@Override
public Pepperoni createPepperoni() {
return new SlicedPepperoni();
}
}
public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory {
@Override
public Sauce createSauce() {
return new PlumTomatoSauce();
}
@Override
public Cheese createCheese() {
return new MozzarellaCheese();
}
@Override
public Pepperoni createPepperoni() {
return new SlicedPepperoni();
}
}
由上可知,纽约和芝加哥原料工厂分别实现了自己风味的三种原料。其实如果原料只有一种的话,上述的抽象工厂模式就会退化成工厂方法模式;抽象方法如果不是一个,工厂方法模式也可以抽象升级为抽象工厂模式来实现。
需要说明的一点是:工厂模式的三种实现中,并不是说哪种实现方式一定最好,一定就比另外两种好。即使是简单工厂,对于对象实例化简单,且不经常改动的场景也很适用。到底要用哪种方式来实现工厂模式,这没有具体的答案。在我之前写过的单例模式的文章中曾说过:设计模式可以按着实际需要进行灵活变动,这往往需要使用者进行多方面的决策考量。例如代码复杂度,灵活性,封装性、复用性等等。不同人针对某种需求所使用的设计模式也不尽相同。
在现在的框架中也有很多地方用到了工厂模式。例如:spring的ioc容器中创建了很多各种各样的BeanFactory;mybatis中的DataExchangeFactory、DataSourceFactory也都有所体现。
最后想说的一句话是设计模式固然很好,但也绝对不要违背使用设计模式的初衷。不能说为了使用设计模式而去使用设计模式。用设计模式不是为了炫技,而是真正考虑在这种需求情况下,我用了这种设计模式比不用会更好,我才用它。盲目生搬硬套设计模式往往会使程序变得更加复杂,这也需要使用者进行多加考虑。