19

设计模式——抽象工厂模式

 4 years ago
source link: http://www.cnblogs.com/shanzhiming/p/12815420.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

目录

  • 设计模式——抽象工厂模式
    • 2. 示例1-使用工厂模式实现对不同数据库的操作
    • 3. 示例2-多数据库且多表操作
    • 4. 重构示例2-使用简单工厂改进抽象工厂
    • 5. 重构示例2-反射+简单工厂
    • 6. 重构示例2-反射+配置文件+简单工厂

shanzm-2020年5月1日 23:20:41

1. 模式简介

抽象工厂模式(Abstract Factory Pattern):为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。

产品族(产品系列):同一个具体工厂创建的不同等级的产品称为同一产品族,或是称为同一产品系列。

注意:同一个产品族的产品是继承于不同的产品抽象类

在抽象工厂模式中有产品族的概念,而在工厂方法模式中是没有这个概念的,因为工厂方法模式中一个具体工厂只创建一种具体产品。

产品等级:又称为产品系列,指的是继承与同一个抽象产品类的所有具体产品称之为同一个产品等级

为了方便理解产品族和产品等级,举一个小栗子:

MRJRNbz.png!web

抽象工厂模式主要类:

  • AbstractProductA抽象产品A类(或是接口),派生出所有的具体产品类ConcreteProductA1、ConcreteProductA2 ……

  • AbstractProductB抽象产品B类(或是接口),派生出所有的具体产品类ConcreteProductB1、ConcreteProductB2 ……

  • AbstractFactory 抽象工厂接口,所有的具体工厂类都是实现该接口

  • ConcreteFactory1 具体工厂1,实现了IFactory接口,创建具体的产品对象ConcreteProductA1和ConcreteProductB1

  • ConcreteFactory2 具体工厂2,实现了IFactory接口,创建具体的产品对象ConcreteProductA2和ConcreteProductB2

注意: 两个抽象产品类可以有关系,比如:共同继承或实现一个抽象类或接口

抽象工厂模式的UML:

NN3MZfy.png!web

注:原图片来自《设计模式实训教程-第二版》

仔细查看UML,可以发现:当系统中只存在一个产品等级时,抽象工厂模式将退化到工厂方法模式。

2. 示例1-使用工厂模式实现对不同数据库的操作

2.1 背景说明

在实际开发中,有可能会出现更换不同的数据库,或是一个项目就使用多个类型的数据库。

所以为便于更换不同的数据库,我们使用工厂模式,定义不同的具体工厂创建不同数据库的操作类

示例来源《大话设计模式》,假设某个项目同时具体MSSQL数据库和Oracle数据库,两个数据库只是类型不同,其中的表以及表的字段都是一样的。

我们需要对两个数据库中的User表进行操作。

按照工厂模式的设计思路,依次实现以下接口和类:

抽象产品: IUserService -声明查询和添加User表数据的方法

具体产品: MSSQLUserServiceOracleUserService -分别针对MSSQ和Oracle数据库的实现IUserService接口

抽象工厂: IDatabaseFactory -声明创建IUserService对象的方法

具体工厂: MSSQLFactoryOracleFactory -实现IDatabaseFactory接口,分别创建MSSQLUserService对象和OracleUserService对象

2.2 代码实现

①创建User类

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

②创建产品总接口IUserService和具体产品MSSQLUserService、OracleUserService

//抽象产品
public interface IUserService
{
    void Insert(User user);
    User GetUser(int id);
}

//具体产品:模拟对MSSQL数据库中的User表的查询和添加操作
public class MSSQLUserService : IUserService
{
    public void Insert(User user)
    {
        Console.WriteLine($"MSSQL数据库User表中中-添加新的用户,Id:{user.Id },Name{user.Name}");
    }
    public User GetUser(int id)
    {
        Console.WriteLine($"MSSQL数据库User表中-查询到用户,Id:{id}");
        return null;
    }
}

//具体产品:模拟对Oracle数据库中的User表的查询和添加操作
public class OracleUserService : IUserService
{
    public void Insert(User user)
    {
        Console.WriteLine($"Oracle数据库User表中-添加新的用户,Id:{user.Id },Name{user.Name}");
    }
    public User GetUser(int id)
    {
        Console.WriteLine($"Oracle数据库User表中-查询到用户,Id:{id}");
        return null;
    }
}

③创建抽象工厂IDatabaseFactory和具体工厂MSSQLFactory、OracelFactory

//抽象工厂
public interface IDatabaseFactory
{
    IUserService CreateUserService();
}

//具体工厂:创建MSSQLUserService对象
public class MSSQLFactory : IDatabaseFactory
{
    public IUserService CreateUserService()
    {
        return new MSSQLUserService();
    }
}

//具体工厂:创建OracleUserService对象
public class OracleFactory : IDatabaseFactory
{
    public IUserService CreateUserService()
    {
        return new OracleUserService();
    }
}

④客户端调用

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };

    IDatabaseFactory msSQlFactory = new MSSQLFactory();
    IDatabaseFactory oracleFactory = new OracleFactory();

    //若是对MSSQL数据库中的User表操作
    IUserService msUserService = msSQlFactory.CreateUserService();
    msUserService.Insert(user);
    msUserService.GetUser(00001);//print:查询到用户,Id:00001

    //若是对Oracle数据库中User表操作
    IUserService oracleUserService = oracleFactory.CreateUserService();
    oracleUserService.Insert(user);
    oracleUserService.GetUser(00001);//print:查询到用户,Id:00001
}

2.3 程序类图

n2uqumn.png!web

3. 示例2-多数据库且多表操作

3.1 背景说明

在示例1中,有两个不同的数据库,每个数据库中都有一张User表,我们实现了对每个数据库的User表查询和添加数据

我们使用了工厂方法模式,即有一个抽象产品接口( IUserService ),有2个具体产品类( MSSQLUserServiceOracleUserService )实现该接口。

有一个抽象工厂接口( IDatabaseFactory ),有两个具体产品工厂类( MSSQLFactoryOracleFactory )实现该接口。

而现在,若是在两个数据库中还有一个部门表Department表,需要对Department表操作。

则需要按照以下修改和添加代码:

  • 添加一个抽象产品接口( IDepService ),和实现该接口的有两个具体产品类( MSSQLDepServiceOracleDepService )。

  • 在原有的抽象工厂接口和具体工厂类中添加创建 MSSQLDepService 对象和 OracleDepService 对象的方法。 注意工厂方法是在原有的工厂中进行扩展。

3.2 代码实现

①在示例1的基础上,添加一个Department类

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
}

②添加一个新的抽象产品接口( IDepService ),和实现该接口的有两个具体产品( MSSQLDepServiceOracleDepService

//抽象产品
public interface IDepartmentService
{
    void Insert(Department dep);
    Department GetDepartment(int id);
}

//具体产品:模拟对MSSQL数据库中的Department表的查询和添加操作
public class MSSQLDepService : IDepartmentService
{
    public Department GetDepartment(int id)
    {
        Console.WriteLine($"MSSQL数据库的Department表中-查询到部门,Id:{id}");
        return null;
    }
    public void Insert(Department dep)
    {
        Console.WriteLine($"MSSQL数据库的Department表中-添加新的部门,Id:{dep.Id }Name:{dep.Name}");
    }
}

//具体产品:模拟对Oracle数据库中的Department表的查询和添加操作
class OracleDepService : IDepartmentService
{
    public Department GetDepartment(int id)
    {
        Console.WriteLine($"Oracle数据库的Department表中-查询到部门,Id:{id}");
        return null;
    }
    public void Insert(Department dep)
    {
        Console.WriteLine($"Oracle数据库的Department表中-添加新的部门,Id:{dep.Id }Name:{dep.Name}");
    }
}

③在示例1的基础上,在原有的抽象工厂接口和具体工厂类中添加创建 MSSQLDepService 对象和 OracleDepService 对象的方法

public interface IDatabaseFactory
{
    IUserService CreateUserService();
    IDepartmentService CreateDepService();//在接口中添加新的方法
}

public class MSSQLFactory : IDatabaseFactory
{
    public IDepartmentService CreateDepService()
    {
        return new MSSQLDepService();
    }
    public IUserService CreateUserService()
    {
        return new MSSQLUserService();
    }
}

public class OracleFactory : IDatabaseFactory
{
    public IDepartmentService CreateDepService()
    {
        return new OracleDepService();
    }
    public IUserService CreateUserService()
    {
        return new OracleUserService();
    }
}

④在客户端调用

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };
   
    IDatabaseFactory DatabaseFactory = new MSSQLFactory();

    //对MSSQL数据库中的User表操作
    IUserService UserService = DatabaseFactory.CreateUserService();
    UserService.Insert(user);
    UserService.GetUser(00001);

    //对MSSQL数据库中的Del表操作
    IDepartmentService DepService = DatabaseFactory.CreateDepService();
    DepService.Insert(dep);
    DepService.GetDepartment(1000);
    Console.ReadKey();
}

运行结果:

Ef2UfaU.png!web

假如需要改换为Oracle数据库,则你只需要将创建具体工厂对象new MSSQLFactory()修改为new OracleFactory(),其他的代码无需修改

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };
   
    IDatabaseFactory DatabaseFactory = new MSSQLFactory();

    //对MSSQL数据库中的User表操作
    IUserService UserService = DatabaseFactory.CreateUserService();
    UserService.Insert(user);
    UserService.GetUser(00001);

    //对MSSQL数据库中的Del表操作
    IDepartmentService DepService = DatabaseFactory.CreateDepService();
    DepService.Insert(dep);
    DepService.GetDepartment(1000);
    Console.ReadKey();
}

运行结果:

YR7jeaJ.png!web

3.3 程序类图

3MbqQbM.png!web

【说明】:

  • MSSQLUserServiceMSSQLDepService 是由同一个具体工厂 MSSQLFactory 创建的,即二者属于同一产品族。

  • OracleUserServiceOracleDepService 是由同一个具体工厂 OracleFactory 创建的,即二者属于同一产品族。

  • 而我们需要切换数据库的时候(即切换产品族),只需要修改创建具体工厂对象: MSSQLFactory 对象或 OracleFactory 对象。这就是抽象工厂模式的最大优点!

4. 重构示例2-使用简单工厂改进抽象工厂

上述示例项目中,假如再添加一个新的表Student,添加对该表的操作类,则先需要定义一个抽象接口 IStudentService 接口,派生针对不同数据库操作的两个类: MSSQLStudentServiceOracleStudentService ,这之后再在 IDatabaseFactory 接口中添加一个 CreateStudentService() 方法,接着在两个具体的工厂类中实现该接口。

我们可以使用简单工厂模式实现上述的示例2中的项目:

完整演示Demo代码 下载

①以下接口和类和示例2中一样

抽象产品A: IUserService ,派生出具体产品: MSSQLUserServiceOracleUserService

抽象产品B: IDepService ,派生出具体产品: MSSQLDepServiceOracleDepService

②定义简单工厂类:

因为这里有两个抽象产品,所以和之前的一般的简单工厂不同的地方就是要建立两个工厂方法:

public class DatabaseFactory
{
    private static readonly string db = "MSSQL";//若是需要更换数据库则将字符串改"Oracle"

    //针对抽象产品IUserService的工厂方法
    public static IUserService CreateUserService()
    {
        IUserService userService = null;
        switch (db)
        {
            case "MSSQL":
                userService = new MSSQLUserService();
                break;
            case "Oracle":
                userService = new OracleUserService();
                break;
        }
        return userService;
    }

    //针对抽象产品IDepService的工厂方法
    public static IDepartmentService CreateDeprService()
    {
        IDepartmentService depService = null;
        switch (db)
        {
            case "MSSQL":
                depService = new MSSQLDepService();
                break;
            case "Oracle":
                depService = new OracleDepService();
                break;
        }
        return depService;
    }
}

如果需要更换数据库,则只需要简单的将 private static readonly string db 该字段赋值改为 "Oracle"

③客户端调用

static void Main(string[] args)
{
    User user = new User() { Id = 0001, Name = "shanzm" };
    Department dep = new Department() { Id = 1000, Name = "Development" };

    IUserService userService = DatabaseFactory.CreateUserService();
    userService.Insert(user);

    IDepartmentService depService = DatabaseFactory.CreateDeprService();
    depService.Insert(dep);

    Console.ReadKey();
}

运行结果:

M7VZbm3.png!web

【说明】

  • 在这里使用简单该厂模式对比使用抽象工厂模式,简化了许多的类和接口,所有的修改都可以在工厂类中进行修改添加

  • 同样也实现了客户端和创建实例过程的分离

④程序类图

NZjAzaz.png!web

对比抽象工厂模式,只是将所有的抽象工厂和具体工厂全部简化为一个工厂类,该工厂类中有两个工厂方法

5. 重构示例2-反射+简单工厂

通过使用反射我们可以免去在工厂方法中使用switch语句,

通过反射获取需要创建实例的对象名,然后创建该类的实例对象( 本质上就是依赖注入

看上去好像并没有变得更加方便,但其实是如有产品族比较多的情况下,switch语句的case语句也相应的变多

所以使用反射,可以省略使用switch还是不错的。

代码实现,在 4. 重构示例2-使用简单工厂改进抽象工厂 的基础上,修改工厂类:

完整演示Demo代码 下载

public class DatabaseFactory
{
    //具体产品所在的程序集名称
    private static readonly string AssemblyName = "04抽象工厂模式-多数据库连接-反射+简单工厂";
    private static readonly string db = "MSSQL";//若是需要更换数据库则将字符串改为"Oracle"

    public static IUserService CreateUserService()
    {
        //具体产品的完全限定名
        //注意因为我们的这个项目中有特殊字符,所以程序集的名字和项目名不一致,查看程序集名和命名空间名可以右键项目属性
        string className = "_04抽象工厂模式_多数据库连接_反射_简单工厂" + "." + db + "UserService";
        return (IUserService)Assembly.Load(AssemblyName).CreateInstance(className);
      
    }
    public static IDepartmentService CreateDeprService()
    {
        string className = "_04抽象工厂模式_多数据库连接_反射_简单工厂" + "." + db + "DepService";
        return (IDepartmentService)Assembly.Load(AssemblyName).CreateInstance(className);
    }
}

6. 重构示例2-反射+配置文件+简单工厂

5. 重构示例2-反射+简单工厂 若是需更换数据,还是需要修改 private static readonly string db = "MSSQL" 字段

即任然需要修改代码后在重新编译,我们可以将需要修改的字段值放在配置文件中

完整演示Demo代码 下载

修改 5. 重构示例2-反射+简单工厂 如下:

①首先本项目添加引用"System.Configuration"

②在配置文件App.Config中添加如下配置

<configuration>
  <appSettings>
    <add key="db" value="MSSQL"/><!--更换数据则<add key="db" value="Oracle"/>-->
  </appSettings>
</configuration>

③修改工厂类中的db字段

private static readonly string db = ConfigurationManager.AppSettings["db"];//db字段的值从配置文件中读取

【说明】:其实在所有在用到简单工厂的地方,都可以考虑使用反射技术去除去switch或if,解除分支判断带来的耦合

7. 总结分析

整个示例项目,由工厂方法模式-->抽象工厂模式-->简单工厂模式,你可以仔细的查看三个实现方式的程序类图,值得琢磨!

7.1 优点

从UML类图中就可以发现:

  1. 便于交换产品系列,每一个具体的工厂对象都只是在客户端中初始化时候实现一次,所以改变一个具体的工厂对象是十分简单的,所以更换一个产品序列也就变得简单了。

    简单的说,就是因为具体产品都是由具体的工厂创建的,所以在更换产品族的时候只需要简单的修改具体工厂对象即可

  2. 创建具体产品对象的过程和客户端分离(可以从UML中明显看出),客户端通过操作抽象产品接口实现操作具体产品实例,具体产品的类名不会出现在客户端中。

7.2 缺点

  1. 添加新的产品族是非常简单的,首先在相应的产品等级结构中添加新的具体产品,然后添加一个具体工厂即可。

  2. 添加新的产品等级是非常麻烦的,首先要添加抽象产品接口,接着派生所有的具体产品,还要在抽象工厂中添加方法,以及所有的具体工厂中实现该方法。

对比以上就明白:

抽象工厂模式的扩展有一定的“开闭原则”倾斜性:

当增加一个新的产品族时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则。

当增加一个新的产品等级时,则所有的工厂类都需要进行修改,不满足开闭原则。

7.3 适应场合

系统中有多个产品族,但每次只使用其中的某一族产品。切换产品族只需要修改一下具体工厂对象即可。

比如本文示例中,针对不同数据库操作我们可以实现不同的产品族,切换数据库只需要简单的修改具体工厂对象。

8. 参考及源码


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK