软件设计模式(2):创建者模式——工厂模式

1.2、工厂模式

在Java中,万物皆是对象,这些对象都需要创建,如果创建的时候直接让用户简单的new出对象,就会造成用户类和对象类双方耦合严重,因为你的代码中会与该类的创建绑定。毕竟无论如何,只要需要使用对象,最终都是需要把具体对象new出来的,这个步骤不可跳过。
所以我们发现,很多时候,将对象的创建这件事交由客户完成并不明智,一来复杂对象的创建往往带来许多麻烦,二来如果该对象的某些地方发生了改变,那么还得来修改这些引用该对象的代码,耦合度过高十分不便。

这里突兀插入一下工厂类的意义,所谓工厂类就是专门用于创建并返回对象的类。且客户不关心对象是如何被创建出来的。

而上面提到的问题,也正是工厂模式打算解决的问题,要想进一步理解工厂模式的意义,我们看下面这个例子:

这是一个咖啡店的例子

public abstract class Coffee {
	//咖啡抽象类
    public abstract void getCoffee();

    public void addMilk(){
        System.out.println("milk");
    }

    public void addSugar(){
        System.out.println("sugar");
    }
}

public class AmericanCoffee extends Coffee{//美式咖啡实体类
    @Override
    public void getCoffee() {
        System.out.println("this is a cup of American Coffee");
    }

}

public class Espresso extends Coffee{//意式浓缩实体类
    @Override
    public void getCoffee() {
        System.out.println("this is a cup of Espresso!");
    }
}

public class CoffeeStore {//咖啡店
    public Coffee buyCoffee(String type){//通过名字买咖啡,没有对应咖啡名则抛出一个错误
        Coffee coffee = null;
        if(type == "Espresso"){
            coffee = new Espresso();
            coffee.getCoffee();
        }else if(type == "American"){
            coffee = new AmericanCoffee();
            coffee.getCoffee();
        }else {
            throw new RuntimeException("不存在指定的咖啡种类!");
        }
        coffee.addMilk();
        coffee.addSugar();
        return coffee;
    }
}

public class Client {//客户来了
    public static void main(String[] args) {
        CoffeeStore cs = new CoffeeStore();
        String s1 = "Espresso";
        String s2 = "American";
        String s3 = "Cappuccino";
        //客户拿了菜单,他说他想喝意式浓缩,美式咖啡,再来一杯卡布奇诺
        Coffee c1 = cs.buyCoffee(s1);
        Coffee c2 = cs.buyCoffee(s2);
        Coffee c3 = cs.buyCoffee(s3);//Error,咱家现在还没有卡布奇诺
    }
}

老板某日视察,突然说每天点卡布奇诺的那么多,我们就做一个卡布奇诺吧!我:那行。

public class CoffeeStore {//咖啡店
    public Coffee buyCoffee(String type){//通过名字买咖啡,没有对应咖啡名则抛出一个错误
        Coffee coffee = null;
        if(type == "Espresso"){
            coffee = new Espresso();
            coffee.getCoffee();
        }else if(type == "American"){
            coffee = new AmericanCoffee();
            coffee.getCoffee();
        }else if(type == "Cappuccino"){
            coffee = new Cappuccino();
            coffee.getCoffee();
        }else {
            throw new RuntimeException("不存在指定的咖啡种类!");
        }
        coffee.addMilk();
        coffee.addSugar();
        return coffee;
    }
}

于是我创建了新对象,再把咖啡店的代码改了一遍。于是这里引入了问题:

假如后面我们需要对咖啡对象的创建做修改,比如决定糖分情况,有没有奶泡,亦或是多了数种咖啡需要创建,那么所有new出咖啡的地方就都需要修改一遍,现在我们只有咖啡店一个用户,假设公司以后有了加盟,开展了其他业务也需要咖啡对象呢?比如我们加入冰淇淋店类IceCreamStore然后该类可以把这些咖啡做成冰淇淋,多一种咖啡那就得同样加上一种冰淇淋去对。
假设我们现在有一百个商店类用到了咖啡,现在我们多了一种咖啡就需要到一百个new咖啡来用的地方修改。。。虽然是极端情况,但也可以想见这是很不方便的。

显然,我们针对此产品,只希望能在要用到它的时候能简单的获取它的对象,不太想要在用它的时候还必须得去关心奇奇怪怪的细节,更不希望因为这些细节发生改变,导致我获取它的时候遇到麻烦。

为什么用个对象这么麻烦呢?因为很显然,我们的代码违背了软件设计的开闭原则,而这个问题也就引出了工厂的概念。

工厂是一个用来创建对象的类,或者说的更细一点:一个隐藏对象创建细节的类,如果我们需要什么对象,我们只需要与工厂招呼一声即可。既不用操心创建过程,也不需要考虑构建对象的细节属性。只需要拿取已经创建好的产品即可。
这样,客户类与其对象彻底解耦,即便后面该类发生了修改,我们也只需要维护工厂类的代码,只要工厂类提供的API还是一样的,就无需修改那些通过工厂类引用了该对象的代码。

所以说,工厂模式最大的特点和意义就是:解耦

​ 我们将要对以下几种工厂模式进行学习:

  • 简单工厂模式(注意,此类不属于GoF设计模式)
  • 工厂方法模式
  • 抽象工厂模式

1.2.1、简单工厂模式

简单工厂模式不是一种设计模式,反而像是一种编程习惯。

简单工厂的特点是工厂本身没有一个抽象类或接口用于继承,而是单纯的用了一个工厂将所有产品对象创建出来,这意味着以后如果需要加入新产品,就必须修改工厂类代码,没法通过实现接口来新创建工厂类,所以这个方法依然违背了开闭原则。

不过该方法可以通过一些改进手段来避免违背开闭原则,具体详见工厂模式拓展

简单工厂结构

简单工厂应当包含以下角色:

  • 抽象产品:定义了产品的标准,描述其主要特性与功能
  • 具体产品:实现了抽象产品类的子类,定义了其具体实现
  • 具体工场:提供了创建产品的方法,调用者通过该方法来创建产品

那么这里要想解除咖啡店与具体咖啡的耦合,我们只需要修改以下咖啡店的代码并创建一个简单咖啡工厂类即可。

public class CoffeeSimpleFactory {//简单咖啡工厂
    public Coffee createCoffee(String type){
        Coffee coffee;
        //注意这里,我们只有一个工厂,所以本质上仍然没有遵守开闭原则,只是修改的对象变成了工厂而已
        if(type == "Espresso"){
            coffee = new Espresso();
        }else if(type == "American"){
            coffee = new AmericanCoffee();
        }else {
            throw new RuntimeException("不存在指定的咖啡种类!");
        }
        return coffee;
    }
}

public class CoffeeStore {//修改后的咖啡店
    public Coffee buyCoffee(String type){
        CoffeeSimpleFactory factory = new CoffeeSimpleFactory();
        Coffee coffee = factory.createCoffee(type);
        coffee.getCoffee();
        coffee.addMilk();
        coffee.addSugar();
        return coffee;
    }
}

有了这个简单工厂,我们就解除了商店和具体咖啡之间的耦合,同时引入了工厂和咖啡的耦合以及商店和工厂的耦合。

后期如果还需要添加新的咖啡,我们势必需要修改工厂类的代码,这虽然违反了开闭原则,但是由于工厂只负责生产对象,不实现具体业务,所以即便违反了原则,我们也只需要修改工厂类的源代码,省去了许多其他的工作。

简单工厂模式优缺点:

优点:

封装了创建对象的过程,可以通过参数直接获取对象,把对象的创建和业务逻辑层分层,这样以后就避免了修改客户代码,如果要实现新产品则直接修改工厂类,无需去业务实现类中修改,这样就降低了客户代码修改的可能性,易于进一步扩展代码

缺点:

以后增加新的对象种类就需要修改工厂类代码,违背了开闭原则,还是不能用拓展代替修改
即便违背了开闭原则,也还是会比正常耦合的情况强得多

补充:静态工厂方法

还有一些人会将工厂类中的创建对象的功能定义为静态的,如此一来就会成为静态工厂模式,它也不是23种设计模式之中的内容,其具体代码如下:

public class CoffeeSimpleFactory {//简单咖啡工厂,静态版
    public static Coffee createCoffee(String type){//静态工厂说白了就是不需要工厂对象的简单工厂
        Coffee coffee;
        if(type == "Espresso"){
            coffee = new Espresso();
        }else if(type == "American"){
            coffee = new AmericanCoffee();
        }else {
            throw new RuntimeException("不存在指定的咖啡种类!");
        }
        return coffee;
    }
}

静态工厂方法和简单工厂方法并没有太大区别,其核心区别就是静态与非静态之间的区别:

  • 类加载时该方法及其所需内容加载至方法区
  • 生命周期与类相同
  • 全局唯一
  • 使用方式上不依赖于实例对象

1.2.2、工厂方法模式

针对简单工厂模式违背了开闭原则的缺点,衍生出了工厂方法模式,可以将该问题完美解决。

其核心思路在于,工厂本身只实现一个接口,由子类来决定究竟要实现哪个产品类对象,使得产品类对象的实例化延迟到子类之中,从而让生产不同产品的问题可以通过拓展子类而不是修改代码的方式来解决。

工厂方法结构

  • 抽象工厂:提供了创建产品的接口,调用者可以通过它来调用具体子类的方法
  • 具体工厂:实现了抽象工厂中的接口方法,完成具体产品的创建
  • 抽象产品:定义了产品的规范,描述产品的属性与功能
  • 具体产品:实现了抽象产品所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应

工厂方法完美遵守了依赖倒转原则,各个类之间的依赖都为其抽象的依赖。

下面我们来修改上面的这个例子:

//首先是两个抽象的接口,作为万能的引用描述对象特点
//产品类接口
public interface Coffee {
    default void addMilk(){
        System.out.println("加奶");
    }
    default void addSugar(){
        System.out.println("加糖");
    }
    void getCoffee();
}
//工厂类接口
public interface CoffeeFactory {
    Coffee createCoffee();
}

//接下来我们要一一对应的实现每个类以及其工厂类
//首先是美式咖啡
public class American implements Coffee{
    @Override
    public void getCoffee() {
        System.out.println("这是一杯美式咖啡");
    }
}

public class AmericanCoffeeFactory implements CoffeeFactory{
    @Override
    public Coffee createCoffee() {
        return new American();
    }
}

//意式浓缩
public class Espresso implements Coffee{

    @Override
    public void getCoffee() {
        System.out.println("这是一杯意式浓缩");
    }
}

public class EspressoFactory implements CoffeeFactory{
    @Override
    public Coffee createCoffee() {
        return new Espresso();
    }
}

//那么这里主题就都构建好了,接下来我们来实现咖啡店类
public class CoffeeStore {
    private CoffeeFactory factory;//每个咖啡店私有的工厂对象

    public void setFactory(CoffeeFactory factory) {//控制反转,将需要什么咖啡的选择权交给用户
        this.factory = factory;
    }

    public Coffee getCoffee(){//获得咖啡对象并处理,随后返回交给用户
        if(factory==null){
            throw new RuntimeException("未指定咖啡种类");
        }
        Coffee coffee = factory.createCoffee();
        coffee.getCoffee();
        coffee.addMilk();
        coffee.addSugar();
        return coffee;
    }
}

//测试
public class Client {
    public static void main(String[] args) {
        CoffeeStore cs = new CoffeeStore();//获取咖啡店对象
        cs.setFactory(new EspressoFactory());//选取意式浓缩
        cs.getCoffee();
        cs.setFactory(new AmericanCoffeeFactory());//选取美式
        cs.getCoffee();
    }
}

可以看出,由于完美的遵守了依赖倒转原则,我们的工厂类,咖啡类,商店类之间都不是直接的实体类依赖,而是依赖于其抽象,如此一来,我们后续添加其他种类的咖啡就无需修改工厂类的代码,而只需要添加新的继承接口的类即可。

比如现在我们希望添加卡布奇诺产品,那么我们只需要添加该实体类以及对应工厂实现类就可以直接实现热插拔的效果。

class Cappuccino implements Coffee{
    @Override
    public void getCoffee(){
        System.out.println("这是一杯卡布奇诺");
    }
}

class CappuccinoFactory implements CoffeeFactory{
    @Override
    public Coffee createCoffee(){
        return new Cappuccino();
    }
}

//试试看
public class Client {
    public static void main(String[] args) {
        CoffeeStore cs = new CoffeeStore();//获取咖啡店对象
        cs.setFactory(new CappuccinoFactory());//选取卡布奇诺,这是合理的修改,来自客户的选择
        cs.getCoffee();
    }
}

成功,我们无需修改原有代码,只需要添加两个实现了接口的子实现类,就可以让用户直接选用对应的工厂产出的咖啡了。

工厂模式优缺点

优点

  • 客户仅仅需要知道对应的工厂的名称,就可以直接得到对应的对象,无需关心其具体创建生产过程
  • 系统添加新的产品时,只需要添加新的继承对应接口的工厂类和实体类,无需修改原有代码就能完成,满足开闭原则。

缺点

  • 每增加一个产品,都必须添加一个对应工厂类和产品类,所以增加了系统的复杂度,这是工厂模式的核心缺陷
    如果产品种类极多,就会产生类爆炸

1.2.3、抽象工厂模式

前面的工厂方法模式中考虑的是一类产品的生产,就好像畜牧业只养动物,汽车业只造汽车,码农只敲代码一样。

这些工厂只生产同种类产品,同种类产品为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品。
但是在现实生活中,有许多工厂都会生产多种类的产品,比如电器场既造冰箱,又造空调。

抽象工厂模式将要考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的产品成为一个产品族,同类产品则为同一个产品等级,也就是说,对于产品对象类我们现在有两条轴线了。

知道了目的,现在我们可以引入抽象工厂模式的概念了:

抽象模式工厂是一种为访问类提供一个用于创建一组相关或相互依赖的对象的接口,且访问类无需指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

结构

  • 抽象工厂:提供了创建产品的接口,包含了多个创建产品的方法,可以创建多个不同等级,同一产品族的产品
  • 具体工厂:实现了抽象工厂中的接口方法,完成具体产品的创建
  • 抽象产品:定义了产品的规范,描述产品的属性与功能,抽象工厂模式会存在多个抽象产品
  • 具体产品:实现了抽象产品所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系,即一种工厂能生产多种产品

聊完了结构,我们来看一个具体情况来深入理解抽象工厂模式:

假设还是那家咖啡店,现在他们决定扩大产业,不光要生产咖啡,还要生产甜品,包括提拉米苏,甜甜圈以及黑森林蛋糕。按照工厂方法模式的话,我们需要两种工厂,而且一种要实现生产咖啡:意式浓缩,美式咖啡,卡布奇诺;另一种要实现生产甜品:提拉米苏,甜甜圈,黑森林蛋糕。再加上它们的本体类,很容易就会发生类爆炸。

那么这时候就需要抽象工厂类出场了,分析以后我们发现,这些产品有如下关系:分两个产品等级:咖啡,甜品。还有三种产品族:意式口味(提拉米苏,意式浓缩),美式口味(甜甜圈,美式咖啡),法式风味(卡布奇诺,黑森林)。

那么类图绘制如下:

可以看到,我们同一个等级的产品继承自同一个接口,而同一个产品族的类,由同一个具体工厂生产,所有工厂继承自一个能生产所有等级产品的工厂类。

什么意思?其实从思想上来说很好理解,既然产品种类多了,那我们必然需要整合生产这些产品的工厂类,但是同等级的产品继承自同一个接口无法整合。
怎么办呢?我们将思想由竖切改为横切,把不同等级的产品整合至同一个工厂中生产,这样还是保持了之前工厂方法模式的依赖倒转原则,即实现类和工厂类皆依赖于抽象。

不过这么做的代价就是会违背接口隔离原则。比如如果现在商店希望生产一种自制的咖啡:霜糖桂香咖啡,它不属于任何产品族,仅仅只是一款自制咖啡,那么从抽象工厂方法就无法完美贴合需求了,因为如果我们要实现具体工厂,就必须要让它实现一个它不需要的createDessert()方法。违背了接口隔离原则。

代码:

//三个接口,咖啡,甜品,工厂
public interface Coffee {
    void getCoffee();

    default void addMilk(){
        System.out.println("加奶");
    }

    default void addSugar(){
        System.out.println("加糖");
    }
}

public interface Dessert {
    void getDessert();
}

public interface DesFactory {
    Coffee createCoffee();
    Dessert createDessert();
}

//甜品实体类
public class Tiramisu implements Dessert{
    @Override
    public void getDessert() {
        System.out.println("这是一块提拉米苏");//三种就这里不一样
    }
}

//咖啡实体类
public class Espresso implements Coffee {
    @Override
    public void getCoffee() {
        System.out.println("这是一杯意式浓缩");//三种就这里不一样
    }
}

//实现工厂类
public class ItalyDesFactory implements DesFactory{
    @Override
    public Coffee createCoffee() {
        return new Espresso();//对应工厂类创建对应产品族对象
    }

    @Override
    public Dessert createDessert() {
        return new Tiramisu();//同上道理
    }
}

//咖啡店,完成控制反转
public class CoffeeStore {
    private DesFactory factory;

    public void setFactory(DesFactory factory) {//IoC核心步骤,将产品族选择权交给用户而非程序员
        this.factory = factory;
    }

    public Coffee orderCoffee(){
        if(factory==null){
            throw new RuntimeException("未指定需要咖啡的种类");
        }
        Coffee coffee = factory.createCoffee();
        coffee.addMilk();
        coffee.addSugar();
        return coffee;
    }

    public Dessert orderDessert(){
        if (factory==null){
            throw new RuntimeException("未指定需要甜品的种类");
        }
        return factory.createDessert();
    }
}

上述例子中,我们最后显然可以完成不同对象的获得,这就是抽象工厂模式,它可以使得当你需要添加一个产品族的时候,只需要添加一个具体工厂类就可以实现。

抽象工厂模式优缺点

优点:

  • 当一个产品族的对象被设计成一起工作时,抽象工厂模式可以保证所有对象都属于该产品族
  • 当需要添加一个产品族的新对象时,仅需要添加一个工厂类

缺点:

  • 如果某个产品族需要添加新产品,所有工厂类皆需要修改
  • 如果某些对象在使用时是不必要的,那么由于违反了接口隔离原则,会造成效率的下降

适用场景

适宜:

  • 当需要一众相互关联,相互依赖的对象共同完成某个工作时。
  • 系统中有多个产品族,但每次调用都仅仅使用其中之一时。

1.2.4、工厂模式拓展

(Spring)简单工厂模式+配置文件

简单工厂模式+配置文件解除耦合。我们在工厂类中加载配置文件,取得全部的类名,然后将所有对象创建出来并保存,如果客户需要即返回该对象。

下面来实例:首先是配置文件bean.properties

American = pojo.AmericanCoffee
Espresso = pojo.Espresso

下面就是工厂类了,工厂类显然需要以下功能:

  • 根据输入名字返回对象
  • 能加载配置文件并根据文件内容创建对象
  • 能保存对象

实现方法就是,首先创建一个HashMap用来存储对象,然后通过字节码对象的加载器获取.properties文件内容,再通过反射,利用foeName()取得字节码对象,再newInstance()取得对象

public class beanFactory {
    private static HashMap<string,coffee> map;//用于存储对象

    static {
        InputStream is = beanFactory.class.getClassLoader().getResourceAsStream("bean.properties");//获取.properties输入流
        Properties p = new Properties();
        try {
            p.load(is);
            Set<object> keys=p.keySet();
            for (Object key : keys) {
                Class clazz = Class.forName((String) key);
                Coffee coffee = (Coffee) clazz.newInstance();
                map.put((String) key,coffee);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Coffee getCoffee(String name){
        return map.get(name);
    }

}

1.2.5、源码案例

Iterator(迭代器)

Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。Iterator 是 Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。

  • 迭代器 it 的基本操作是 next 、hasNext 和 remove。
  • 调用 it.next() 会返回迭代器的下一个元素,并且更新迭代器的状态。
  • 调用 it.hasNext() 用于检测集合中是否还有元素,一般配合next方法使用
  • 调用 it.remove() 将迭代器返回的元素删除。

要想获取一个Iterator对象可以使用 iterator() 方法:

ArrayList<string> list = new ArrayList<string>();
Iterator<string> it = list.iterator();

而这个获取对象的过程,其实就是一个工厂方法模式的实例:

  • Collection:抽象工厂
  • Iterator:抽象产品
  • ArrayList:具体工厂
  • ArrayList内部的Iter:具体产品

从这个例子我们还可以看出来,这些设计模式是可以相互嵌套的,并不是说具体工厂就一定得是一个纯粹的工厂,工厂本身也可以和功能复合,而这一点可以通过实现多个接口实现——只要满足开闭原则,依赖倒转原则,接口隔离原则。。。
在设计过程中,我们必须要考虑好六大原则的问题,避免出现过度耦合等失误。

posted on 2021-10-18 16:42  寒光潋滟晴方好  阅读(72)  评论(0编辑  收藏  举报