一起学习设计模式--04.抽象工厂模式

2022-12-06 18:41:01 浏览数 (3)

前言

在工厂方法模式中通过引入工厂等级结构,解决了简单工厂模式中工厂类职责过重的问题。但是由于工厂方法模式中每个工厂只生产一类产品,这样可能会导致系统中存在大量的工厂类,势必会增加系统的开销。

为了解决这个问题,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。

一、界面皮肤库的初始设计

A科技公司打算开发一套界面皮肤库,可以对Winform桌面软件进行界面美化。用户可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素。结构示意图如下:

该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由的选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。

A科技公司的开发人员基于上述要求,决定使用工厂方法模式来进行系统的设计,为了保证系统的灵活性和可扩展性,提供了一系列具体工厂来创建按钮、文本框、组合框等界面元素,客户端针对抽象工厂编程。初始结构如图:

上图中提供了大量的工厂来创建具体的界面组件,可以通过配置文件更换具体界面组件从而改变风格。但是这个设计方案存在以下几个问题:

  1. 当需要增加新皮肤时,虽然不用修改现有的代码,但是需要增加大量的类。每一个新增具体组件都需要增加一个具体工厂,类的个数成对增长,这无疑会导致系统越来越庞大,从而增加了系统的维护成本和运行开销。
  2. 由于同一种风格的具体界面组件通常需要一起显示,这就需要为每个组件都选择一个具体工厂,用户在使用时必须逐个进行设置。如果某个具体工厂选择失误将会导致页面显示混乱(显示的花花绿绿),虽然可以适当增加一些约束语句,但客户端代码和配置文件都较为复杂。

怎么既能减少系统中类的个数,又能保证客户端每次始终只使用某一种风格的具体界面组件呢?很显然工厂方法模式无法解决这个问题。而抽象工厂模式就可以让这些问题迎刃而解。

二、产品等级结构和产品族

工厂方法模式中,具体工厂负责生产具体的产品,每个具体工厂都对应一个具体产品。但有时,我们希望一个工厂可以生产多个产品对象。比如,一个电器工厂,它可以生产手机、平板、耳机等多种产品。为了更好的立即抽象工厂模式,这里引入两个概念:

  1. 产品等级结构。产品等级结构就是产品的继承结构。比如一个抽象类是手机,它的子类有小米手机、华为手机、苹果手机。抽象手机和具体的品牌手机就构成了一个产品等级结构,抽象手机是父类,具体品牌的手机是其子类。【同一类型的产品】
  2. 产品族。在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。【同一品牌的不同产品就构成了一个产品族,比如小米的产品有小米手机、小米电脑、小米耳机、小米电视等,这些都是小米产品家族中的一员】

产品等级结构和产品族示意图如下:

上图一共3个产品族(小米、华为、苹果),分属于3个不同产品等级结构(手机、电脑、平板)。只要知道一个产品的产品族和产品等级结构就可以确定这个产品。(坐标的概念)

如果一个工厂生产的具体产品不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时,就可以使用抽象工厂模式。

每个具体的工厂都可以生产属于一个产品族的所有产品,比如小米的工厂可以生产小米手机、小米电脑、小米平板等产品,每个产品又位于不同的产品等级结构中。这样上图中如果使用工厂方法模式,则需要为每个产品都提供一个具体的工厂,这就要创建9个工厂类。但如果使用抽象工厂模式,我们只需要为三个产品族创建3个工厂类即可,这样就极大的减少了工厂类的个数。

三、抽象工厂模式概述

抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式的具体工厂不只是创建一种产品,而是负责创建一族产品。定义如下:

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或互相依赖对象的接口,而无须指定它们的具体的类。抽象工厂模式又称 Kit 模式,它是一种对象创建型模式。

抽象工厂模式中,每个具体的工厂都提供了多个工厂方法用于生产多种不同类型的产品,这些产品构成了一个产品族。**比如:小米工厂有多个产品线,可以生产多种类型的不同小米产品,这些不同类型的产品组成了小米产品家族。**抽象工厂模式的结构图如下:

抽象工厂模式结构图中包含4个角色:

  1. AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每个方法对应一种产品。
  2. ConcreteFactory(具体工厂):他实现了抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每个产品都位于某个产品等级结构中。
  3. AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
  4. ConcreteProduct(具体产品):它定义具体工厂生成的具体产品对象,实现在抽象产品接口中声明的业务方法。

在抽象工厂中声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或具体类。典型代码如下:

代码语言:javascript复制
public interface AbstractFactory{
 AbstractProductA CreateProductA();//工厂方法一
    AbStractProductB CreateProductB();//工厂方法二
}

具体工厂则实现了抽象工厂,每个具体的工厂方法可以返回一个特定的产品对象,且同一个具体工厂所创建的产品对象构成了一个产品族。

代码语言:javascript复制
public class ConcreteFactory1 : AbstractFactory{
    //工厂方法一
 public AbstractProductA CreateProductA(){
     return new ConcreteProductA1();
    }
    
    //工厂方法二
    public AbstractProductB CreateProductB(){
     return new ConcreteProductB1();
    }
}

四、完整解决方案

1、结构

开发人员使用抽象工厂模式来重构界面皮肤库的设计,结构图如下:

  • ISkinFactory 接口充当抽象工厂
  • SpringSkinFactory 和 SummerSkinFactory 充当具体工厂
  • Button、TextField、ComboBox 充当抽象产品。
  • SpringButton、SummerButton、SpringTextField、SummerTextField、SpringComboBox、SummerComboBox 充当具体产品。
2、完整代码
代码语言:javascript复制
    /// <summary>
    /// 按钮接口:抽象产品
    /// </summary>
    public interface Button
    {
        void Display();
    }

    /// <summary>
    /// Spring按钮类:具体产品
    /// </summary>
    public class SpringButton : Button
    {
        public void Display()
        {
            Console.WriteLine("显示Spring风格按钮");
        }
    }
    
    /// <summary>
    /// Summer按钮类:具体产品
    /// </summary>
    public class SummerButton : Button
    {
        public void Display()
        {
            Console.WriteLine("显示Summer风格按钮");
        }
    }
    
    /// <summary>
    /// 文本框接口:抽象产品
    /// </summary>
    public interface TextField
    {
        void Display();
    }

    /// <summary>
    /// Spring 文本框类:具体产品
    /// </summary>
    public class SpringTextField : TextField
    {
        public void Display()
        {
            Console.WriteLine("显示Spring风格文本框");
        }
    }
    
    /// <summary>
    /// Summer 文本框类:具体产品
    /// </summary>
    public class SummerTextField : TextField
    {
        public void Display()
        {
            Console.WriteLine("显示Summer风格文本框");
        }
    }
    
    /// <summary>
    /// 组合框接口:抽象产品
    /// </summary>
    public interface ComboBox
    {
        void Display();
    }

    /// <summary>
    /// Spring组合框类:具体产品
    /// </summary>
    public class SpringComboBox : ComboBox
    {
        public void Display()
        {
            Console.WriteLine("显示Spring风格组合框");
        }
    }
    
    /// <summary>
    /// Summer 组合框类:具体产品
    /// </summary>
    public class SummerComboBox : ComboBox
    {
        public void Display()
        {
            Console.WriteLine("显示Summer风格组合框");
        }
    }
    
    /// <summary>
    /// 界面皮肤工厂接口:抽象工厂
    /// </summary>
    public interface ISkinFactory
    {
        Button CreateButton();
        TextField CreateTextField();
        ComboBox CreateComboBox();
    }

    /// <summary>
    /// Spring 皮肤工厂:具体工厂
    /// </summary>
    public class SpringSkinFactory : ISkinFactory
    {
        public Button CreateButton()
        {
            return new SpringButton();
        }

        public TextField CreateTextField()
        {
            return new SpringTextField();
        }

        public ComboBox CreateComboBox()
        {
            return new SpringComboBox();
        }
    }
    
    /// <summary>
    /// Summer 皮肤工厂:具体皮肤工厂
    /// </summary>
    public class SummerSkinFactory : ISkinFactory
    {
        public Button CreateButton()
        {
            return new SummerButton();
        }

        public TextField CreateTextField()
        {
            return new SummerTextField();
        }

        public ComboBox CreateComboBox()
        {
            return new SummerComboBox();
        }
    }

为了让系统更具备良好的灵活性和可扩展性,这里依旧将具体工厂添加到配置文件中,并添加配置文件的帮助类,具体代码如下:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <!--value为具体工厂类的完全限定名(命名空间 类名)-->
    <add key="SkinFactory" value="LXP.DesignPattern.AbstractFactory.SpringSkinFactory"/>
  </appSettings>
</configuration>

帮助类代码:

代码语言:javascript复制
    public class AppConfigHelper
    {
        /// <summary>
        /// 获取具体皮肤工厂方法
        /// </summary>
        /// <returns></returns>
        public static object GetSkinFactory()
        {
            try
            {
                var skinFactoryName = ConfigurationManager.AppSettings["SkinFactory"];
                var type = Type.GetType(skinFactoryName);

                return type == null ? null : Activator.CreateInstance(type);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            return null;
        }
    }

客户端测试代码:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            var skinFactory = (ISkinFactory) AppConfigHelper.GetSkinFactory();
            var button = skinFactory.CreateButton();
            var textField = skinFactory.CreateTextField();
            var comboBox = skinFactory.CreateComboBox();

            button.Display();
            textField.Display();
            comboBox.Display();
        }
    }

输出结果:

上述实现,如果以后要更换皮肤,只需要修改配置文件即可。事实上,在实际工作中,通常会提供一个可视化界面来管理配置,这样用户如果要更换皮肤只需要在配置界面配置即可,无须手动修改配置文件,更加的方便。如果要增加新的皮肤,只需要添加一族新的具体的组件并对应提供一个具体的工厂,修改配置文件即可,无须修改代码,符合开闭原则

五、开闭原则的倾斜行

上述实现可以较为方便的增加新的皮肤,但是也存在一个非常严重的问题:由于设计时只针对Button、TextField、ComboBox提供了不同风格化的显示,忘记为单选按钮(RadioButton)提供,那么无论选择那种皮肤,单选按钮都不会变化,在界面上就会显得“格格不入”。开发人员决定往系统中增加单选按钮,但是发现原有的系统不能在符合开闭原则的情况下添加新的组件,原因是抽象工厂 ISkinFactory 中没有提供创建单选按钮的方法,如果要增加单选按钮,首先要修改抽象工厂接口,添加创建单选按钮的方法,然后在不同风格的具体工厂类中实现创建单选按钮的方法,然后还要修改客户端等一系列操作。

这也是抽象工厂模式最大的缺点。在抽象工厂模式中增加新的产品族很方便,但是增加新的产品等级结构就很麻烦,抽象工厂模式的这种性质称为开闭原则的倾斜性(倾向于产品族的扩展)。开闭原则要求系统对扩展开放,对修改关闭,通过扩展达到增强其功能的目的,对于涉及多个产品族和多个产品等级结构的系统,其功能增强包括两个方面:

  1. 增加产品族,对于增加新的产品族,抽象工厂模式很好的支持了开闭原则,只需要增加具体产品并对应增加一个具体工厂,对已有的代码无需做任何的修改。
  2. 增加产品等级结构。如果需要增加新的产品等级结构,就需要修改所有的工厂角色(包括抽象工厂类)和客户端代码,在所有的工厂类中都要增加生产新产品的方法,违背了开闭原则。

正是因为抽象工厂模式存在的开闭原则的倾斜性,它以一种倾斜的方式来满足开闭原则,为增加新的产品族提供方便,无法为增加新的产品等级结构提供方便。这种就要求设计人员在设计之初就能够全面考虑,不会在设计完成以后向系统中添加新的产品等级结构,也不会删除系统中已有的产品等级结构,不然系统将会出现大范围的修改,为后续的维护工作带来诸多麻烦。

六、抽象工厂模式总结

抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了较为强大的工厂类并且具备更好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和 API 类库的设计中。

1、主要优点:
  1. 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
  3. 增加新的产品族很方便,无需修改原有系统,符合开闭原则。
2、主要缺点:

抽象工厂模式的主要缺点就是:增加新的产品等级结构很麻烦,需要对原有的系统进行大范围的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了开闭原则。

3、适用场景
  1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无需关系对象的创建过程,将对象的创建和使用解耦。
  2. 系统中有多个产品族,而且每次只使用其中某一个产品族。可以通过配置文件等方式来使得用户可以动态的改变产品族,也可以很方便的增加新的产品族。
  3. 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是任何没有关系的对象,但它们都具有一些共同的约束。即共同属于谁。

示例代码:

https://github.com/crazyliuxp/DesignPattern.Simples.CSharp

参考资料:

0 人点赞