大多数人说的工厂模式,应该是指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模式。他俩往往也是混合在一起的。通常我们在设计系统的时候应该是一个逐步迭代的过程,总是从最简单的设计慢慢一步步演变而来。驱动这种演变的往往是需求变动。当需求变动比较频繁的时候,我们才往复杂的结构演变。这个例子里面的迭代演变看起来就像是: