讲个故事,看看能不能理解工厂方法模式

1,556 阅读9分钟

工厂方法模式

前言

今天想聊一下老生常谈的一种模式,工厂方法模式。另外本文中会出现大量的代码,不过都很简单容易理解。

下面两张图,是本文中可能会用到的。感兴趣的可以看一下。希望大家可以看完本文,喜欢的话可以点个赞支持下。

常见的设计原则

设计原则.png

工厂模式的种类

工厂模式.png

讲个故事

这几年新能源车比较流行。特斯拉,比亚迪,五菱等车企都在发展新能源车。汽车嘛,品牌不同,配置不同,价格也不太一样。毕业生小白就职于一家汽车媒体公司,一天组长把他叫来,让他收集一下这些车企新能源车的相关信息。今天的故事就从这里开始了。

正常的写法

收到任务后的小白心想,这个任务太简单了,没有任何难度。于是他立刻开始动手,因此有了下面这份代码。

产品类

// 比亚迪类
class BYD{
    public void name() {
        System.out.println("比亚迪");
    }
    public void price() {
        System.out.println("22万");
    }
    public void kind() {
        System.out.println("新能源");
    }
}
// 特斯拉类
class TSLA{
    public void name() {
        System.out.println("特斯拉");
    }

    public void price() {
        System.out.println("32万");
    }

    public void kind() {
        System.out.println("新能源汽车");
    }
}

客户端类

// 客户端调用代码
class Client {
    public static void main(String[] args) {
        BYD byd = new BYD();
        TSLA tsla = new TSLA();
    }
}

目前看着还算不错,两个品牌都有了自己的对象。于是他立刻把代码交给组长看。

普通写法遇到的问题

组长看过之后,对他说: 现在来说,只有两个品牌还好,但是后面的业务肯定还要发展,品牌肯定不止一个。
如果Client中有10个汽车品牌,100个汽车品牌该怎么办?难道一个Client类要和100个汽车品牌的类都有关联吗?迪米特法则——最少知道原则,难道你没听过吗?
一旦100个类中的一个类进行修改,就可能导致Client类出现问题!
你去对比下设计原则,看下问题出在哪儿!!!

简单工厂模式的写法

小白,仔细看了一遍代码,发现现在这种写法的确是耦合过于严重。Client类会和每个用到的汽车产品类产生关联。
不过好在可以用依赖反转 + 封装的方式修改原有代码,解决这个问题。

  1. 将汽车都有的属性或者方法方法抽离到汽车接口中
  2. 所有的汽车产品都实现汽车接口。这样可以通过多态的特性,将Client类与汽车产品之间进行解耦。
  3. 将所有对象的创建过程封装到一个管理类中,这个类可以叫做Manager或者Factory。

简单工厂模式的代码

1.将公共的方法抽离到接口中
/**
 * @author jtl
 * @date 2021/7/20 16:18
 */
interface Car {
    void name();

    void price();

    void kind();
}
2.汽车品牌实现该接口
class BYD implements Car {
    @Override
    public void name() {
        System.out.println("比亚迪");
    }

    @Override
    public void price() {
        System.out.println("22万");
    }

    @Override
    public void kind() {
        System.out.println("新能源");
    }
}

class BMW implements Car{
    @Override
    public void name() {
        System.out.println("宝马");
    }

    @Override
    public void price() {
        System.out.println("40万");
    }

    @Override
    public void kind() {
        System.out.println("燃油车");
    }
}

class Tesla implements Car {
    @Override
    public void name() {
        System.out.println("特斯拉");
    }

    @Override
    public void price() {
        System.out.println("32万");
    }

    @Override
    public void kind() {
        System.out.println("新能源汽车");
    }
}

3.汽车的管理工厂类
class CarFactory {

    public static Car getCar(String name) {
        Car car =null ;
        switch (name){
            case "宝马":
                car = new BMW();
                break;
            case "特斯拉":
                car = new TSLA();
                break;
            case "比亚迪":
                car = new BYD();
                break;
            default:
                break;
        }

        return car;
    }
}
4.客户端调用
class Client {
    public static void main(String[] args) {
        Car car = CarFactory.getCar("比亚迪");
        Car car = CarFactory.getCar("特斯拉");
        Car car = CarFactory.getCar("宝马");
    }
}

简单工厂遇到的问题

通过简单工厂模式,让Client类只和Car还有CarFactory打交道,降低了耦合度,哪怕出现1000个汽车品牌,也都和Client类没有关系了。

但是随之而来的是一个新的问题。每次要新增汽车品牌,都要修改Factory类中的方法。这可是违反了设计原则中的最关键的一条原则之一,开闭原则,即对扩展开放对修改关闭

而且虽然Client类耦合度下来了,但是所有的汽车类都和Factory类有了关联。这依旧违反迪米特法则——最少知道原则

工厂方法模式

正在小白一筹莫展的时候,组长走了过来对他说。你能想到这一步已经很出色了,我这里有一份代码已经上传到GitLab上,里面用到了工厂方法模式。你拉取一下看看写法。随后不等小白说谢就转身离开了。小白把代码拉下来之后就看到了下面的代码。

正常方式实现的工厂方法

抽象产品代码
/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:17
 * Detail(详情):抽象产品接口
 */
public interface ICar {
    void name();

    void price();
}
实际产品代码
/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:20
 * Detail(详情):具体产品类----比亚迪电动车
 */
public class BYD implements ICar{
    @Override
    public void name() {
        System.out.println("比亚迪新能源电动车");
    }

    @Override
    public void price() {
        System.out.println("20W");
    }
}

/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:21
 * Detail(详情):具体产品类---特斯拉电动车
 */
public class TSLA implements ICar{
    @Override
    public void name() {
        System.out.println("特斯拉新能源电动车");
    }

    @Override
    public void price() {
        System.out.println("28W");
    }
}
抽象工厂代码
/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:18
 * Detail(详情):抽象工厂接口
 */
public interface IFactory {
    ICar buildCar();
}
实际工厂代码
/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:23
 * Detail(详情):具体工厂类---比亚迪新能源工厂
 */
public class BYDFactory implements IFactory{
    @Override
    public ICar buildCar() {
        return new BYD();
    }
}

/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:24
 * Detail(详情):具体工厂类---特斯拉上海工厂
 */
public class TSLAFactory implements IFactory{
    @Override
    public ICar buildCar() {
        return new TSLA();
    }
}
客户端调用代码
/**
 * Author(作者):jtl
 * Date(日期):2023/2/8 14:24
 * Detail(详情):工厂方法模式客户端
 */
public class Client {
    public static void main(String[] args) {
        IFactory bydFactory = new BYDFactory();
        bydFactory.buildCar().name();

        IFactory tslaFactory = new TSLAFactory();
        tslaFactory.buildCar().name();
    }
}

为了解决,简单工厂模式违反开闭原则迪米特法则——最少知道原则。组长对简单工厂进行了修改。

  1. 将汽车共有属性或者方法抽离到**汽车接口(ICar)**中
  2. 所有的汽车产品(TSLA,BYD等) 都实现汽车接口(ICar)。这样可以通过多态的特性,进行Client类与汽车产品之间的解耦
  3. 将创建汽车产品TSLA,BYD等) 的功能进行抽离封装到IFactory工厂接口
  4. 每一个汽车产品(TSLA,BYD等) 都有自己的实现了IFactory接口汽车工厂(TSLAFactory,BYDFactory等)

新的问题

虽然说,工厂方法模式解决了简单工厂中Factory类和各种汽车产品类耦合度过高的问题。而且再也不用,每次新增一个汽车产品就去简单工厂中修改代码了。

但是工厂方法模式有一个致命的问题就是,每新增一个汽车品牌就要增加一个汽车产品和汽车工厂。今天新增个五菱的汽车和五菱工厂,明天再新增个理想的汽车和理想的工厂。新增1000个汽车品牌就要新增1000个汽车和工厂。这样下去class的数量就会过多。这是一个不可忽视的问题。

通过反射实现的工厂方法

小白把他想到的问题,一五一十的告诉了组长。组长十分安慰的看着小白说,你能想到这个问题很好,说明你进步了。工厂方法模式的确会出现这样的问题。因此我用反射写了一份工厂方法模式的代码,虽然说和汽车没关系,但是你可以类比一下。毕竟不能什么都和汽车有关系啊!

抽象工厂代码

对应IFactory

public abstract class Factory {
   public abstract <T extends Product> T createProduct(Class<T> clazz);
}
抽象产品代码

对应ICar

public abstract class Product {
    abstract void method();
}
具体工厂代码

对应BYDFactory等汽车工厂

public class ConcreteFactory extends Factory{

    // 通过反射的方式创建具体工厂,解决了,一个具体工厂对应一个具体产品的问题
    @Override
    public <T extends Product> T createProduct(Class<T> clazz) {
        Product product = null;
        try {
            product = (Product) Class.forName(clazz.getName()).newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }

        return (T) product;
    }
}
具体产品代码

对应BYD,TSLA等汽车产品

public class ConcreteProductA extends Product{
    @Override
    void method() {
        System.out.println("具体产品A");
    }
}

public class ConcreteProductB extends Product{
    @Override
    void method() {
        System.out.println("具体产品B");
    }
}
客户端代码实现
public class Client {
    public static void main(String[] args) {
        Factory factory = new ConcreteFactory();
        Product productA = factory.createProduct(ConcreteProductA.class);
        Product productB = factory.createProduct(ConcreteProductB.class);

        productA.method();//具体产品A

        productB.method();//具体产品B
    }
}

反射工厂方法中的妥协

通过反射的方式,解决了工厂方法模式中,过多的产品导致的产品工厂过多的问题。但是,Client类又和产品类有了一些耦合。只能说,没有十全十美的设计模式,只有合不合适。

枯燥无味的定义

听完了故事,就来看一下,官方说法的定义和使用场景吧,

相关定义

工厂方法模式分为四个组成部分,抽象工厂,抽象产品,具体工厂,具体产品。
工厂方法模式属于创建型设计模式。是为了将对象的创建与Client类解耦。
工厂会返回对象给ClientClient不知道创建的具体细节。

使用场景

1. 有大量相同属性的同类型对象。
2. 需要将这些对象的创建对客户端进行解耦。

使用步骤

1. 将具有相同特征的对象的共同特征进行提取。生成抽象产品类。
2. 生成继承抽象产品的具体产品。
3. 创建抽象工厂,生成返回抽象产品的抽象方法。
4. 生成创建具体产品的具体工厂,该工厂继承抽象工厂。重写抽象方法,返回具体产品的对象。

注意事项

  1. 优点,将对象的创建与客户端代码分离实现解耦。
  2. 缺点,每新增一个种类就要新增一个抽象产品和抽象工厂。
  3. 提升,可以用反射的方式实现具体工厂,而不是每有一个具体产品就创建一个与之相对应的具体工厂。可以减少代码量。

总结

至此,工厂方法模式就讲完了。简单工厂可以看作是工厂方法模式的一种。如果是简单的创建同类型对象的话,简单工厂模式就可以胜任了。但是如果是大的项目,还是推荐使用工厂方法模式进行解耦。至于是使用正常的工厂方法,还是反射的工厂方法,那就完全看个人的选择了。毕竟,只有合适才是最好的

另外,工厂方法模式的使用前提是,大量的同类型的对象的创建,这点可千万别记错了。