工厂方法模式

工厂方法模式

案例

我们有一个生产镜子工厂,工厂里可以生产圆形和方形镜子,顾客通过选择来使用不同的镜子。我们怎么通过程序来表示出这个过程呢?通过上一节的介绍,我们很快想到通过简单工厂模式的思路来解决这个问题:

1.首先有一个抽象的产品类:

/**
 * 抽象的产品类:镜子
 */
public interface Mirror {
    void look();
}
  • 圆形镜子产品类:
/**
 * 具体的产品类:圆形镜子
 */
public class RoundMirror implements Mirror {
    public void look() {
        System.out.println("使用圆形镜子看~~~");
    }
}
  • 方形镜子产品类:
/**
 * 具体的产品类:方形镜子
 */
public class SquareMirror implements Mirror {
    public void look() {
        System.out.println("使用方形镜子看~~~");
    }
}

2.生成镜子的工厂类:

/**
 * 生产产品的工厂类:镜子工厂
 */
public class MirrorFactory {
    public static Mirror getMirror(String type) {
        if("round".equalsIgnoreCase(type)){
            return new RoundMirror();
        }else if("square".equalsIgnoreCase(type)){
            return new SquareMirror();
        }else{
            throw new RuntimeException("选择的类型不存在");
        }
    }
}

3.客户端通过输入不同的类型来使用镜子:

/**
 * 客户端通过镜子工厂来生成镜子,从而使用它
 */
public class Main {
    public static void main(String[] args) {
        MirrorFactory mirrorFactory = new RoundMirrorFactory();
        Mirror mirror = mirrorFactory.getMirror();
        mirror.look();
    }
}

上面的代码使用了简单工厂模式的思想,通过创建了一个镜子工厂类MirrorFactory类来解决了创建不同镜子的问题。但它的缺点也很明显:

  • 工厂类中会包含很多的if...else...的代码。
  • 添加新类型的镜子类,如三角形的镜子,就需要修改工厂方法的代码。

如何改进这些问题?接下来就通过工厂方法模式来进行改进。

模式介绍

工厂方法模式(factory method)是一种常用的类创建型设计模式,此模式的核心精神是封装类中变化的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。它的核心结构有四个角色,分别是抽象工厂;具体工厂;抽象产品;具体产品。

角色构成:

  • Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
  • ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
  • Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口
  • ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

UML 类图:

工厂方法模式UML类图

特点:

工厂方法(Factory Method)模式的意义是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类当中。核心工厂类不再负责产品的创建,这样核心类成为一个抽象工厂角色,仅负责具体工厂子类必须实现的接口,这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品。

代码改造

通过上面的介绍,接下来对上面的案例代码进行如下改造:

  • Product(抽象产品):
/**
 * 镜子产品接口:担当抽象产品角色
 */
public interface Mirror {
    void look();
}
  • ConcreteProductA(具体产品):
/**
 * 圆形镜子:担当具体产品角色
 */
public class RoundMirror implements Mirror {
    public void look() {
        System.out.println("使用圆形镜子看~~~");
    }
}
  • ConcreteProductB(具体产品):
/**
 * 方形镜子:担当具体产品角色
 */
public class SquareMirror implements Mirror {
    public void look() {
        System.out.println("使用方形镜子看~~~");
    }
}
  • Factory(抽象工厂):
/**
 * 镜子工厂:担当抽象工厂角色
 */
public interface MirrorFactory {
    Mirror getMirror();
}
  • ConcreteFactoryA(具体工厂):
/**
 * 圆形镜子工厂:担当具体工厂角色
 */
public class RoundMirrorFactory implements MirrorFactory {
    public Mirror getMirror() {
        return new RoundMirror();
    }
}
  • ConcreteFactoryB(具体工厂):
/**
 * 方形镜子工厂:担当具体工厂角色
 */
public class SquareMirrorFactory implements MirrorFactory {
    public Mirror getMirror() {
        return new SquareMirror();
    }
}

客户端通过具体的镜子工厂使用具体的镜子:

/**
 * 客户端通过不同的镜子工厂来生成镜子,从而使用不同的镜子
 */
public class Main {
    public static void main(String[] args) {
        // 通过使用不同的镜子工厂来创建实际镜子对象,这里也可以通过读取配置文件的方式选取不同的工厂类
        MirrorFactory mirrorFactory = new RoundMirrorFactory();
//        MirrorFactory mirrorFactory = new SquareMirrorFactory();
        Mirror mirror = mirrorFactory.getMirror();
        mirror.look();
    }
}

改造后的代码,我们引入了抽象工厂角色MirrorFactory(抽象工厂可以是接口,也可以是抽象类或者具体类,我们这里使用的是接口),在抽象工厂中声明了工厂方法getMirror但并未实现工厂方法,具体产品对象的创建由其子类RoundMirrorFactorySquareMirrorFactory负责。这样在使用时,客户端针对抽象工厂MirrorFactory编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品。这样我们在新增一个产品(比如三角形镜子)时,我们可以通过新增一个具体的产品(比如TriangleMirror)和一个具体的工厂类(比如TriangleMirrorFactory),就可以达到扩展的目的。

模式应用

在我们现在的开发中,Spring 框架可以说是非常主流,并且非常优秀的框架,基本上我们的开发都离不开它的支持。它的优秀就源于它的设计思想非常的好。接下来我们就简单的探索一下工厂方法模式在 Spring--BeanFactory 中的使用。来看下如下代码

1、首先是 pom 文件:

<properties>
    <spring.version>5.1.15.RELEASE</spring.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

2、实体类Person

public class Person {
    private String name;
    private Integer age;
    // 省略getter/setter
}

3、spring 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="person" class="com.phoegel.factorymethod.analysis.Person">
        <property name="name" value="张三"/>
        <property name="age" value="18"/>
    </bean>
</beans>

4、客户端使用:

public class Main {
    public static void main(String[] args) {
        // spring 配置文件
        String config = "applicationContext.xml";
        // 可能平常我们都是这样写的
        // ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
        // 这里我为了与前面介绍的角色一致,下面的两种方式使用了同一个接口接收
        // 1、通过 ApplicationContext 容器读取配置
        BeanFactory context = new ClassPathXmlApplicationContext(config);
        Person person1 = (Person) context.getBean("person");
        System.out.println(person1);
        System.out.println("----------------------------");
        // 2、通过 BeanFactory 容器读取配置
        BeanFactory factory = new XmlBeanFactory(new ClassPathResource(config));
        Person person2 = (Person) factory.getBean("person");
        System.out.println(person2);
    }
}

以上我贴出了关键的代码,可以看到我们使用了两种读取配置文件的方式,来获取Person类的实例。这里说明一下:

  • BeanFactory是一个顶级接口,其中最主要的就是定义了获取对象的方法getBean()方法,就像是我们上面案例中的MirrorFactory,是一个抽象工厂的角色。
  • ClassPathXmlApplicationContext则实现了BeanFactory接口中定义的getBean()方法,类似于上面案例中的RoundMirrorFactory类,是一个具体工厂的角色。
  • XmlBeanFactory与上面的ClassPathXmlApplicationContext作用类似,也是一个具体工厂的角色。

​ 下面他们之间的简单 UML 类图:

bean-factory-uml

从上面的图中我们就可以清晰的看到这个与我们最上面画的工厂方法模式 UML 的工厂部分非常相似,它的核心就是定义了一个抽象工厂类,客户端通过使用这个工厂提供的具体实现来使用我们具体的产品。

总结

1.主要优点:

  • 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
  • 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
  • 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。

2.主要缺点:

  • 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  • 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

3.适用场景:

  • 客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。
  • 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/factory-method
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/2e5cc127cbd5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354