在学习工厂方法模式之前,我们先来看一个需求,设计一个咖啡店点餐系统。下面我们就来分析一下该需求。
咖啡店点餐系统点的肯定是咖啡,所以我们需要设计一个咖啡类(即Coffee),而咖啡它又有不同的品种:美式咖啡、拿铁咖啡等。这样,我们又得设计两个类,即美式咖啡类(即AmericanCoffee)和拿铁咖啡类(即LatteCoffee),很显然,我们得让这俩类去继承咖啡类,因为它俩可以向上抽取出共性的东西,而这些共性的东西可以放在咖啡类里面,用以提高代码的一个复用性。关于咖啡的这三个类设计完了之后,咱们还得设计一个咖啡店类,该类里面就具有点咖啡的功能,客户来了之后可以进行咖啡的一个点餐。
简单工厂模式 + 配置文件
简单工厂模式里面包含如下角色:
- 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。上面点咖啡的案例中就用到了抽象产品这个角色,就是咖啡类(即Coffee),它里面定义了一套规范
- 具体产品:实现或者继承抽象产品的子类。在上面点咖啡的案例中,具体产品指的就是拿铁咖啡、美式咖啡
- 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品
类图设计:
代码实现:
咖啡类:
//咖啡类
public abstract class Coffee {
public abstract String getName();
//加调料
public void addSpices(){
System.out.println("加糖");
}
}
美式咖啡类:
//美式咖啡
public class AmericanCoffee extends Coffee{
@Override
public String getName() {
return "美式咖啡";
}
@Override
public void addSpices() {
System.out.println("加少糖");
}
}
拿铁咖啡类:
public class LatteCoffee extends Coffee{
@Override
public String getName() {
return "拿铁咖啡";
}
@Override
public void addSpices() {
System.out.println("加奶");
}
}
接着,在maven工程目录下创建一个配置文件,为了演示方便,这里我们不妨使用properties文件来作为配置文件,文件名字就起为bean.properties
american=pattern.factory.config_factory.AmericanCoffee
latte=pattern.factory.config_factory.LatteCoffee
紧接着,创建工厂类,这里我们创建的是静态工厂模式的工厂类。
public class CoffeeFactory {
/**
* 我们所要所做的事情,就是加载配置文件,然后去获取配置文件中配置的全类名,并创建该类的对象进行存储。
*/
// 1. 定义容器对象存储咖啡对象
/*
* 容器选择双列集合
* 键:(咖啡)名称
* 值:咖啡对象
*/
private static HashMap<String,Coffee> map = new HashMap<String,Coffee>();
// 2. 加载配置文件中的全类名,并通过反射创建对象进行存储,注意,配置文件只需要加载一次。
// 由于配置文件只需要加载一次,所以我们最好将代码写在静态代码块里面
static {
//2.1 创建Properties对象
Properties p = new Properties();
//2.2 调用p对象的load方法进行配置文件的加载,注意,配置文件(即bean.properties)是在类路径下面哟!
InputStream in = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
p.load(in);
//从p集合中获取全类名并创建对象
Set<Object> keys = p.keySet();
for(Object key : keys){
//获取到全类名
String className = p.getProperty((String) key);
// 通过反射技术创建对象
Class clazz = Class.forName(className);
Coffee coffee = (Coffee) clazz.newInstance();
// 将名称和对象存储到容器中
map.put((String) key,coffee);
}
} catch (IOException | InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 根据名称获取Coffee对象
* @param name:咖啡的名称
* @return
*/
public static Coffee createCoffee(String name){
return map.get(name);
}
}
创建一个测试类来测试一下:
public class Client {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee("american");
coffee.addSpices();
System.out.println(coffee.getName());
Coffee coffee1 = CoffeeFactory.createCoffee("latte");
coffee1.addSpices();
System.out.println(coffee1.getName());
}
}
使用工厂模式+配置文件这种方式,扩展性更好,而且,它也符合开闭原则,这是因为如果我们此时再定义一个Coffee的子类的话,那么工厂类的代码是不需要改变的,我们只需要在配置文件里面再配置一行内容(即新咖啡类的全类名)就行了。
工厂方法模式
定义一个用于创建对象的接口(该接口就是工厂,里面并没有具体的去创建某一个产品对象),让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化(也即对象的创建)延迟到其工厂的子类中。
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它里面并没有创建任何产品对象。调用者通过它访问具体工厂(即抽象工厂的子类)的工厂方法来创建产品
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
使用工厂方法模式对以上案例进行改进之后设计出来的类图:
以上类图中可以看到,右边是咖啡类、美式咖啡类、拿铁咖啡类这三个类,而且它们也是父子类的关系,这和我们之前是一样的,所以我就不再过多的去强调了。中间是咖啡店类,它里面具有一个点咖啡的功能,而且它依赖于抽象(即Coffee),而没有依赖于具体,这就符合我们之前讲过的依赖倒转原则了。左边是一个接口(即CoffeeFactory),也就是抽象工厂,而且它里面有一个抽象的方法(即createCoffee)用于创建咖啡对象,这也就形成一套规范了,那么就要求子类必须去重写了。此外,CoffeeFactory接口下面还有两个子实现类:
- 美式咖啡生产工厂类(即AmericanCoffeeFactory),它是得要去重写父接口中的抽象方法的,很明显它是用来生产具体的美式咖啡的,所以它还得依赖于美式咖啡类
- 拿铁咖啡工厂类(即LatteCoffeeFactory),它也需要重写父接口中创建咖啡的抽象方法,很明显它是用来生产具体的拿铁咖啡的,所以它还得依赖于拿铁咖啡类
代码实现:
咖啡类、美式咖啡类、拿铁咖啡类这三个类不需要进行修改
根据以上类图去创建一个接口,即CoffeeFactory,它属于抽象工厂角色。
//CoffeeFactory:抽象工厂
public interface CoffeeFactory {
// 创建咖啡对象的方法
Coffee createCoffee();
}
接着,创建以上CoffeeFactory接口的子实现类
/**
* 美式咖啡工厂类,专门用来生产美式咖啡
*/
public class AmericanCoffeeFactory implements CoffeeFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
/**
* 拿铁咖啡工厂类,专门用来生产拿铁咖啡
*/
public class LatteCoffeeFactory implements CoffeeFactory {
@Override
public Coffee createCoffee() {
return new LatteCoffee();
}
}
创建一个咖啡店类,从以上类图中可以看到,它里面有一个点咖啡的功能,即orderCoffee,它并不需要参数,不需要参数的话,那么它就得根据具体的工厂去获取咖啡对象了。注意,在该方法里面,我们不再需要去创建具体的工厂对象了,而是会从客户端传递进来。
/**
* 咖啡店
*/
public class CoffeeStore {
private CoffeeFactory factory; // 依赖于抽象(即CoffeeFactory),而没有依赖于具体
public void setFactory(CoffeeFactory factory) {
this.factory = factory;
}
// 点咖啡功能。注意,这儿不再需要创建具体工厂对象了,而是会从客户端传递进来
public Coffee orderCoffee() {
Coffee coffee = factory.createCoffee();
// 加配料
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
测试类:
public class Client {
public static void main(String[] args) {
// 创建咖啡店对象
CoffeeStore store = new CoffeeStore();
// 创建具体工厂对象
// CoffeeFactory factory = new AmericanCoffeeFactory();
CoffeeFactory factory = new LatteCoffeeFactory();
store.setFactory(factory);
// 点咖啡
Coffee coffee = store.orderCoffee();
System.out.println(coffee.getName());
}
}
我们在客户端传递的是拿铁咖啡工厂对象,所以应该生产的是拿铁咖啡
如果我们想要添加一种新的咖啡的品种的话,那么再定义一个Coffee类的子类即可,而且此时工厂类中的方法都不需要修改,现在只需要再创建一个新的咖啡工厂的子实现类就行了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象,由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
工厂方法模式
优点:
- 用户只需要知道具体工厂的名称就可以得到所要的产品,无须知道产品的具体创建过程,产品的具体创建过程被封装到了具体的工厂里面
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足了开闭原则,因为我们只是新添加了类,并没有去修改之前类中的代码
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。如果产品类特别特别多,那么就会产生类爆炸现象,也即系统里面的类特别特别多