写在前面
这篇博文介绍设计模式的形式将与其他篇博文不太一样,这里我们将从一个问题入手,逐步了解到简单工厂、工厂方法与抽象工厂模式。
PS:这篇博文涉及的内容较多,所以篇幅有点长,请耐心阅读。
为什么要使用工厂模式?
在 OO 设计中,有一个重要的设计原则:针对接口编程而不针对实现编程。每当我们使用 new 去实例化一个对象时,用到的就是实现编程,而不是接口。这样以来代码绑定着具体类,会导致代码更脆弱,缺乏弹性。
在技术上,使用 new 没有错,毕竟这是 Java 的基础部分。真正错的是“改变”,以及它会影响 new 的使用。针对接口编程,可以隔离掉以后系统可能发生的一堆改变。原因是,如果代码针对接口编程,那么通过多态,它可以与任何新类实现该接口。
与以往的使用 new 的方式不同,工厂模式使用“工厂”实例化对象。这样一来代码的扩展性变得更强,又可以降低代码之间的耦合。
一、从问题中引出工厂模式
1.1 问题描述
比萨店:假设你有一家比萨店,为了吸引更多的顾客,所以你们的比萨店提供了很多种类型的比萨,比如奶酪比萨,希腊比萨等。针对这个问题设计比萨从生产到售卖相关的类。
设计图
1.2 代码实现
比萨类 Pizza
package com.jas.simplefactory;
public abstract class Pizza {
public String name;
public String sauce;
public void prepare(){
System.out.println("准备 ..." + name);
System.out.println("添加配料 ..." + sauce);
}
public void bake(){
System.out.println("烘烤 25 分钟。");
}
public void cut(){
System.out.println("把比萨饼切成对角片。");
}
public void box(){
System.out.println("将比萨放到比萨商店的盒子中。");
}
}
奶酪比萨类 CheesePizza
package com.jas.simplefactory;
public class CheesePizza extends Pizza {
public CheesePizza(){
name = "奶酪比萨";
sauce = "大蒜番茄酱";
}
}
希腊比萨类 GreekPizza
package com.jas.simplefactory;
public class GreekPizza extends Pizza{
public GreekPizza(){
name = "希腊比萨";
sauce = "大蒜番茄酱";
}
}
比萨商店类 PizzaStore
package com.jas.simplefactory;
public class PizzaStore {
public Pizza orderPizza(String type){
Pizza pizza = null;
//比萨商店负责生产比萨
if("cheese".equals(type)){
pizza = new CheesePizza();
}else if("greek".equals(type)){
pizza = new GreekPizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
测试类 PizzaTestDrive
package com.jas.simplefactory;
public class PizzaTestDrive {
public static void main(String[] args) {
PizzaStore pizzaStore = new PizzaStore();
// 点一份奶酪比萨
pizzaStore.orderPizza("cheese");
}
}
/**
* 输出
*
* 准备 ...奶酪比萨
* 添加配料 ...大蒜番茄酱
* 烘烤 25 分钟。
* 把比萨饼切成对角片。
* 将比萨放到比萨商店的盒子中。
*
*/
1.3 设计引发的问题
上面代码中我们只列出了两种类型的比萨,在实际生产的过程中肯定不止这两种比萨。为了解决这个问题我们可以在 PizzaStore 类中的 orderPizza() 方法中添加类型判断,从而生产不同类型的比萨。所以,随着比萨类型的增多,这个方法会不停的进行判断。
假如某个比萨的销量不好,我们不想再生产这种类型的比萨,那么我们必须要找到 PizzaStore 类中的 orderPizza() 方法,将该种类型的比萨删除。
所以我们总结出:比萨的类型是多变的。这时候我们就要重新考虑设计的方式,简单工厂模式就可以帮我们解决这个问题。它将一些容易改变的代码抽取出来,单独封装。
正好符合我们的一个设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
二、工厂模式登场
2.1 简单工厂模式亮相
(1)简单工厂模式
在《Head First 设计模式》一书中这样描述简单工厂:简单工厂其实并不是一个设计模式,反而比较像一种编程习惯。但是由于经常被使用,所以我们给它一个“Head First Pattern 荣誉奖”。
(2)简单工厂组成结构
工厂类角色:含有一定的商业逻辑和判断逻辑,用来创建产品。
抽象产品角色:它一般是具体产品继承的父类或者实现的接口。
具体产品角色:工厂类所创建的对象就是此角色的实例。
(3)简单工厂设计图
(4)简单工厂代码实现
工厂类 SimplePizzaFactory
package com.jas.simplefactory;
public class SimplePizzaFactory {
private SimplePizzaFactory(){};
public static Pizza createPizzs(String type){
Pizza pizza = null;
if("cheese".equals(type)){
pizza = new CheesePizza();
}else if("greek".equals(type)){
pizza = new GreekPizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
改写比萨商店类 PizzaStore
package com.jas.simplefactory;
public class PizzaStore {
public Pizza orderPizza(String type){
Pizza pizza = null;
pizza = SimplePizzaFactory.createPizzs(type);
return pizza;
}
}
测试类 PizzaTestDrive
package com.jas.simplefactory;
public class PizzaTestDrive {
public static void main(String[] args) {
PizzaStore pizzaStore = new PizzaStore();
//点一份希腊比萨
pizzaStore.orderPizza("greek");
}
}
/**
* 输出
*
* 准备 ...希腊比萨
* 添加配料 ...大蒜番茄酱
* 烘烤 25 分钟。
* 把比萨饼切成对角片。
* 将比萨放到比萨商店的盒子中。
*
*/
(5) 简单工厂总结
使用简单工厂模式,我们将 PizzaStroe 中易改变的代码抽取出来单,独用一个工厂类封装起来。当我们想要增加或减少比萨种类时,我们不必在去修改 PizzaStroe 类,直接修改 SimplePizzaFactory 类中的代码即可。
这样以来代码变得更灵活了,代码的复用性变得更强。当其他类中也需要 Pizza 实例时,也可以直接从 SimplePizzaFactory 类中获得。
2.2 工厂方法模式亮相
(1)工厂方法模式
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
(2)工厂方法模式组成结构
抽象工厂角色:是具体工厂角色必须实现的接口或者必须继承的父类。在 java 中它由抽象类或者接口来实现。声明了抽象的工厂方法。
具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。实现了父类或接口中定义的抽象工厂方法。
抽象产品角色:它是具体产品继承的父类或者是实现的接口。在 java 中一般由抽象类或者接口来实现。
具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在 java 中由具体的类来实现。
(3)工厂方法模式的 UML 图解
(4) 比萨店连锁啦
由于比萨店的销量比较好,你挣了一大笔钱,因此你想开几家分店。最终你选择在北京和上海各开一家分店。但是由于地区的差异,北京和上海两地的口味也不一样。
北京人希望比萨的调料会多一些,而上海人则希望比萨的调料少一些。所以根据口味的不同,你要制造不同口味的比萨来满足顾客。
我们已经有了一个简单的比萨生产工厂 SimplePizzaFactory,但是这个工厂加工的比萨并不对北京和上海两地人的胃口。于是就想着再建两个比萨生产工厂 (BJPizzaFactory 和 SHPizzaFactory),分别生产不同口味的比萨。
但是这样做有一个不好的地方,虽然不同的比萨生产工厂能够生产出对应的比萨,但是其他的方法比如:烘焙,切片和包装却使用着自己的生产流程 (生产一致则存在大量重复代码)。但是你希望这些生产流程在每个工厂都应该一样 (复用烘焙,切片和包装的代码,或者覆盖它们),使用简单工厂的方式就不再能够行的通了。
于是工厂方法就站了出来。
(5)工厂方法模式设计图
(6)工厂方法模式代码实现
代码用了上面的 Pizza 类。
抽象工厂角色 PizzaStore 抽象类
package com.jas.factorymethod;
import com.jas.simplefactory.Pizza;
public abstract class PizzaStore {
public Pizza orderPizza(String type){
Pizza pizza = null;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
/**
* 工厂方法,将生产比萨的方法定义成抽象方法,让子类去实现
* @param type 比萨的类型
* @return 对应的比萨实例
*/
protected abstract Pizza createPizza(String type);
}
具体工厂角色 BJPizzaStore 类 (这里为了防止篇幅过长,省略了 SHPizzaStore 类和对应的比萨类)
package com.jas.factorymethod;
import com.jas.simplefactory.Pizza;
public class BJPizzaStore extends PizzaStore {
@Override
protected Pizza createPizza(String type) {
Pizza pizza = null;
if("cheese".equals(type)){
pizza = new BJCheesePizza();
}else if("greek".equals(type)){
pizza = new BJGreekPizza();
}
return pizza;
}
}
具体产品角色 BJCheesePizza 类 (这里只定义一种比萨的种类)
package com.jas.factorymethod;
import com.jas.simplefactory.Pizza;
public class BJCheesePizza extends Pizza {
public BJCheesePizza(){
name = "北京人喜欢吃的奶酪比萨";
sauce = "浓浓的大蒜番茄酱";
}
/**
* 覆盖 curt() 方法将比萨切成块状
*/
@Override
public void cut(){
System.out.println("把比萨饼切成方块状。");
}
}
测试类
package com.jas.factorymethod;
public class PizzaTestDrive {
public static void main(String[] args) {
PizzaStore pizzaStore = new BJPizzaStore();
//根据北京人的口味要一个奶酪比萨
pizzaStore.orderPizza("greek");
}
}
/**
* 输出
*
* 准备 ...北京人喜欢吃的希腊比萨
* 添加配料 ...浓浓的大蒜番茄酱
* 烘烤 25 分钟。
* 把比萨饼切成方块状。
* 将比萨放到比萨商店的盒子中。
*
*/
(7)工厂方法模式总结
简单工厂模式中将生产比萨的代码单独抽取了出来,用一个工厂进行封装,由该工厂生产比萨实例。但是由于开了比萨连锁店,建立多个比萨工厂并不是好的解决办法。
所以我们使用了工厂方法模式,在比萨 PizzaStore 抽象类中定义了一个抽象方法 createPizza() 方法,由其子类去实现该方法,这样以来不同的连锁店就能生产自己的比萨了。
如果增加产品或者改变产品的实现,PizzaStore 并不需要做任何的改变。以很好的方式实现了 PizzaStore 与具体比萨之间的解耦。
2.3 抽象工厂模式亮相
(1) 抽象工厂模式
抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道或关心产出的具体产品类是什么。这样一来,客户就从具体的产品中被解耦。
(2) 抽象工厂模式的 UML 图解
(3)比萨店连锁店出了一些状况
比萨连锁店成功的关键是在于新鲜、高质量的原料。但是有的连锁店开始使用低价的原料来增加利润,为了防止比萨店的声誉遭到破坏,你必须采取相对应的措施。为了解决这个问题,你打算新建一家生产原料的工厂,并将原料运往各家加盟店,这样以一来,所有的加盟店都使用你生产的原料来制作比萨。
但是这样做却有一个问题:因为加盟店坐落于不同的地域,比如北京和上海,它们需要的酱料是不一样的。所以为了满足需求,你为它们准备了两组不同的原料。
于是你有了一个想法,你打算创建一个原料接口,由不同地域的工厂自己去实现。
(4)抽象工厂模式设计图
(5) 抽象工厂模式代码实现
抽象原料工厂 PizzaInfgredientFactory 接口
package com.jas.abstractfactory;
public interface PizzaInfgredientFactory {
Dough createDough();
Cheese createCheese();
}
具体原料工厂 SHPizzaInfgredientFactory 类
package com.jas.abstractfactory;
/**
* 这里是上海原料加工厂,生产的原料是薄面团与 Reggiano 干酪
*/
public class SHPizzaInfgredientFactory implements PizzaInfgredientFactory {
@Override
public Dough createDough() {
return new ThinCrustDough();
}
@Override
public Cheese createCheese() {
return new ReggianoCheese();
}
}
抽象 Pizza 类
package com.jas.abstractfactory;
public abstract class Pizza {
public String name;
//为了篇幅过长,不再贴出原料相关代码
public Dough dough;
public Cheese cheese;
public abstract void prepare();
public void bake(){
System.out.println("烘烤 25 分钟。");
}
public void cut(){
System.out.println("把比萨饼切成对角片。");
}
public void box(){
System.out.println("将比萨放到比萨商店的盒子中。");
}
}
具体 CheesePizza 类 (这里只新建了一个比萨具体实现类)
package com.jas.abstractfactory;
public class CheesePizza extends Pizza {
PizzaInfgredientFactory infgredientFactory = null;
public CheesePizza(PizzaInfgredientFactory infgredientFactory){
this.infgredientFactory = infgredientFactory;
}
@Override
public void prepare() {
System.out.println("准备" + name);
dough = infgredientFactory.createDough();
cheese = infgredientFactory.createCheese();
}
}
具体 SHPizzaStore 类
package com.jas.abstractfactory;
public class SHPizzaStore {
PizzaInfgredientFactory infgredientFactory = new SHPizzaInfgredientFactory();
protected Pizza createPizza(String type){
Pizza pizza = null;
if("cheese".equals(type)){
//生产比萨的原料来自上海的原料加工工厂
pizza = new CheesePizza(infgredientFactory);
pizza.name = "原料来自上海工厂加工的奶酪比萨";
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
测试类 PizzaTestDrive
package com.jas.abstractfactory;
public class PizzaTestDrive {
public static void main(String[] args) {
SHPizzaStore pizzaStore = new SHPizzaStore();
pizzaStore.createPizza("cheese");
}
}
/**
* 准备原料来自上海工厂加工的奶酪比萨
* 烘烤 25 分钟。
* 把比萨饼切成对角片。
* 将比萨放到比萨商店的盒子中。
*/
(6)抽象工厂模式总结
通过抽象工厂所提供的接口,可以创建产品的家族,利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各式各样的工厂,制造各样不同的产品。
三、工厂方法模式与抽象工厂模式总结
工厂方法与抽象工厂的工作都是负责创建对象,但是工厂方法使用的方法是继承,而抽象工厂使用的是组合。
这意味着,利用工厂方法创建对象,需要扩展一个类,并实现其中的工厂方法。由这个方法创建对象,只不过这个方法通过子类创建对象,用这种做法,客户只需要知道他们所使用的抽象类型就可以了,而由子类负责决定具体类型。所以,工厂方法只负责将客户从具体类型中解耦。
抽象工厂提供一个类来创建一个产品家族的抽象类型,这个类型的子类定义了产品被产生的方法。要使用这个工厂,必须先要实例化它,然后将它传入一些针对抽象类型所写的代码中。
所以,和工厂方法一样,抽象工厂也可以实现客户从所使用地实际具体产品中解耦。但是它还有另一个优点:可以把一群相关的产品集合起来创建。但是这也有一个缺点:如果新加入创建的产品,就必须要改变接口...这样做的后果是很严重的。
其实抽象工厂中的具体工厂经常使用工厂方法来创建产品。总之,它们两个都可以将对象的创建封装起来,使应用程序解耦,因此降低程序之间的依赖。
当你需要创建产品家族和想让制造的相关产品集合起来时,你可以使用抽象工厂。当你需要创建一种类型的对象时,你可以选择使用工厂方法。
参考资料
《Head First 设计模式》