重温设计模式(五)——我所理解的"抽象工厂"

在各位读这篇文章之前,我给大家一个提醒,我的文章也许称不上一个设计模式类的文章,只能算是自己在项目中的一个设计总结,在这里,我很欢迎大家和我一起讨论。但是同时我想说,我写博客的目的本身就是希望我的经验可以提醒他人,但是如果造成了一种相反的影响,那就违背我的初衷了,所以希望大家对我的观点思考,思考,再思考!另外,感谢Jake.NET,装配脑袋,横刀天笑等对我的鼓励。

1. 代码引子

让我们由一段代码引出我们的这篇文章。

相信每个人,无论是B/S,还是C/S。都无可避免地要去和数据库打交道。那么这样的代码再常见不过:

static void Main(string[] args)
{
    string connectionString = ".........";
    SqlConnection thisConnection = new SqlConnection(connectionString);
    string commandString = "select * from A";
    using (thisConnection)
    {
        thisConnection.Open();
        SqlCommand thisCommand = new SqlCommand(commandString, thisConnection);
        thisCommand.ExecuteNonQuery();
    }
}

这样的代码在平时无可争议,运行得也很棒。

可是我在公司就遇到过这样的情况,客户偏偏说,他们觉得SQL Server不好,不能满足什么什么的需求,虽然他说的是谬论,但是我们也必须得去满足客户的需求。

然后就去按照他们的要求去换成Oracle的数据库。却发现工作量非常非常大。于是这个单子就谈丢了。

错误出在哪里?

2. 怎么样去修改?

string connectionString = ".............";
OracleConnection thisConnection = new OracleConnection(connectionString);
string commandString = "select * from A";
using (thisConnection)
{
    thisConnection.Open();
    OracleCommand thisCommand = new OracleCommand(commandString, OracleConn);
    thisCommand.ExecuteNoQuery();
}

每一处,我们都需要把代码改成这个样子,我么知道,在一个程序中,涉及到数据库操作的代码是无处不在的。这样大的工作量,果然不是短时间内可以完成的。更重要的是,很可能因为疏漏而让一个操作完蛋………

3. 引出抽象工厂

我一直没搞明白这个名字是什么意义,为什么是抽象工厂呢??

这个问题我们不去讨论了。步入正式话题,让我们看看究竟什么是抽象工厂,他的目的是什么?

提供一个创建一系列相关相关或互相依赖对象的接口,而无需指定他们具体的类。

接下来还是老套路,让我们看看他的UML图:

abstract

4. 产品族

当同事问我抽象工厂的含义时,我总是会给他引出这样一个词:产品族。

当今社会,很少有一家公司去自己完整地去造一个产品。

一辆车,可能是中国的外壳,美国的引擎,日本的油漆,德国的玻璃等等。

那么我们就把这一套,外壳,引擎,油漆,玻璃称作一个产品族。

5. 横向和纵向

接下来,让我们来解析下横向和纵向的区别。

其实这也是抽象工厂和工厂方法解决问题的类型。

什么是横向。一棵树,同一父节点下的叶子节点之间,也就是兄弟节点之间我们把他们成为横向关系。

image

那纵向呢?不同根节点下的,但是他们最终却会被组合到一起,那么我们就把这种关系称作是纵向。

例如,美国的引擎,中国的外壳。他们一个继承自引擎类,一个继承自外壳类,不是兄弟节点,他们最终却会被组合到一起,于是我们就称他们之间是纵向的关系。

通过这个我们也可以发现,横向是绝对的,纵向是相对的。

中国的玻璃,日本的玻璃,无论如何,他们都继承自玻璃,都是横向关系。

但是对于中国的外壳,美国的引擎而言,假设有一天,造出一个汽车,他不需要引擎了,那么他们之间的纵向关系也就取消了。

6. 抽象工厂的小应用

虽然我竭力希望找到一个不那么俗气的例子,可是却仍然未能脱俗。

还是用电脑来举例子吧。

class Program
{
    static void Main(string[] args)
    {
        AbstractFactory factory = new Factory1();
        CPU cpu = factory.CreateCPU();
        Graphics graphics = factory.CreateGraphics();
        cpu.DoSomething();
        graphics.DoSomething();
    }

}
/// <summary>
/// 各种CPU
/// </summary>
abstract class CPU
{ 

}
class IntelCPU : CPU
{ 

}
class AMDCPU : CPU
{ 

}

/// <summary>
/// 各种显卡
/// </summary>
abstract class Graphics
{ 

}
class ATIGraphics : Graphics
{ 

}
class GForceGraphics : Graphics
{ 

}

/// <summary>
/// 各种抽象工厂
/// </summary>
abstract class AbstractFactory
{
    public abstract CPU CreateCPU();
    public abstract Graphics CreateGraphics();
}
class Factory1 : AbstractFactory
{
    public override CPU CreateCPU()
    {
        return new AMDCPU();
    }
    public override Graphics CreateGraphics()
    {
        return new ATIGraphics();
    }
}
class Factory2 : AbstractFactory
{
    public override CPU CreateCPU()
    {
        return new IntelCPU();
    }
    public override Graphics CreateGraphics()
    {
        return new GForceGraphics();
    }
}

CPU与显卡是一个纵向的组合关系,每种CPU都可以与每种显卡进行一个自由的组合,因此我们在这里用抽象工厂模式来完成一个他们整体的组合关系。

7. 抽象工厂的.NET应用

在这里,我们回归在文章开始我们那个数据库的问题。

在.NET Framework中,为我们提供这样一个命名空间:System.Data.Common;

这个命名空间包含由各种.NET Framework数据提供程序共享的类。旨在给开发人员提供一种方法以编写将作用于所有.NET Framework数据提供程序的ADO.NET代码。

我们试着用这个方法去改造上面的代码:

static void Main(string[] args)
{
    DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient");
    DbConnection thisConnection = factory.CreateConnection();
    string commandString = "select * from A";
    using (thisConnection)
    {
        thisConnection.Open();
        DbCommand thisCommand = factory.CreateCommand();
        thisCommand.CommandText = commandString;
        thisCommand.Connection = thisConnection;
        thisCommand.ExecuteNonQuery();
    }
}

这个时候,我们可以说,你的数据库操作与具体的数据库解耦了。

但是,我们在修改的时候,依然需要去修改N处的代码。因为你需要在每个调用处都把System.Data.SqlClient修改为System.Data.OracleClient。

怎么办呢?

8. 抽象工厂+配置文件

配置文件是个很管用的东西,每次修改配置文件后,整个项目不用再经过痛苦的编译过程。

那么我们就可以把变化的部分写入配置文件。

这也是很多设计思路中都通用的一个地方。

这里我就不具体写出代码了,思路大概就是:

<add key=”SQL Server” value=”System.Data.SqlClient”>

然后我们在代码处不把具体的字符串写死,而是去调用这段配置文件的Value即可。

9. 抽象工厂+简单工厂

让我们回顾一下,简单工厂解决了什么?

简单工厂将对象的具体创建过程与客户端隔离,而把对象的创建统一封装在一个工厂中。

那么我们就在上面的代码中,DBProviderFactory的创建,我们就把他拿到简单工厂中去实现:

static class SimpleFactory
{
    public DbProviderFactory GetFactory(string dbName)
    {
        switch (dbName)
        { 
            case "SQL Server":
                return DbProviderFactories.GetFactory("System.Data.SqlClient");
                break;
            case "Oracle":
                return DbProviderFactories.GetFactory("System.Data.OracleClient");
                break;
            default:
                return DbProviderFactories.GetFactory("System.Data.Oledb");
        }
    }
}

10. 貌似回到原点

这个时候很多人会说,你这个样子不又回到原点了么?你之前要用DBProviderFactory本来就是为了避免在N个客户端进行修改,但是这样写,我又需要在每个客户端都去修改传入的DBName。

OK。没问题!

让我们继续改善程序,在上文:重温设计模式(四)——工厂模式 中,我提出了一个简化版的简单工厂。

我们重写上面的SimpleFactory:

static class SimpleFactory
{
    public DbProviderFactory GetFactory()
    {
        return DbProviderFactories.GetFactory("System.Data.SqlClient");
    }
}

然后我们把这个简单工厂单独去封装在一个dll内,之后,我们如果需要改变数据库连接的时候,我们只需要去修改这个dll就可以了。牵一发,而动全身。

可谓完美么?

11. 简单工厂 vs.  简化版简单工厂

很多人会说,如果能这样的话,那简单工厂要之何用?

我们来这样考虑。如果在一个项目中,我们用了两套数据库,一个为Oracle,一个为SQL Server, 那你怎么样去解决?

你这个只能返回一个固定的工厂。在这个情况下就不能很好地满足需求了。

所以,你什么样的设计要取决于你的实际情况。不要不设计,也不要过度设计。

12. 类爆炸

在之前的文章中,我用过很多次的对象爆炸这个词。在这里,我想提出一个新的词,叫做类爆炸。

什么叫类爆炸。还用上面的电脑的例子来说话。

Intel的CPU,AMD的CPU,ATI的显卡,GForce的显卡,两组横向关系,每组横向关系有两个元素,所以是2*2=4种组合,如果10组横向关系,每组关系有10个元素呢?那我们就需要10*10个抽象工厂,而且我们知道抽象工厂的作用仅仅在于帮助我们去建造出这个组合对象。

那怎么办?怎么办?怎么办?

13. 改造抽象工厂

我们之前看到,如果为每一个产品都去指定一个抽象工厂,势必要引起抽象工厂的类爆炸。

那么我们该如何去改进?

我们看到,每个抽象工厂的纵向产品数是固定的,他们不一样的只是横向产品的选择问题。

那么我们就把我们想要选择的横向产品添加进去。

class AbstractFactory
{
    private string cpuType;
    private string graphicsType;
    public AbstractFactory(string cpuType, string graphicsType)
    {
        this.cpuType = cpuType;
        this.graphicsType = graphicsType;
    }
    public CPU CreateCPU()
    {
        Type t = Type.GetType(cpuType);
        return (CPU)Activator.CreateInstance(t);
    }
    public Graphics CrateGraphics()
    {
        Type t = Type.GetType(graphicsType);
        return (Graphics)Activator.CreateInstance(t);
    }
}

然后我们在客户端这样去写:

static void Main(string[] args)
{
    AbstractFactory factory = new AbstractFactory("InterCPU", "ATIGraphics");
    CPU cpu = factory.CreateCPU();
    Graphics graphics = factory.CrateGraphics();
}

当然,我们可以把这个new factory的过程封装到一个统一的dll中去。

我想,这个时候肯定会有人说,这样不好,这样不就相当于又增加了客户端和产品类之间的耦合了么?

这个很简单,我们一样可以用配置文件去解决这个问题。

这里就不再去写代码了。

14 . 抽象工厂的局限性

让我们继续考虑上面那个电脑的例子。原来,我们的电脑有CPU,有显卡,使用抽象工厂,我们可以很灵活地去配置我们电脑的硬件设备。

但是我们设想,如果有一天,电脑增加了一个键盘,这个键盘有可能是微软的,有可能是IBM的。那怎么办?

我们需要增加一个键盘的类,然后增加两个子类。另外,我们还需要去修改抽象工厂的类。

如果我们采用经典的抽象工厂,就可能要去修改上万个抽象工厂。

怎么办?怎么办?怎么办?

15.  泛型改造抽象工厂

既然创建的步骤都是一样,那么我们就把创建的过程给抽象出来,当然,方法就是泛型。

class AbstractFactory
{
    public static T Create<T>()
    {
        return (T)Activator.CreateInstance(typeof(T));
    }
}

但是这样我们又如何不向客户端去暴露具体类型呢?

方法是我们像提供一个字典,然后我们要去维护的仅仅是这个字典。

这个字典我们可以选择在配置文件中去写:

<add key=”CPU” value=”IntelCPU”/>

<add key=”Graphics” value=”ATIGraphics”/>

那么我们在客户端仅需如此调用:

static void Main(string[] args)
{
    CPU cpu=AbstractFactory.Create<Type.GetType([cpu])>();
    Graphics graphics=AbstractFactory.Create<Type.GetType["graphics"]>();
}

那么,当我们增加一个键盘的时候,我们只需要在配置文件中做出修改即可,而不用去懂那成千上万个抽象工厂了。

16. 写在模式后

以上的文章,每一种对模式的改造都是针对不同情况下的一种个人提出的改进。

我们学习模式本就是学习那一种设计思路,而没有必要这一种情况,我就去循规蹈矩地照着UML图去写对应的类和模式,而完全可以使用一套自己的改良方案。

每一种情况我们都有着一个对应的,我们心中的一个抽象工厂,究竟什么事抽象工厂。

GOF为我们提供了一个模板,而这个模板要靠我们自己去进一步地设计。

设计原则是做什么用的?设计模式是做什么用的?

他们是一个优秀的实践标准,但是他们并不是一个通用的东西。能满足我们需求的东西就是好东西,能满足我们需求和变化的设计就是好设计,而那一刻,无论是设计原则,还是设计模式,都融入我们的心中,而变得空旷起来。

总之,遵守了所有的设计原则和设计模式的设计不见得是一个好设计,而适当地违反,又不见得是一个坏设计,甚至可能更优秀,更轻量。这才是我们的目的!

17. 工厂大总结

简单工厂是一个最简单的工厂,最轻量级,他相当于一个网络中心,把所有的职责全都担在自己的肩上,一旦这个工厂崩溃了,那么整个系统就崩溃了。

工厂方法是最适中的工厂,每一个工厂都封装了一个产品的创建过程。

抽象工厂是解决了一个产品族的创建问题,在抽象工厂中,我们要注意横向和纵向这两个概念。

针对不同的情况我们要采取不同的设计,工厂模式,抽象工厂未必就优于简单工厂,甚至未必优于无模式。

18. 最后忠告

一个真正优秀的软件设计人员,他们不会去套用每一个模式,他们在心中都会有自己的一套设计策略,这才谓之无刀胜有刀。

死背设计模式,对着GOF的UML图去一点点地敲代码,去拼凑模式,那只能说,你还是停留在编码上,在设计这条路上,你还差得远。

还记得《建筑永恒之道》中说:什么是永恒,学会他,然后马上忘掉他。

posted @ 2009-04-13 00:39  飞林沙  阅读(2365)  评论(39编辑  收藏  举报