
# 创建型设计模式（上）

本教程主要介绍一系列用于创建对象的经验总结。

**知识结构：**

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927163042692.png">
<p style= "text-align:center;font-size:15px">图1 知识结构</p>
</div>

---
## 简单工厂模式

*Sunny* 软件公司欲基于 *C#* 语言<u>开发一套图表库</u>，该图表库可以为应用系统提供各种不同外观的图表，如：
- 柱状图（*histogram*）
- 饼状图（*pie*）
- 折线图（*line*）
- ...


*Sunny* 软件公司开发人员提出了一个初始设计方案，将所有图表的实现代码封装在一个`Char`类中，其框架代码如下所示：

```c
public class Chart
{
    private string _type;
    private double[][] _data;
    
    public Chart(double[][] data, string type)
    {
        _type = type.ToLower();
        _data = data;
        if (_type == "histogram")
        {
            //初始化柱状图
        }
        else if (_type == "pie")
        {
            //初始化饼状图
        }
        else if (_type=="line")
        {
            //初始化折线图
        }
    }
    
    public void Display()
    {
        if (_type == "histogram")
        {
            //显示柱状图
        }
        else if (_type == "pie")
        {
            //显示饼状图
        }
        else if (_type == "line")
        {
            //显示折线图
        }
    }
}

```

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

不难看出，`Chart`类是一个“巨大的”类，在该类的设计中存在如下几个问题：

- 在`Chart`类中包含很多`if…else…`代码块，整个类的代码冗长，代码越长，阅读难度、维护难度和测试难度也越大。而且大量条件语句的存在还将影响系统的性能，程序在执行过程中需要做大量的条件判断。
- `Chart`类的职责过重，将各种图表对象的初始化代码和显示代码集中在一个类中实现，违反了“**单一职责原则**”。
- 当需要增加新类型的图表时，必须修改`Chart`类的源码，违反了“**开闭原则**”。


为了将`Chart`类的职责分离，*Sunny* 软件公司开发人员决定对图表库进行重构，重构后的结构如下图所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926180257922.png">
<p style= "text-align:center;font-size:15px">图2 图表库结构图</p>
</div>


完整代码如下：
```c
public interface IChart
{
    void Display();
}

public class HistogramChart : IChart
{
    public void Display()
    {
        Console.WriteLine("显示柱状图");
    }

    public HistogramChart()
    {
        Console.WriteLine("创建柱状图");
    }
}

public class PieChart : IChart
{
    public void Display()
    {
        Console.WriteLine("显示饼状图");
    }
    
    public PieChart()
    {
        Console.WriteLine("创建饼状图");
    }
}

public class LineChart : IChart
{
    public void Display()
    {
        Console.WriteLine("显示折线图");
    }

    public LineChart()
    {
        Console.WriteLine("创建折线图");
    }
}

public class ChartFactory
{
    public static IChart CreateChart(string type)
    {
        IChart chart = null;
        switch (type.ToLower())
        {
            case "histogram":
                chart = new HistogramChart();
                break;
            case "pie":
                chart = new PieChart();
                break;
            case "line":
                chart = new LineChart();
                break;
        }
        return chart;
    }
}
```

客户端代码：

```c
class Program
{
    static void Main(string[] args)
    {
        IChart chart = ChartFactory.CreateChart("histogram");
        chart.Display();
    }
}
```


<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092618042159.png">
<p style= "text-align:center;font-size:15px">图3 输出结果</p>
</div>


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


**简单工厂模式** 提供了专门的工厂类来创建对象，将对象的创建和对象的使用分开，其要点在于：<u>当你需要什么，只需要传入一个正确的参数，就可以获取你所需要的对象，而无须知道其创建的细节</u>。其结构图如下所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926180643737.png">
<p style= "text-align:center;font-size:15px">图4 简单工厂模式结构图</p>
</div>



在使用 **简单工厂模式** 时，首先需要对产品类进行重构，<u>不能设计一个包罗万象的产品类，而需要根据实际情况设计一个产品等级结构</u>，将所有产品类公共的代码移至抽象产品类，并在抽象产品类中声明一些抽象方法，以供不同的具体产品类实现。

典型的抽象产品类代码如下所示：

```c
public abstract class Product
{
    public void MethodSame()
    {
        //所有产品类的公共业务方法
    }
    //声明抽象业务方法
    public abstract void MethodDiff();
}
```

在具体产品类中实现抽象产品类中声明的抽象方法，不同的具体产品类可以提供不同的实现，典型的具体产品类代码如下所示：

```c
public class ConcreteProductA : Product
{
    public override void MethodDiff()
    {
        //实现业务方法
    }
}

public class ConcreteProductB : Product
{
    public override void MethodDiff()
    {
        //实现业务方法
    }
}
```

**简单工厂模式** 的核心是工厂类，在没有工厂类之前，客户端一般会使用`new` 关键字来直接创建产品对象，而在引入工厂类之后，客户端可以通过工厂类中的静态工厂方法，根据传入的不同参数创建不同的产品对象。典型的工厂类代码如下所示：

```c
public class Factory
{
    public static Product CreateProduct(string arg)
    {
        Product product = null;
        switch (arg)
        {
            case "A":
                product = new ConcreteProductA();
                break;
            case "B":
                product = new ConcreteProductB();
                break;
        }
        return product;
    }
}
```

在客户端代码中，我们通过调用工厂类的工厂方法即可得到产品对象，典型代码如下所示：

```c
public class Client
{
    public static void Main()
    {
        //通过工厂类创建产品对象
        Product product = Factory.CreateProduct("A");
        product.MethodSame();
        product.MethodDiff();
    }
}
```

**简单工厂模式简化**

有时候，为了简化 **简单工厂模式**，我们可以将抽象产品类和工厂类合并，将静态工厂方法移至抽象产品类中。客户端可以通过产品父类的静态工厂方法，根据参数的不同创建不同类型的产品子类对象。如下图所示：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926181709821.png">
<p style= "text-align:center;font-size:15px">图5 简化的简单工厂模式</p>
</div>


---
## 工厂方法模式

*Sunny* 软件公司欲开发一个<u>系统运行日志记录器</u>（`Logger`），该记录器可以通过多种途径保证系统能够记录日志，如：
- 通过文件记录
- 通过数据库记录

用户可以通过<u>修改配置文件</u>灵活地更换日志记录方式。在设计各类日志记录器时，*Sunny* 公司的开发人员发现需要对日志记录器进行一些初始化工作，初始化参数的设置较为复杂，而且某些参数的设置有严格的先后次序，否则可能会发生记录失败。

如何封装记录器的初始化过程并且保证多种记录器切换的灵活性是 *Sunny* 公司开发人员面临的一个难题。


*Sunny* 公司开发人员最初使用 **简单工厂模式** 对日志记录器进行了设计，初始结构如下图所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926185733428.png">
<p style= "text-align:center;font-size:15px">图6 基于简单工厂模式设计的日志记录器结构图</p>
</div>



工厂类 `LoggerFactory`代码片段如下所示：



```c
public class LoggerFactory
{
    public static ILogger CreateLogger(string arg)
    {
        if (arg.ToUpper() == "DB")
        {
            //连接数据库，代码省略
            ILogger logger = new DatabaseLogger();
            //初始化数据库日志记录器，代码省略
            return logger;
        }
        if (arg.ToUpper() == "FILE")
        {
            ILogger logger = new FileLogger();
            //初始化文件日志记录器，代码省略
            return logger;
        }
        return null;
    }
}
```

通过使用 **简单工厂模式**，我们将日志记录对象的创建和使用分离，客户端只需要使用由工厂类创建的日志记录器对象即可，无须关心对象的创建过程，但是我们发现，<u>虽然简单工厂模式实现了对象的创建和使用分离，但是仍然存在如下两个问题</u>：

- 工厂类过于庞大，包含了大量的`if`语句代码，导致维护和测试难度增大，违反了“**单一职责原则**”。
- 系统扩展不灵活，如增加新类型的日志记录器，必须修改静态工厂方法的业务逻辑，违反了“**开闭原则**”。

如何解决这两个问题？

于是产生了一种 **简单工厂模式** 的改进方案 -- **工厂方法模式**。


*Sunny* 公司开发人员决定改进 **简单工厂模式** 来设计日志记录器，解决方案的基本结构如下图所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926190432206.png">
<p style= "text-align:center;font-size:15px">图7 日志记录器结构图</p>
</div>


产品等级结构的代码如下：

```c
public interface ILogger
{
    void WriteLog();
}

public class DatabaseLogger : ILogger
{
    public void WriteLog()
    {
        Console.WriteLine("数据库日志记录。");
    }
}

public class FileLogger : ILogger
{
    public void WriteLog()
    {
        Console.WriteLine("文件日志记录");
    }
}
```

工厂等级结构的代码如下：

```
public interface ILoggerFactory
{
    ILogger CreateLogger();
}

public class FileLoggerFactory : ILoggerFactory
{
    public ILogger CreateLogger()
    {
        //创建文件日志记录器对象
        ILogger logger = new FileLogger();
        //初始化文件日志记录器，代码省略
        return logger;
    }
}

public class DatabaseLoggerFactory : ILoggerFactory
{
    public ILogger CreateLogger()
    {
        //连接数据库，代码省略
        //创建数据库日志记录器对象
        ILogger logger = new DatabaseLogger();
        //初始化数据库日志记录器，代码省略
        return logger;
    }
}
```

客户端代码如下：

```c
class Program
{
    static void Main(string[] args)
    {
        ILoggerFactory factory = new FileLoggerFactory();
        ILogger logger = factory.CreateLogger();
        logger.WriteLog();
    }
}
```


为了让系统具有更好的灵活性和扩展性，*Sunny* 公司开发人员决定对日志记录器客户端代码进行重构，使得在不修改任何客户端代码的基础上可以实现更换或增加新的日志记录方式。

*Sunny* 公司开发人员创建了 *App.config* 文件用于存储日志记录器工厂类的类名。

```c
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="FactoryName" 
         value="SunnyFactoryMethod.FileLoggerFactory"/>
  </appSettings>
</configuration>
```

客户端不是通过 `new` 来创建工厂对象，而是通过读取配置文件获取该类名字符串，再使用 `C#` 反射机制，根据类名字符串来生成对象。



```c
using System.Reflection;
using System.Configuration;

class Program
{
    static void Main(string[] args)
    {
        string className = ConfigurationManager.AppSettings["FactoryName"];
        Assembly assembly = Assembly.Load("SunnyFactoryMethod");
        ILoggerFactory factory = assembly.CreateInstance(className) as ILoggerFactory;
        if (factory != null)
        {
            ILogger logger = factory.CreateLogger();
            logger.WriteLog();
        }
    }
}
```


引入配置文件 *App.config* 后，如果需要增加新的日志记录方式，只需要执行如下几个步骤：
- Step1：新的日志记录器需要实现`ILogger`接口。
- Step2：对应增加一个新的具体日志记录器工厂，需要实现`ILoggerFactory`接口，在`CreateLogger`方法中配置好初始化参数和环境变量，返回具体日志记录器对象。
- Step3：修改配置文件 *App.config*，将新增的具体日志记录器的工厂类的类名字符串替换原有工厂类类名字符串。
- Step4：编译新增的具体日志记录器类和具体日志记录器工厂类，运行客户端即可使用新的日志记录方式，而原有代码无须修改，符合“**开闭原则**”。

通过上述重构，可以使系统更加灵活，由于很多设计模式都关注系统的扩展性和灵活性，因此都定义了抽象层，在抽象层中声明业务方法，而将业务方法的实现放在实现层中。


> **工厂方法模式**（Factory Method Pattern）
> 定义一个用于创建对象的接口，让子类决定将哪一个类实例化。**工厂方法模式** 让一个类的实例化延迟到其子类。
> 
> **工厂方法模式** 又简称为 **工厂模式**（Factory Pattern），也可称为 **虚拟构造器模式**（Virtual Constructor Pattern）或 **多态工厂模式**（Polymorphic Factory Pattern）。



在 **工厂方法模式** 中，我们<u>不再提供一个统一的工厂类来创建所有的产品对象，而是针对不同的产品提供不同的工厂，提供一个与产品等级结构对应的工厂等级结构</u>。



**工厂方法模式** 结构如下图所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092619113942.png">
<p style= "text-align:center;font-size:15px">图8 工厂方法模式结构图</p>
</div>


**工厂方法模式** 结构图中包含如下几个角色：

- `Product`：抽象产品类，是 **工厂方法模式** 所创建对象的基类。
- `ConcreteProduct`：具体的产品类，继承`Product`抽象类。某种类型的具体产品由专门的具体工厂创建，具体工厂和具体产品之间一一对应。
- `Factory`：抽象工厂类，声明了工厂方法（`FactoryMethod`），用于返回一个`Product`类型的对象。抽象工厂类是 **工厂方法模式** 的核心，所有创建对象的工厂类都必须继承它。
- `ConcreteFactroy`：复写工厂方法以返回一个`ConcreteProduct`实例。


实现代码如下：

```c
public abstract class Factory
{
    public abstract Product FactoryMethod();
}

public class ConcreteFactory : Factory
{
    public override Product FactoryMethod()
    {
        return new ConcreteProduct();
    }
}
```

在实际使用时，具体工厂类在实现工厂方法时除了创建具体产品对象之外，还可以负责产品对象的初始化以及一些资源和环境配置工作，例如连接数据库、创建文件等。


在客户端代码中，只需要关心工厂类即可，不同的具体工厂可以创建不同的产品，典型的客户端代码如下：

```c
class Program
{
    static void Main(string[] args)
    {
        //可通过配置文件实现
        Factory creator = new ConcreteFactory();
        Product product = creator.FactoryMethod();
    }
}
```

**扩展工厂方法模式**

*Sunny* 公司开发人员通过进一步分析，发现可以通过多种方式来初始化日志记录器，例如：
- 可以为各种日志记录器提供默认实现；
- 还可以为数据库日志记录器提供数据库连接字符串，为文件日志记录器提供文件路径；
- 也可以将参数封装在一个`object`类型的对象中，通过`object`对象将配置参数传入工厂类。


此时，<u>可以提供一组重载的工厂方法，以不同的方式对产品对象进行创建</u>。当然，对于同一个具体工厂而言，无论使用那个工厂方法，创建的产品类型均要相同。如下图所示：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200926191556383.png">
<p style= "text-align:center;font-size:15px">图9 重载的工厂方法结构图</p>
</div>

---
## 抽象工厂模式

*Sunny* 软件公司欲<u>开发一套界面皮肤库</u>，可以对 *C#* 桌面软件进行界面美化。用户在使用时可以通过菜单来选择皮肤，不同的皮肤提供视觉效果不同的按钮、文本框、组合框等界面元素，其结构如下图所示：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092709582697.png">
<p style= "text-align:center;font-size:15px">图10 界面皮肤库结构示意图</p>
</div>


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

*Sunny* 软件公司的开发人员针对上述要求，决定使用 **工厂方法模式** 进行系统的设计，初始结构如下图所示：


<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927102331785.png">
</div>

<br>

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092710420585.png">
</div>

<br>

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927104326806.png">
<p style= "text-align:center;font-size:15px">图11 基于工厂方法模式的界面皮肤库初始结构图</p>
</div>


在图11中，提供了大量工厂来创建具体的界面组件，可以通过配置文件更换具体界面组件从而改变界面风格。但是，此设计方案存在如下问题：
- 当需要增加新的皮肤时，虽然不要修改现有代码，但是需要增加大量类，针对每一个新增加具体组件都需要增加一个具体工厂，类的个数成对增加，这无疑会导致系统越来越庞大，增加系统的维护成本和运行开销。
- 由于同一种风格的具体界面组件通常要一起显示，因此需要为每个组件都选择一个具体工厂，用户在使用时必须逐个进行设置，如果某个具体工厂选择失误会导致界面显示混乱。


改进后的设计如下图所示：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092710553586.png">
<p style= "text-align:center;font-size:15px">图12 界面皮肤库结构图</p>
</div>


产品等级结构：

```c
public interface IButton
{
    void Display();
}

public class SpringButton:IButton
{
    public void Display()
    {
        Console.WriteLine("显示浅绿色按钮。");
    }
}

public class SummerButton : IButton
{
    public void Display()
    {
        Console.WriteLine("显示浅蓝色按钮。");
    }
}

public interface ITextField
{
    void Display();
}

public class SpringTextField : ITextField
{
    public void Display()
    {
        Console.WriteLine("显示绿色边框文本框。");
    }
}

public class SummerTextField : ITextField
{
    public void Display()
    {
        Console.WriteLine("显示蓝色边框文本框。");
    }
}

public interface IComboBox
{
    void Display();
}

public class SpringComboBox : IComboBox
{
    public void Display()
    {
        Console.WriteLine("显示绿色边框组合框。");
    }
}

public class SummerComboBox : IComboBox
{
    public void Display()
    {
        Console.WriteLine("显示蓝色边框组合框。");
    }
}
```

工厂等级结构：

```c
public interface ISkinFactory
{
    IButton CreateButton();
    ITextField CreateTextField();
    IComboBox CreateComboBox();
}

public class SpringSkinFactory : ISkinFactory
{
    public IButton CreateButton()
    {
        return new SpringButton();
    }
    public ITextField CreateTextField()
    {
        return new SpringTextField();
    }
    public IComboBox CreateComboBox()
    {
        return new SpringComboBox();
    }
}

public class SummerSkinFactory : ISkinFactory
{
    public IButton CreateButton()
    {
        return new SummerButton();
    }
    public ITextField CreateTextField()
    {
        return new SummerTextField();
    }
    public IComboBox CreateComboBox()
    {
        return new SummerComboBox();
    }
}
```

配置文件如下：

```c
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="FactoryName" 
         value="SunnyAbstractFactory.SpringSkinFactory"/>
  </appSettings>
</configuration>
```

客户端代码如下：

```c
using System.Reflection;
using System.Configuration;
class Program
{
    static void Main(string[] args)
    {
        string factory = ConfigurationManager.AppSettings["FactoryName"];
        ISkinFactory skinFactory = Assembly.Load("SunnyAbstractFactory").CreateInstance(factory) as ISkinFactory;
        if (skinFactory != null)
        {
            IButton button = skinFactory.CreateButton();
            ITextField textField = skinFactory.CreateTextField();
            IComboBox comboBox = skinFactory.CreateComboBox();
            
            button.Display();
            textField.Display();
            comboBox.Display();
        }
    }
}
```


<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927110009941.png">
<p style= "text-align:center;font-size:15px">图13 程序输出结果</p>
</div>


如果需要更换皮肤，只需修改配置文件即可，在实际环境中，我们可以提供可视化界面，例如菜单或者窗口来修改配置文件，用户无须直接修改配置文件。

如果需要增加皮肤，只需要增加一族新的具体组件并对应提供一个新的具体工厂，修改配置文件即可使用新的皮肤，原有代码无须修改，符合“**开闭原则**”。

在 **工厂方法模式** 中具体工厂负责生产具体产品，每一个具体工厂对应一种具体产品。但是有时候我们希望一个工厂可以提供多个产品对象，而不是单一的产品对象，如一个电器工厂，它可以生产电视机、电冰箱等多种电器，而不是只生产某一种电器。

为了更好地理解 **抽象工厂模式**，我们先引入两个概念：
- **产品等级结构**：<u>产品等级结构即产品的继承结构</u>，如一个抽象类是电视机，其子类有海尔电视机、海信电视机，则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构。
- **产品族**：<u>指由同一个工厂生产的，位于不同产品等级结构中的一组产品</u>，如海尔电器工厂生产的海尔电视机、海尔电冰箱。海尔电视机位于电视机产品等级结构中，海尔电冰箱位于电冰箱产品等级结构中，海尔电视机、海尔电冰箱构成一个产品族。


产品等级结构与产品族如下图所示：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927110454323.png">
<p style= "text-align:center;font-size:15px">图14 产品族和产品等级结构示意图</p>
</div>

图14中一共有五个 **产品族**，分别属于三个不同的 **产品等级结构**。如果使用 **工厂方法模式**，需要提供15个具体工厂，而使用 **抽象工厂模式** 只需要提供5个具体工厂。



**抽象工厂模式** 与 **工厂方法模式** 最大的区别在于：
- **工厂方法模式**：针对的是一个产品等级结构；
- **抽象工厂模式**：针对的是一个产品族，每一个具体工厂可以生产属于一个产品族的所有产品。



> **抽象工厂**（Abstract Factory）
> 提供一个创建一系列相关或相互依赖对象的接口，而无须指定它们具体的类。



在 **抽象工厂模式** 中，每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品，这些产品构成了一个产品族，结构如下图所示：



<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/202009271106577.png">
<p style= "text-align:center;font-size:15px">图15 抽象工厂模式结构图</p>
</div>


抽象工厂可以是接口，也可以是抽象类，典型代码如下：

```c
public interface IFactory
{
    IProductA CreateProductA();
    IProductB CreateProductB();
}
```

具体工厂实现了抽象工厂。典型代码如下：
```c
public class ConcreteFacory1 : IFactory
{
    public IProductA CreateProductA()
    {
        return new ConcreteProductA1();
    }

    public IProductB CreateProductB()
    {
        return new ConcreteProductB1();
    }
}
```

与 **工厂方法模式** 一样，**抽象工厂模式** 也可为每一种产品提供一组重载的工厂方法，以不同的方式对产品对象进行创建。

**扩展案例 I**

在如今的 *Web* 应用程序中 *HTML5* 正在成为趋势，但是仍然有很多不支持 *HTML5* 的浏览器。我们需要开发一个能够让用户在浏览器中进行绘图的通用画布，如：
- 绘制直线；
- 矩形；
- ...

程序可以通过判断浏览器是否支持 *HTML5*，来采用相应的绘图方式。


解决方案如下：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927111138774.png">
<p style= "text-align:center;font-size:15px">图16 通用画布结构图</p>
</div>


产品等级结构代码：

```c
public interface IBaseLine
{
    void SetWidth(int width);
    void DrawLine(int x1, int y1, int x2, int y2);
}

public class GraphicalLine : IBaseLine
{
    public void SetWidth(int width)
    {
        Console.WriteLine("set Graphical line width.");
    }

    public void DrawLine(int x1, int y1, int x2, int y2)
    {
        Console.WriteLine("draw Graphical line.");
    }
}

public class Html5Line : IBaseLine
{
    public void SetWidth(int width)
    {
        Console.WriteLine("set html5 line width.");
    }

    public void DrawLine(int x1, int y1, int x2, int y2)
    {
        Console.WriteLine("draw html5 line.");
    }
}

public interface IBaseRectangle
{
    void SetLineWidth(int width);
    void DrawRect(int x1, int y1, int x2, int y2);
}

public class GraphicalRectangle : IBaseRectangle
{
    public void SetLineWidth(int width)
    {
        Console.WriteLine("set Graphical rect");
    }

    public void DrawRect(int x1, int y1, int x2, int y2)
    {
        Console.WriteLine("draw Graphical rect");
    }
}

public class Html5Rectangle : IBaseRectangle
{

    public void SetLineWidth(int width)
    {
        Console.WriteLine("set html5 rect");
    }

    public void DrawRect(int x1, int y1, int x2, int y2)
    {
        Console.WriteLine("draw html5 rect");
    }
}
```

工厂等级结构代码：

```c
public interface ICanvasFactory
{
    IBaseRectangle CreateRectangle();
    IBaseLine CreateLine();
}

public class GraphicalCanvas : ICanvasFactory
{
    public IBaseRectangle CreateRectangle()
    {
        return new GraphicalRectangle();
    }

    public IBaseLine CreateLine()
    {
        return new GraphicalLine();
    }
}

public class Html5Canvas : ICanvasFactory
{
    public IBaseRectangle CreateRectangle()
    {
        return new Html5Rectangle();
    }

    public IBaseLine CreateLine()
    {
        return new Html5Line();
    }
}
```

客户端代码如下：
```c
class Program
{
    private static bool isHtml5 = false;
    private static void Draw(ICanvasFactory canvas)
    {
        IBaseLine line = canvas.CreateLine();
        IBaseRectangle rect = canvas.CreateRectangle();
        line.SetWidth(1);
        line.DrawLine(1, 1, 2, 2);
        rect.SetLineWidth(1);
        rect.DrawRect(1, 1, 2, 2 );
    }
    static void Main(string[] args)
    {
        if (isHtml5 == false)
            Draw(new GraphicalCanvas());
        else
            Draw(new Html5Canvas());
    }
}
```


**扩展案例 II**

大鸟看着刚推门而入的小菜问道：“怎么这么晚才回来？”

**小菜**：“嗨，没办法呀。我本来写好了一个项目，是给一家企业做的电子商务网站，是用 *SQL Server* 作为数据库的，应该说上线后除了开始有些小问题，基本都还可以。而后，接到另外一家公司类似需求的项目，但这家公司想省钱，租用了一个空间，只能用 *Access*，不能用 *SQL Server*，于是要求我改造原来那个项目的代码。”

**大鸟**：“你的麻烦来了。”



**小菜**：“是呀，那是相当的麻烦。但开始我觉得很简单呀，因为 *SQL Server* 和 *Access* 在 *ADO. NET* 上的使用是不同的：
- 在 *SQL Server*上用的是`System.Data.SqlClient`命名空间下的`SqlConnection`、`SqlCommand`、`SqlParameter`、`SqlDataReader`、`SqlDataAdapter`；
- *Access* 则要用`System.Data.OleDb`命名空间下的相应对象；

我以为只要做一个全体替换就可以了，可替换后，错误百出。”

**大鸟**：“那是一定的，两者有不少不同的地方。你都找到了些什么问题？”



**小菜**：“实在是多呀。
- 在插入数据时 *Access* 必须要 `insert into`而 *SQL Server* 可以不用`into`的；
- *SQL Server*中的`GetDate()`在 *Access* 中没有，需要改成`Now()`；
- *SQL Server* 中有字符串函数`Substring`，而 *Access* 中根本不能用，我找了很久才知道，可以用`Mid`，这好像是 *VB* 中的函数。”

**大鸟**：“小菜还真犯了不少错呀，`insert into`这是标准语法，你干吗不加`into`，这是自找的麻烦。”

**小菜**：“这些问题也就罢了，最气人的是程序的登陆代码，老是报错，我怎么也找不到出了什么问题，搞了几个小时。最后才知道，原来 *Access* 对一些关键字，例如 *password* 是不能作为数据库的字段的，如果字段名是 *password*，*SQL Server* 中什么问题都没有，运行正常，在 *Access* 中就是报错，而且报得让人莫名其妙。”


**大鸟**：“‘关键字’应该要用‘[’ 和 ‘]’ 包起来，不然是容易出错的。”

**小菜**：“就这样，今天加班到这时候才回来。”

**大鸟**：“以后你还有的是班要加了。”

**小菜**：“为什么？”

**大鸟**：“只要网站要维护，比如修改或增加一些功能，你就得改两个项目吧，至少在数据库中做改动，相应的程序代码都要改，甚至和数据库不相干的代码也要改，你既然有两个不同的版本，两倍的工作量也是必然的。”

**小菜**：“是呀，如果哪一天要用 *Oracle* 数据库，估计我要改动的地方更多了。”



**大鸟**：“那是当然，*Oracle* 的 *SQL* 语法与 *SQL Server* 的差别更大。你的改动将是空前的。”

**小菜**：“大鸟只会夸张，哪有这么严重，大不了再加两天班就什么都搞定了。”

**大鸟**：“哼”，大鸟笑着摇摇头，很不屑一顾，“<u>菜鸟程序员碰到问题，只会用时间来摆平，所以即使整天加班，老板也不想给菜鸟加工资，原因就在于此</u>。”

**小菜**：“你什么意思嘛！那你说怎么搞定才是好呢？”小菜气道。


**大鸟**：“先写一段你原来数据访问的做法吧，以操作`User`表和`Department`表为例。”

- `User`类和`Department`类，假设只有`Id`和`Name`两个字段，其余的省略。
- `SqlServerUser`类：用于操作`User`表，假设只有“新增用户”和“得到用户”方法，其余方法以及具体的 *SQL* 语句省略。
- `SqlServerDepartment`类：用于操作`Department`表，假设只有“新增部门”和“得到部门”方法，其余方法以及具体的 *SQL* 语句省略。


<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092711381345.png">
<p style= "text-align:center;font-size:15px">图17 最初数据库访问的结构</p>
</div>


客户端代码如下：
```c
class Program
{
    static void Main(string[] args)
    {
        User user = new User
        {
            Id = 1,
            Name = "Patrick"
        };
        Department department = new Department
        {
            Id = 1,
            Name = "NCEPU"
        };
        SqlServerUser suser = new SqlServerUser();
        SqlServerDepartment sdepartment = new SqlServerDepartment();
        suser.Insert(user);
        suser.GetUser(1);
        sdepartment.Insert(department);
        sdepartment.GetDepartment(1);
    }
}
```

“这里之所以不能换数据库，原因就在于

```c
SqlServerUser suser = new SqlServerUser()；
SqlServerDepartment sdepartment = new SqlServerDepartment ();
```

使得`suser`和`sdepartment`对象被框死在 *SQL Server* 上了。如果这里是灵活的，专业的说法，是多态的，那么在执行`Insert`和`Get`方法时就不用考虑是用 *SQL Server* 还是用 *Access* 了。”



> 由于这里不满足 <b>依赖倒转原则</b>，使得数据库更换后需要修改大量的代码。这样也违背了 <b>开闭原则</b>。


<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/20200927114149697.png">
<p style= "text-align:center;font-size:15px">图18 利用抽象工厂访问数据库的结构</p>
</div>



利用 **抽象工厂模式** 来访问数据库时，若客户端程序很多地方都在使用`IUser`或`IDepartment`，而这样的设计，其实在每一个类的开始都需要声明：

```c
IFactory factory = new SqlServerFactory();
```
如果我有100个调用数据库访问的类，就要更改100次

```c
IFactory factory = new AccessFactory();
````

这样的代码才行。这不能解决更改数据库访问时，改动一处就完全更改的要求！

<u>编程是艺术，这样大批量的改动，显然是非常丑陋的做法</u>。一定有更好的办法。”


利用命名规则+反射原理，重新设计的方案如下：

<div style="text-align:center">
<img src="https://img-blog.csdnimg.cn/2020092711434663.png">
<p style= "text-align:center;font-size:15px">图19 访问数据库的改进</p>
</div>


配置文件如下：
```c
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="DB" value="SqlServer"/>
  </appSettings>
</configuration>
```


程序代码如下：

```c
namespace AbstractFactory
{
    using System.Reflection;
    using System.Configuration;
    public class DataAccess
    {
        private const string AssemblyName = "AbstractFactory ";
        private static readonly string Db = ConfigurationManager.AppSettings["DB"];
        public static IUser CreateUser()
        {
            string className = AssemblyName + "." + Db + "User";
            return Assembly.Load(AssemblyName).CreateInstance(className) as IUser;
        }
        public static IDepartment CreateDepartment()
        {
            string className = AssemblyName + "." + Db + "Department";
            return Assembly.Load(AssemblyName).CreateInstance(className) as IDepartment;
        }
    }
}
```


**“开闭原则”的倾斜性**

*Sunny* 公司使用 **抽象工厂模式** 设计了界面皮肤库，该皮肤库可以较为方便地增加新的皮肤，但是现在遇到一个非常严重的问题：
- 由于设计时考虑不全面，忘记为单选按钮（RadioButton）提供不同皮肤的风格化显示，导致无论选择哪种皮肤，单选按钮都显得那么“格格不入”。
- *Sunny* 公司的设计人员决定向系统中增加单选按钮，但是发现原有系统居然不能够在符合“**开闭原则**”的前提下增加新的组件，原因是抽象工厂`ISkinFactory`中根本没有提供创建单选按钮的方法，如果需要增加单选按钮，首先需要在`ISkinFactory`中新增创建单选按钮方法的声明，然后逐个修改具体工厂类，增加相应方法以实现在不同的皮肤中创建单选按钮，此外还需要修改客户端，否则单选按钮无法应用于现有系统。

这是 **抽象工厂模式** 最大的缺点。



在 **抽象工厂模式** 中，增加新的产品族很方便，但是增加新的产品等级结构很麻烦，**抽象工厂模式** 的这种性质称为<u>“**开闭原则**”的倾斜性</u>。

“**开闭原则**”要求系统对扩展开放，对修改封闭，通过扩展达到增强其功能的目的，对于涉及到<u>多个产品族</u>与<u>多个产品等级结构</u>的系统，其功能增强包括两个方面：
- 增加产品族：对于增加新的产品族，**抽象工厂模式** 很好地支持了“**开闭原则**”。
- 增加新的产品等级结构：对于增加新的产品等级结构，需要修改所有的工厂角色，包括抽象工厂类，在所有的工厂类中都需要增加生产新产品的方法，违背了“**开闭原则**”。



正因为 **抽象工厂模式** 存在“**开闭原则**”的倾斜性，它以一种倾斜的方式来满足“**开闭原则**”，为增加新产品族提供方便，但不能为增加新产品等级结构提供这样的方便。

因此要求设计人员在设计之初就能够全面考虑，不会在设计完成之后向系统中增加新的产品等级结构，也不会删除已有的产品等级结构，否则将会导致系统出现较大的修改，为后续维护工作带来诸多麻烦。
