Saturday, September 29, 2007

(ZT) About Abstract Factory

一、概述

在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时由于需求的变化,往往存在着更多系列对象的创建工作。如何应对这种变化?如何绕过常规的对象的创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合?这就是我们要说的抽象工厂模式。抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态,工厂方法针对的仅仅是一种“产品”,或者称为“类”,而抽象工厂实际上针对很多平行的产品,因此层次不同。抽象工厂才是名副其实的“工厂”,即不仅仅只生产一种产品,抽象工厂是层次较高的模式,针对应用中需要使用的一系列相关的类给出一个创建接口。

学会了用抽象工厂模式,你将理解OOP的精华:面向接口编程。

二、意图

提供一个接口,让该接口负责创建一系列相关或者相互依赖的对象,无需指定它们具体的类。

三、Abstract Factory模式的结构:

四、举例

学习抽像工厂模式时,个人觉得吕震宇老师的一个例子很能说明问题,如下所示:

// "AbstractFactory"
abstract class AbstractFactory
{
// Methods
abstract public AbstractProductA CreateProductA();
abstract public AbstractProductB CreateProductB();
}

// "ConcreteFactory1"
class ConcreteFactory1 : AbstractFactory
{
// Methods
override public AbstractProductA CreateProductA()
{
return new ProductA1();
    }
override public AbstractProductB CreateProductB()
{
return new ProductB1();
    }
}

// "ConcreteFactory2"
class ConcreteFactory2 : AbstractFactory
{
// Methods
override public AbstractProductA CreateProductA()
{
return new ProductA2();
    }

override public AbstractProductB CreateProductB()
{
return new ProductB2();
    }
}

// "AbstractProductA"
abstract class AbstractProductA
{
}

// "AbstractProductB"
abstract class AbstractProductB
{
// Methods
abstract public void Interact(AbstractProductA a);
}

// "ProductA1"
class ProductA1 : AbstractProductA
{
}

// "ProductB1"
class ProductB1 : AbstractProductB
{
// Methods
override public void Interact(AbstractProductA a)
{
        Console.WriteLine(this + " interacts with " + a);
    }
}

// "ProductA2"
class ProductA2 : AbstractProductA
{
}

// "ProductB2"
class ProductB2 : AbstractProductB
{
// Methods
override public void Interact(AbstractProductA a)
{
        Console.WriteLine(this + " interacts with " + a);
    }
}

// "Client" - the interaction environment of the products
class Environment
{
// Fields
private AbstractProductA AbstractProductA;
private AbstractProductB AbstractProductB;

// Constructors
public Environment(AbstractFactory factory)
{
        AbstractProductB = factory.CreateProductB();
        AbstractProductA = factory.CreateProductA();
    }

// Methods
public void Run()
{
        AbstractProductB.Interact(AbstractProductA);
    }
}

/**//// <summary>
/// ClientApp test environment
/// </summary>
class ClientApp
{
static void Main(string[] args)
{
        AbstractFactory factory1 = new ConcreteFactory1();
        Environment e1 = new Environment(factory1);
        e1.Run();

        AbstractFactory factory2 = new ConcreteFactory2();
        Environment e2 = new Environment(factory2);
        e2.Run();
        Console.ReadLine();
    }
}

五、优点

1、分离了具体的类。抽象工厂模式帮助你控制一个应用创建的对象的类,因为一个工厂封装创建产品对象的责任和过程。它将客户和类的实现分离,客户通过他们的抽象接口操纵实例,产品的类名也在具体工厂的实现中被分离,它们不出现在客户代码中;

2、使产品系列容易交换,只要更换相应的具体工厂即可(经常用工厂方法来实现);

3、有利于产品的一致性,由抽象工厂创建的产品必须符合相同的接口,任何在子类中的特殊功能都不能体现在统一的接口中;

六、缺点

1、难以支持新产品的类型,如果抽象工厂的基类要发生变化,那么针对每个产品系列的具体工厂也都要发生变化,显然这是很麻烦的;

2、不能创建有着不同个数对象的系列;

注:

抽象工厂模式主要在于应对“新系列”的需求变化。其缺点在于难于应付“新对象”的需求变动。如果在开发中出现了新对象,该如何去解决呢?这个问题并没有一个好的答案,下面我们看一下李建忠老师的回答:

“GOF《设计模式》中提出过一种解决方法,即给创建对象的操作增加参数,但这种做法并不能令人满意。事实上,对于新系列加新对象,就我所知,目前还没有完美的做法,只有一些演化的思路,这种变化实在是太剧烈了,因为系统对于新的对象是完全陌生的。”

七、适用性

分析了上面的优缺点,当我们在遇到如下情况时,应当考虑使用抽象工厂模式:
1、一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的;
2、这个系统有多于一个的产品组合,而系统只需要使用其中某一产品组合;
3、同属于同一个产品组合的产品是在一起使用的,这一约束必须在系统的设计中体现出来;
4、系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现;

八、实现要点

1、抽象工厂将产品对象的创建延迟到它的具体工厂的子类。

2、如果没有应对“多系列对象创建”的需求变化,则没有必要使用抽象工厂模式,这时候使用简单的静态工厂完全可以。

3、系列对象指的是这些对象之间有相互依赖、或作用的关系,例如游戏开发场景中的“道路”与“房屋”的依赖,“道路”与“地道”的依赖。

4、抽象工厂模式经常和工厂方法模式共同组合来应对“对象创建”的需求变化。

5、通常在运行时刻创建一个具体工厂类的实例,这一具体工厂的创建具有特定实现的产品对象,为创建不同的产品对象,客户应使用不同的具体工厂。

6、把工厂作为单件,一个应用中一般每个产品系列只需一个具体工厂的实例,因此,工厂通常最好实现为一个单件模式。

7、创建产品,抽象工厂仅声明一个创建产品的接口,真正创建产品是由具体产品类创建的,最通常的一个办法是为每一个产品定义一个工厂方法,一个具体的工厂将为每个产品重定义该工厂方法以指定产品,虽然这样的实现很简单,但它确要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。

九、应用场景

1、应用程序开发中需要支持多数据库,比如支持Oracle、SqlServer等;

public class DBOperator
{
private DBFactory dbf;
private IDbConnection dbconn;
public DBOperator(DBFactory dbf)
{
this.dbf = dbf;
    }

public void Open(string connstring)
{
        dbconn = dbf.GetDBConnection();
        dbconn.ConnectionString = connstring;
        dbconn.Open();
    }

public DataSet ExecSQL(string sql)
{
        IDbCommand dbc = dbf.GetDBCommand();
        dbc.CommandText = sql;
        dbc.CommandType = CommandType.Text;
        dbc.Connection = dbconn;
        IDataAdapter ida = dbf.GetDBDataAdapter(dbc);
        DataSet ds = null;
        ida.Fill(ds);
return ds;
    }
}
public abstract class DBFactory
{
public abstract IDbCommand GetDBCommand();
public abstract IDbConnection GetDBConnection();
public abstract IDataAdapter GetDBDataAdapter(IDbCommand idc);
}

public class SQLDBFactory : DBFactory
{
public SQLDBFactory()
{ }
public override IDbCommand GetDBCommand()
{
return new SqlCommand();
    }
public override IDbConnection GetDBConnection()
{
return new SqlConnection();
    }
public override IDataAdapter GetDBDataAdapter(IDbCommand idc)
{
return new SqlDataAdapter((SqlCommand)idc);
    }
}
public class OracleDBFactory : DBFactory
{
//因只是表达意思,此处没有实现
}
public class OleDBFactory : DBFactory
{
//因只是表达意思,此处没有实现
}

//Client
public class DBUse
{
    DBFactory dbf = null;
public DBUse(string dbType)
{
switch (dbType.ToLower())
{
case "sql":
                dbf = new SQLDBFactory();
break;
case "oracle":
                dbf = new OracleDBFactory();
break;
default:
                dbf = new OleDBFactory();
break;
        }
    }

public DataSet GetDataSet(string sql)
{
        DBOperator dbop = new DBOperator(dbf);
        dbop.Open("connctionstring");
return dbop.ExecSQL(sql);
    }
}
class Program
{
static void Main(string[] args)
{
//客户程式调用
    }
}

2、开发电脑组装软件时,比如要组装不同品牌的PC;

针对电脑组装,我们再来看一下上面的例子,我们可以做如下假设:

1)一台电脑的组成就是主板和CPU

2)ConcreteFactory1对应于方正公司,ConcreteFactory2对应于联想公司

3)AbstractProductA对应于主板,AbstractProductB对应于CPU

4)可以把Environment想像成一个第三方公司的组装工艺,此公司从方正和联想采购主板和CPU进行组装

然后客户程式(第三方公司具体组装电脑)在使用时,理解如下:

class ClientApp
{
//如下两个Run(),在实际使用时,一般只会使用其中一个
public static void Main(string[] args)
{
//获得方正公司的产品,注意此处客户程式并不知道有哪些具体产品
    AbstractFactory factory1 = new ConcreteFactory1();
//按照组装工艺进行组装,就得到方正品牌PC
    Environment e1 = new Environment( factory1 );
    e1.Run();
//获得联想公司的产品
    AbstractFactory factory2 = new ConcreteFactory2();
//按照组装工艺进行组装,就得到联想品牌PC
    Environment e2 = new Environment( factory2 );
    e2.Run();
  }
}

此时,有心的读者可能已经发现,在客户程式中,不还是要和具体的类有耦合(AbstractFactory factory1 = new ConcreteFactory1();
)吗?我们可以有两种方式来解决,第一是把这种修改放到配置文件中,实现运行时的维护,如下:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<appSettings>

<add key="factoryName" value="ModernFacilitiesFactory"></add>

</appSettings>

</configuration>

然后客户程式在调用时:

/**//// <summary>
/// ClientApp test environment
/// </summary>
class Program
{
public static AbstractFactory GetInstance()
{
string factoryName = ConfigurationSettings.AppSettings["factoryName"];
        AbstractFactory f;
switch (factoryName)
{
case "ConcreteFactory1":
                f = new ConcreteFactory1();
break;
case "ConcreteFactory2":
                f = new ConcreteFactory2();
break;
default:
                f = null;
break;
        }
return f;
    }
static void Main(string[] args)
{
//此处就已经和ConcreteFactory解耦
        AbstractFactory factory1 = GetInstance();
        Environment e1 = new Environment(factory1);
        e1.Run();
    }
}

这样如果用户哪一天需要组装一台联想电脑,只要把配置文件中的factoryName改为ConcreteFactory2即可,程式不需要做任何修改。这种方式适用在系列对象不发生系列增加的情况下。

第二就是利用一个反射的机制来实现,此种方式可以扩展新的系列对象,我们就可以通过添加DLL并配合配置文件的使用,就能在不修改源程序代码的情况下,扩展出我们需要的新的系列对象。如下:

/**//// <summary>
/// ClientApp test environment
/// </summary>
class Program
{
public static AbstractFactory GetInstance()
{
string factoryName = ConfigurationSettings.AppSettings["factoryName"];
        AbstractFactory f;
if (factoryName != "")
            f = (AbstractFactory)Assembly.Load(factoryName).CreateInstance(factoryName);
else
            f = null;
return f;
    }
static void Main(string[] args)
{
//此处就已经和ConcreteFactory解耦
        AbstractFactory factory1 = GetInstance();
        Environment e1 = new Environment(factory1);
        e1.Run();
    }
}

这样,我们在扩展时仅需将扩展的DLL放在相应的路径下并配合配置文件即实现了我们的扩展。

十、总结

总之,抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,运用抽象工厂模式的关键点在于应对“多系列对象创建”的需求变化。一句话,学会了抽象工厂模式,你将理解OOP的精华:面向接口编程。

以上仅是学习时的个人理解,不对之处请指正,谢谢!