一起学习设计模式--02.简单工厂模式

2022-12-06 18:38:33 浏览数 (3)

工厂模式是最常用的一类创建型设计模式。我们所说的工厂模式是指工厂方法模式,它也是使用频率最高的工厂模式。

简单工厂模式是工厂方法模式的小弟,它不属于GoF 23种设计模式,但是在软件开发中应用也颇为频繁,通常将它作为学习其它工厂模式的入门。

一、图表库的设计

A科技公司计划使用C#语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,比如柱状图、饼状图、折线图等。A科技公司的图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以比较方便的对图表库进行扩展,以便能够在将来增加一些新类型的图表。A科技公司的研发人员提出了一个初始化设计方案,将所有图表的实现代码封装在一个 Chart 类中,代码如下:

代码语言:javascript复制
    public class Chart
    {
        private string type; //图表类型

        public Chart(object[][] data, string type)
        {
            this.type = type;

            if (type.Equals("histogram", StringComparison.OrdinalIgnoreCase))
            {
                //初始化柱状图
            }
            else if (type.Equals("pie", StringComparison.OrdinalIgnoreCase))
            {
                //初始化饼状图
            }
            else if (type.Equals("line", StringComparison.OrdinalIgnoreCase))
            {
                //初始化折线图
            }
        }

        public void Display()
        {
            if (this.type.Equals("histogram", StringComparison.OrdinalIgnoreCase))
            {
                //显示柱状图
            }
            else if (this.type.Equals("pie", StringComparison.OrdinalIgnoreCase))
            {
                //显示饼状图
            }
            else if (this.type.Equals("line", StringComparison.OrdinalIgnoreCase))
            {
                //显示折线图
            }
        }
    }

客户端代码通过调用 Chart 类的构造函数来创建图表对象,根据参数 type 的不同可以得到不同类型的图表,然后再调用 Display() 方法来显示相应的图表。

但是 Chart 类是一个巨大的类,在该类的设计中存在以下几个问题:

  1. 类中包含大量的 “if - else” 代码块,整个类的代码相当冗长,代码越长,代码的可读性、维护难度、测试难度也越大。而且大量的判断会影响性能,无论是什么类型的图表,类的内部都需要做大量的判断。
  2. Chart 类的职责过重,违反了单一职责原则。它将图表的创建和显示都放在一个类中,不利于类的重用和维护。而且类的构造函数中有大量的判断,并且对象初始化的操作也都写在构造函数中,降低了创建效率。
  3. 如果需要增加新的图表,就需要修改 Chart 类,违反了开闭原则。
  4. 客户端只能通过 new 来实例化 Chart 对象,这样的话 Chart 对象和客户端的耦合度较高,对象的创建和使用无法分离。
  5. 客户端在创建 Chart 对象之前可能还需要进行大量的初始化设计,比如柱状图的颜色、高度等。如果在 Chart 类的构造函数中没有提供一个默认设置,那就只能由客户端来完成初始设置,那么这些初始设置在每次创建 Chart 对象的时候都会出现,导致了代码的重复。
1.简单工厂模式的流程

首先简单工厂模式不属于GoF 23种经典设计模式,但通常将它作为学习其它工厂模式的基础,它的设计思想很简单,基本流程如下:

  1. 将需要创建各种不同对象的相关代码封装到不同的类中,这些类称为具体的产品类。
  2. 将他们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体的产品类都是这个抽象产品类的子类。
  3. 提供一个工厂类用于创建各种产品,在工厂类中提取一个创建产品的工厂方法,该方法可以根据传入的参数不同创建不同的具体产品对象。
  4. 客户端只需要调用工厂类的工厂方法并传入相应的参数即可得到一个具体的产品对象。
2.简单工厂模式的定义

简单工厂模式定义如下:

简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。

3.简单工厂模式的要点

简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无需知道其创建细节。

4.简单工厂模式的结构

在简单工厂模式结构图中包含3个角色:

  1. Factory(工厂角色):即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。在工厂类中提供了静态的工厂方法FactoryMethod(),返回抽象产品类型 Product。
  2. Product(抽象产品角色):它是工厂类创建所有对象的父类,封装了各种产品对象的公共方法。抽象产品的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
  3. ConceteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每个具体产品角色都继承了抽象产品角色,需要实现抽象产品中声明的抽象方法。

在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须使用 new 关键字来创建对象,它是工厂模式中最简单的一员。

在使用简单工厂模式时,首先需要对产品类进行重构,将所有产品类中公共的代码转移到抽象产品类中,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现。

三、完整解决方案

为了将 Chart 类的职责分离,同时将 Chart 对象的创建和使用分离,A科技公司开发人员决定使用简单工厂模式对图表库进行重构,重构后的图表库结构如下:

IChart 接口充当抽象产品类,其子类HistogramChart、LineChart、PieChart充当具体产品类,ChartFactory充当工厂类。完整代码如下:

代码语言:javascript复制
    /// <summary>
    /// 抽象图表接口:抽象产品类
    /// </summary>
    public interface IChart
    {
        void Display();
    }


    /// <summary>
    /// 柱状图类:具体产品类
    /// </summary>
    public class HistogramChart : IChart
    {
        public HistogramChart()
        {
            Console.WriteLine("创建柱状图!");
        }

        public void Display()
        {
            Console.WriteLine("显示柱状图!");
        }
    }
    

    /// <summary>
    /// 折线图类:具体产品类
    /// </summary>
    public class LineChart : IChart
    {
        public LineChart()
        {
            Console.WriteLine("创建折线图!");
        }

        public void Display()
        {
            Console.WriteLine("显示折线图!");
        }
    }
    

    /// <summary>
    /// 饼状图类:具体产品类
    /// </summary>
    public class PieChart : IChart
    {
        public PieChart()
        {
            Console.WriteLine("创建饼状图!");
        }

        public void Display()
        {
            Console.WriteLine("显示饼状图!");
        }
    }
    

    /// <summary>
    /// 图表工厂类:工厂类
    /// </summary>
    public class ChartFactory
    {
        /// <summary>
        /// 静态工厂方法
        /// </summary>
        /// <param name="type">图表类型</param>
        /// <returns></returns>
        public static IChart GetChart(string type)
        {
            IChart chart = null;
            if (type.Equals("histogram", StringComparison.OrdinalIgnoreCase))
            {
                chart = new HistogramChart();
                Console.WriteLine("初始化设置柱状图");
            }
            else if (type.Equals("line", StringComparison.OrdinalIgnoreCase))
            {
                chart = new LineChart();
                Console.WriteLine("初始化设置折线图");
            }
            else if (type.Equals("pie", StringComparison.OrdinalIgnoreCase))
            {
                chart = new PieChart();
                Console.WriteLine("初始化设置饼状图");
            }

            return chart;
        }
    }

编写客户端测试代码:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            IChart chart = ChartFactory.GetChart("line");
            chart.Display();
        }
    }

编译运行输出,结果如下:

在客户端的测试代码中,我们可以看到使用工厂类的静态工厂方法来创建具体产品对象。如果后期需要更换产品,只需要修改静态工厂方法的入参即可。例如,我们需要将折线图改成饼状图,只需要将代码:IChart chart = ChartFactory.GetChart("line"); 中的 line 改为 pie 即可。

四、方案的改进

一切进行的很顺利,但是A科技公司的开发人员发现,在创建具体的 Chat 对象时,每次更换具体的 Chart 对象类型都需要修改静态工厂方法中的入参,然后还需要重新编译客户端代码,这对客户端来说**违反了开闭原则。**那有没有一种方法可以在不修改代码的前提下就能更换具体的产品对象呢?当然有!在C#中我们可以将静态工厂方法中的入参配置到config文件中,这样每次要替换具体的产品类,我们只需要修改配置文件即可。比如:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="chartType" value="line"/>
  </appSettings>
</configuration>

客户端代码如下:

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            //获取配置文件中的chartType的值
            string type = System.Configuration.ConfigurationManager.AppSettings["chartType"];
            IChart chart = ChartFactory.GetChart(type);
            chart.Display();
        }
    }

从上边的代码可以看出,客户端代码中不包含任何与具体图表相关的信息。如果要更换具体图表对象,只需要修改config配置文件中的配置即可,无需修改任何代码,符合开闭原则。

补充:

在ASP.NET Core中配置文件通常使用的是json文件,比如appsettings.json,另外还有很多种配置文件类型,比如:ini、环境变量、用户机密等

五、创建对象与使用对象

如果一个类即要负责创建引用的对象,又要使用使用引用对象的方法,这样就会使创建对象和使用对象的职责耦合在一起,这样会导致一个很严重的问题,那就是违反了开闭原则。那怎么解决这个问题呢?

最常用的一种方法就是将创建对象的职责移除,并交由其它类来负责创建。由谁创建呢?答案是:工厂类。通过引入工厂类,客户类不涉及对象的创建,对象的创建者也不会涉及对象的使用。

所有的工厂模式都强调一点:两个类A和B之间的关系应该仅仅是A创建B或者是A使用B,而不能两种关系都有。将对象的创建和使用分离,也使得系统更加符合单一职责原则,有利于对功能的复用和系统的维护。

将创建对象和使用对象分离还有一个好处:防止用来实例化一个类的数据和代码在多个类中到处都是。

也不是说将每一个类都配置一个工厂类,而是要具体问题具体分析,对于那些产品类很简单,而且也不存在太多变数,构造过程也很简单的类,就无需为其提供工厂类,直接在使用的时候实例化即可。

六、简单工厂模式总结

简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离开来。

1.主要优点
  1. 简单工厂模式实现了对象创建和使用的分离。对于客户端来说免除了直接创建产品对象的职责,而仅仅负责“消费”产品即可。
  2. 客户端无需知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。
  3. 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
2.主要缺点
  1. 工厂类集中了所有具体产品类的创建逻辑,职责过重。一旦工厂类不能正常工作,整个系统都会受到影响。
  2. 简单工厂模式会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
  3. 系统扩展困难。如果要添加新产品就需要修改工厂逻辑,如果产品类较多时,有可能会造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  4. 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
3.适用场景
  1. 工厂类负责创建的对象比较少。由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

示例代码:

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

参考资料:

0 人点赞