首页 > 代码库 > 再议工厂模式(Abstract Factory)和DIP的关系

再议工厂模式(Abstract Factory)和DIP的关系

大多数人说的工厂模式,应该是指GOF设计模式里面的Abstract Factory模式。

这是一种很常见又很有用的模式。它和DIP原则又有什么关系呢?

DIP原则

DIP: Dependency inversion principle。

DIP也就是依赖倒置原则,讲的是上层模块应该依赖于接口,具体类应该依赖于抽象接口(也就是被迫实现抽象接口)。因为抽象接口更接近于它的使用者(上层模块),所以看上去就像具体类依赖于上层模块一样,这才称之为依赖倒置。

如果严格按照DIP来讲,任何一条new语句就违反了DIP。比如:

class Pic
{
public:
     virtual void draw() = 0;
};

class Png : public Pic
{
public:
     virtual void draw()
     {
            printf("draw png\n");
     }
}

void main()
{
     Png* p = new Png();
     p->draw();
}

看上面的代码,main()函数作为Pic类库的使用者,new Png()本身就已经违反了DIP,这是不是很搞笑,这种代码很常见啊。但它实实在在违反了DIP。我们在客户代码里面看到了Png类,Png类是一个具体类,那就说明客户依赖于具体类了。那还不就是违反了DIP?严格来说,确实违反了。但是不一定有多大坏处。这个要看Png具体类发生变化的可能性有多大?如果Png类基本不会变,那违反就违反,没有什么关系。就好象我们使用stl里面的类一样,那些类几乎不会变,那客户代码直接依赖于它们,又有何不可。但是如果Png类发生变化的可能性很大,这个时候依赖于具体类就不是很好了。

上面的代码,如果改成:

void main()
{
     Pic* p = new Png();
     p->draw();
}

那么情况就会好很多,因为只有new依赖于具体类,new返回的指针保存在Pic* p里面,所有其他地方都将使用Pic*, 这就是说除了new本身,其他地方都是依赖于接口。但是new本身确实还是违法了DIP。我们可以画个图:

我们可以看到上面的图里面,main()作为一个客户,它强依赖(或者关联)于Pic接口,然后又有个Png的依赖关系(虚线)。确实,main()有很多地方需要调用Pic* p,所以是关联。在new Pic(),就是一个弱依赖关系。其实上面的这个设计在大多数情况下都是可行的,没什么问题,也很常见。

那么如果当Png变化的可能性很大的时候,比如Png的构造函数经常变,或者创建其他Pic子类的可能性很大,甚至Png的类名会变化。这个时候,上面的图就有点问题了。毕竟main弱依赖于具体类了。

这个时候,我们就可以考虑工厂模式了。(GOF叫做Abstract Factory)。

简单工厂模式(Abstract Factory)

我们可以考虑把上面的图演化成:


我们断开了main()和Png的依赖,取而代之的是引入一个工厂类PicFactory。main()关联了PicFactory,而PicFactory依赖于Png。代码大致如下:

class PicFactory
{
public:
      Pic* makePng()
      {
              return new Png()
      }
};

void main()
{
       PicFactory f;
       Pic* p = f.makePng();
} 

这就是一个简单工厂。这么做有什么好处呢?因为Png类很易变,所以我们把它放到了工厂类中。客户main()只需要使用工厂类来创建Pic对象。肯定有人会问,刚才我们是依赖于Png类,现在依赖于PicFactory类,这有什么分别呢?关键就在于:Png类或者Pic的其他子类易变,而工厂类比较稳定,依赖于一个比较稳定的类总比依赖于易变的类来的好。

比如我们现在需要增加一个新的Pic子类,叫做Gif,我们要做的就是:

class Gif : public Pic
{
public:
     virtual void draw()
     {
        printf("Gif::draw\n");
     }
};

class PicFactory
{
public:
         Pic* makePng()
         {
                return new Png();
         }
         Pic* makeGif()
         {
                return new Gif();
         }
}

void main()
{
          PicFactory f;
          Pic* gif = f.makeGif()
          gif->draw();
}

1. 新增一个Gif类

2. 在PicFactory里面增加一个函数makeGif()

3. 客户main那里就可以通过工厂来创建Gif对象了。

这么做有个问题,就是每次增加一个新的Pic子类,工厂都得相应增加一个函数,这也不是很舒服。

可以稍微变通一下,把工厂类改成:

typedef enum PicType{Png = 0, Gif};
class PicFactory
{
public:
    Pic* makePic(PicType t)
    {
           Pic* p = nullptr;
           switch(t)
           {
                case Png:
                     p = new Png();
                     break;
                case Gif:
                     p = new Gif();
                     break;
           }
           return p;
    }
}

这样的话,我们每次增加新的Pic子类,不需要增加新的工厂函数了,只需要修改一下makePic函数就行了。比原来的好一些。

工厂模式的另外一个好处就是:工厂本身也可以替换。

工厂模式(可替换)

考虑这么一个问题,Png有另外一种实现,这个Png支持在Edit Control里面画出来,比如在qq的编辑框里面画,我们叫做PngEx。这个时候有两种办法来修改工厂类:

1. 在makePic里面增加一个新的case,然后返回PngEx对象

2. 创建一个新的工厂类。

具体使用哪个,应该看具体情况。如果我们现在的需求是:更换一组对象的创建。那么可能#2会比较好。通常我们的一个工厂创建的是一组相关的产品,如果需要创建另外一组产品,那么就创建另外一个工厂。比如我们一个工厂创建一组Pic对象,它们只支持GDI绘画,另外一个工厂支持DIRECT X绘画。

要达到这种效果,我们首先需要改造工厂类,我们需要给工厂类弄一个抽象接口,实际上这就是Abstract Server模式。

看:

OK, 现在main()就依赖于工厂抽象类了,这就更加灵活了。当我们想使用另外一组产品的时候,换个工厂就行了。比如:

void main()
{
       PicFactory* f = new GDIFactory();
  //     PicFactory* f = new DXFactory();
       f->makePic();
}

直接更换工厂就可以创建另外一组产品。细心的同学一定会发现,这个new岂不是又违反了DIP?肯定是啊。是不是很晕?确实,很多时候设计领域总是会出现一些另外很纠结的事情。OK,之前也已经讲到过,如果严格遵守DIP的话,任何一行new代码都违反了DIP。关键是要看这个违反有没有关系。如果目标对象比较稳定,那么违反也不要紧。比如这里的Factory,除了它有可能增加新的派生工厂外,其他几乎不会变。所以这个地方的违反是不要紧的。我们之所以引入工厂,是因为Pic类很易变,工厂本身比较稳定,这个时候工厂模式就可以发挥作用了。

至于什么时候引入工厂模式,要看具体情况而定。一个比较简单的准则是:当你所要创建的产品(pic类)比较易变时,就该考虑了。

当然工厂模式也有一定的坏处:首先它要创建工厂类,这是一个主要开销。工厂模式的好处就是:可以解开客户和产品具体类的耦合,实现DIP原则。

实际上更加一般的情况是:一个工厂可以创建一系列产品,而这些产品并不是继承于同一个接口。更加一般的情况应该如下图:




迭代演变

工厂模式是一个很有效的模式,很多地方都可以看到。但是我们也不能一上来就使用,那就是滥用。因为工厂模式本身就会带来一些问题,最起码的引入了新的工厂类,这就是一个开销。没有最好的设计,只有合适的设计。当我们在某些场景需要考虑DIP原则的时候,工厂模式往往是很有效的。当然还有Abstract Server模式。他俩往往也是混合在一起的。通常我们在设计系统的时候应该是一个逐步迭代的过程,总是从最简单的设计慢慢一步步演变而来。驱动这种演变的往往是需求变动。当需求变动比较频繁的时候,我们才往复杂的结构演变。这个例子里面的迭代演变看起来就像是:





再议工厂模式(Abstract Factory)和DIP的关系