设计模式精髓—封装变化

设计模式精髓—封装变化

模式,是为了需求变动而产生,抛开需求谈模式,显得很苍白。 无论是创建型模式、结构型模式还是行为型模式,归根结底都是寻找软件中可能存在的“变化”,然后利用抽象的方式对这些变化进行封装。由于抽象没有具体的实现,就代表了一种无限的可能性,使得其扩展成为了可能。

创建型模式的目的就是封装对象创建的变化。例如Factory Method模式和Abstract Factory模式,建立了专门的抽象的工厂类,以此来封装未来对象的创建所引起的可能变化。而Builder模式则是对对象内部的创建进行封装,由于细节对抽象的可替换性,使得将来面对对象内部创建方式的变化,可以灵活的进行扩展或替换。

至于结构型模式,它关注的是对象之间组合的方式。本质上说,如果对象结构可能存在变化,主要在于其依赖关系的改变。当然对于结构型模式来说,处理变化的方式不仅仅是封装与抽象那么简单,还要合理地利用继承与聚合的方法,灵活地表达对象之间的依赖关系。例如Decorator模式,描述的就是对象间可能存在的多种组合方式,这种组合方式是一种装饰者与被装饰者之间的关系,因此封装这种组合方式,抽象出专门的装饰对象显然正是“封装变化”的体现。同样地,Bridge模式封装的则是对象实现的依赖关系,而Composite模式所要解决的则是对象间存在的递归关系。

行为型模式关注的是对象的行为。行为型模式需要做的是对变化的行为进行抽象,通过封装以达到整个架构的可扩展性。例如策略模式,就是将可能存在变化的策略或算法抽象为一个独立的接口或抽象类,以实现策略扩展的目的。或者封装一个请求(Command模式),或者封装一种状态(State模式),或者封装“访问”的方式(Visitor模式),或者封装“遍历”算法(Iterator模式)。而这些所要封装的行为,恰恰是软件架构中最不稳定的部分,其扩展的可能性也最大。将这些行为封装起来,利用抽象的特性,就提供了扩展的可能。

利用设计模式,通过封装变化的方法,可以最大限度的保证软件的可扩展性。面对纷繁复杂的需求变化,虽然不可能完全解决因为变化带来的可怕梦魇,然而,如能在设计之初预见某些变化,仍有可能在一定程度上避免未来存在的变化为软件架构带来的灾难性伤害。

比如拿工厂模式来说:应用场所是------创建对象的方法相同,但创建具体的对象会经常变化。 换句话说:我的代码中许多地方都要产生一个新的对象,而这个创新的对象在未来一段时间内,有可能会经常变化。当这个对象真的要变了时,我如何用最少的代价修 改我的代码呢?因此,有必要把"创新对象"进行解耦。尽可能的,我只改一两处,就实现 了所有代码的修改。如何实现呢?  凡是要生成新对象的代码,都统一用个类的方法产 实例,如果对象要变时,修改这个类就行了。 

抽象工厂模式的定义:抽象工厂模式提供了一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体的类。抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道实际的具体产品是什么,这样客户就从具体的产品中被解耦了。

举个例子http://blog.csdn.net/rainylin/article/details/1715308 : 我们需要设计一个数据库组件,它能够访问微软的Sql Server数据库。根据ADO.Net的知识,我们需要使用如下的对象:SqlConnection, SqlCommand, SqlDataAdapter等。

如果仅就Sql Server而言,在访问数据库时,我们可以直接创建这些对象:
SqlConnection connection = new SqlConnection(strConnection);//连接数据库
SqlCommand command = new SqlCommand(connection);//创建数据库操作语句
SqlDataAdapter adapter = new SqlDataAdapter();//对结果集进行处理

在一个数据库组件中,充斥着如上的语句,显然是不合理的。它充满了僵化的坏味道,一旦要求支持其他数据库时,原有的设计就需要彻底的修改,这为扩展带来了困难。

那么我们来思考一下,以上的设计应该做怎样的修改?假定该数据库组件要求或者将来要求支持多种数据库,那么对于Connection,Command,DataAdapter等对象而言,就不能具体化为Sql Server的对象。也就是说,我们需要为这些对象建立一个继承的层次结构,为他们分别建立抽象的父类,或者接口。然后针对不同的数据库,定义不同的具体类,这些具体类又都继承或实现各自的父类,例如Connection对象:


Connection对象的层次结构

我为Connection对象抽象了一个统一的IConnection接口,而支持各种数据库的Connection对象都实现了IConnection接口。同样的,Command对象和DataAdapter对象也采用了相似的结构。现在,我们要创建对象的时候,可以利用多态的原理创建:
IConnection connection = new SqlConnection(strConnection);

从这个结构可以看到,根据访问的数据库的不同,对象的创建可能会发生变化。也就是说,我们需要设计的数据库组件,以现在的结构来看,仍然存在无法应对对象创建发生变化的问题。利用“封装变化”的原理,我们有必要把创建对象的责任单独抽象出来,以进行有效地封装。例如,如上的创建对象的代码,就应该由专门的对象来负责。我们仍然可以建立一个专门的抽象工厂类DBFactory,并由它负责创建Connection,Command,DataAdapter对象。至于实现该抽象类的具体类,则与目标对象的结构相同,根据数据库类型的不同,定义不同的工厂类,类图如图四所示:


DBFactory的类图

是一个典型的Abstract Factory模式的体现。类DBFactory中的各个方法均为abstract方法,所以我们也可以用接口来代替该类的定义。继承DBFactory类的各个具体类,则创建相对应的数据库类型的对象。以SqlDBFactory类为例,创建各自对象的代码如下:

public class SqlDBFactory: DBFactory
{
       public override IConnection CreateConnection(string strConnection)
       {
              return new SqlConnection(strConnection);
       }

       public override ICommand CreateCommand(IConnection connection)
       {
              return new SqlCommand(connection);
       }

       public override IDataAdapter CreateDataAdapter()
       {
              return new SqlDataAdapter();
       }
}

现在要创建访问Sql Server数据库的相关对象,就可以利用工厂类来获得。首先,我们可以在程序的初始化部分创建工厂对象:

DBFactory factory = new SqlDBFactory();

然后利用该工厂对象创建相应的Connection,Command等对象:
IConnection connection = factory.CreateConnection(strConnection);
ICommand command = factory.CreateCommand(connection);

由于我们利用了封装变化的原理,建立了专门的工厂类,以封装对象创建的变化。可以看到,当我们引入工厂类后,Connection,Command等对象的创建语句中,已经成功地消除了其与具体的数据库类型相依赖的关系。在如上的代码中,并未出现Sql之类的具体类型,如SqlConnection、SqlCommand等。也就是说,现在创建对象的方式是完全抽象的,是与具体实现无关的。无论是访问何种数据库,都与这几行代码无关。至于涉及到的数据库类型的变化,则全部抽象到DBFactory抽象类中了。需要更改访问数据库的类型,我们也只需要修改创建工厂对象的那一行代码,例如将Sql Server类型修改为Oracle类型:DBFactory factory = new OracleDBFactory();

很显然,这样的方式提高了数据库组件的可扩展性。我们将可能发生变化的部分封装起来,放到程序固定的部分,例如初始化部分,或者作为全局变量,更可以将这些可能发生变化的地方,放到配置文件中,通过读取配置文件的值,创建相对应的对象。如此一来,不需要修改代码,也不需要重新编译,仅仅是修改xml文件,就能实现数据库类型的改变。例如,我们创建如下的配置文件:

创建工厂对象的代码则相应修改如下:
string factoryName = ConfigurationSettings.AppSettings[“db”].ToString();
//DBLib为数据库组件的程序集:
DBFactory factory = (DBFactory)Activator.CreateInstance(“DBLib”,factoryName).Unwrap();

为数据库组件的程序集:当我们需要将访问的数据库类型修改为Oracle数据库时,只需要将配置文件中的Value值修改为“OracleDBFactory”即可。这种结构具有很好的可扩展性,较好地解决了未来可能发生的需求变化所带来的问题。

再来个策略模式的例子

设想这样一个需求,我们需要为自己的框架提供一个负责排序的组件。目前需要实现的是冒泡排序算法和快速排序算法,根据“面向接口编程”的思想,我们可以为这些排序算法提供一个统一的接口ISort,在这个接口中有一个方法Sort(),它能接受一个object数组参数。对数组进行排序后,返回该数组。接口的定义如下:

public interface ISort
{
    void Sort(ref object[] beSorted);
}

其类图如下:

 

然而一般对于排序而言,排列是有顺序之分的,例如升序,或者降序,返回的结果也不相同。最简单的方法我们可以利用if语句来实现这一目的,例如在QuickSort类中:

public class QuickSort:ISort
{
    private string m_SortType;

    public QuickSort(string sortType)
    {
         m_SortType = sortType;
    }

    public void Sort(ref object[] beSorted)
    {
           if (m_SortType.ToUpper().Trim() == “ASCENDING”)
          {
              //执行升序的快速排序;
          }
          else
          {
              //执行降序的快速排序;
          }
     }
}

当然,我们也可以将string类型的SortType定义为枚举类型,减少出现错误的可能性。然而仔细阅读代码,我们可以发现这样的代码是非常僵化的,一旦需要扩展,如果要求我们增加新的排序顺序,例如字典顺序,那么我们面临的工作会非常繁重。也就是说,变化产生了。通过分析,我们发现所谓排序的顺序,恰恰是排序算法中最关键的一环,它决定了谁排列在前,谁排列在后。然而它并不属于排序算法,而是一种比较的策略,后者说是比较的行为。

如果仔细分析实现ISort接口的类,例如QuickSort类,它在实现排序算法的时候,需要对两个对象作比较。按照重构的做法,实质上我们可以在Sort方法中抽取出一个私有方法Compare(),通过返回的布尔值,决定哪个对象在前,哪个对象在后。显然,可能发生变化的是这个比较行为,利用“封装抽象”的原理,就应该为该行为建立一个专有的接口ICompare,然而分别定义实现升序、降序或者字典排序的类对象。

 

我们在每一个实现了ISort接口的类构造函数中,引入ICompare接口对象,从而建立起排序算法与比较算法的弱耦合关系(因为这个关系与抽象的ICompare接口相关),例如QuickSort类:

public class QuickSort:ISort
{
    private ICompare m_Compare;
    public QuickSort(ICompare compare)
    {
        m_Compare= compare;
    }
    public void Sort(ref object[] beSorted)
    {
        //实现略
        for (int i = 0; i < beSorted.Length - 1; i++)
       {
            if (m_Compare.Compare(beSorted[i],beSorted[i+1))
           {
               //略;
           }
        }
        //实现略
    }
}

最后的类图如下:

 

通过对比较策略的封装,以应对它的变化,显然是Stategy模式的设计。事实上,这里的排序算法也可能是变化的,例如实现二叉树排序。由于我们已经引入了“面向接口编程”的思想,我们完全可以轻易的添加一个新的类BinaryTreeSort,来实现ISort接口。对于调用方而言,ISort接口的实现,同样是一个Strategy模式。此时的类结构,完全是一个对扩展开发的状态,它完全能够适应类库调用者新需求的变化。

 

posted on 2015-09-25 16:53  淡如清风  阅读(2073)  评论(2编辑  收藏  举报

导航