OOD设计之依赖倒置原则:解耦高层模块和底层模块的利器是让两者皆依赖于抽象。抽象是什么呢?抽象是一个约定,在C++中这个约定可通过一个纯虚类来表示,Java中便是interface。为什么要解耦高层模块和底层模块啊?因为谁都不想被谁锁定喽。e.g. 有两个模块:一个是用于管理图像的模块 A,另一个是对图片进行编解码的模块 B;显然模块 A 在读取图片的时候要依赖于模块B提供的图片解码服务,在保存图片的时候需要依赖模块B提供的图片编码服务。那么这时候,模块 A 的代码中就会混杂着模块B的APIs。过了段时间之后,发现不知从哪冒出个模块C也提供了类似模块B的功能,并且编解码效率比模块B的要好。那么这时模块A就按捺不住了,为提高性能它想把模块B抽换为模块C。但是由于模块A中的代码中混杂着模块B的API,因此这种替换是很花费时间的,也是违反开闭原则的。这样的设计也是糟糕的。回顾头来想想,如何避免这种情况发生呢?其实道理很简单,只要将模块A中读取和保存图片的部分定义成两个接口,然后再用模块B提供的API来实现这些接口。具体地,可以定义一个很简单的模块D,用它来依赖模块B实现模块A中的接口。这样当模块C出现的时候,我们就可以再写个模块E,让模块E来依赖模块C并且实现模块A中的接口,这样就可以很轻易地将模块B和模块D抽换为模块C和模块E,而不用修改模块A的代码,这很好滴遵循了对修改封闭对扩展开放的OOD开闭原则。
之前在重构一个图像算法库时遇到过一个类似的问题:老库中有个算法,它在运行过程中要读入一个图片做mask用。当时它依赖于MFC库的一个Bitmap类,这个类通过接受一个图片地址就可以将其load进来。为了跨平台的考虑,我不希望新库中依赖第三方读存图片的库,也不希望新库的使用者将Mask作为一个参数传递给算法,因为在新库的参数设计部分没有考虑像Mask这种大数据量的参数。那么怎么解决这个问题呢?我对这个问题的解决之道遵循的便是依赖倒置原则,既然新库中不想自己load图片,那么就将这个任务扔给应用程序来做好了。我只需要在库内部定义一组接口,让库的使用者实现这个接口就完事了。e.g
struct Mask{ char* data; unsigned int width,height; unsigned int stride; unsigned int bytePerPixel; }; class MaskLoader{ public: virtual ~MaskLoader(); virtual Mask* load(const char* fileName) = 0; virtual void unload(Mask** mask) = 0; protected: MaskLoader(); };
结构体Mask是新库中要用到的Mask类,接口MaskLoader是新库内部用来加载Mask的加载器,因为不止一个算法会用到MaskLoader类,因此在库内部声明一个全局变量让所有的算法类共享之,库的使用者需要实现MaskLoader类,并将MaskLoader的派生类的对象设置到库内部就完事了。
MaskLoader* gMaskLoader(0);
为了不暴露内部数据,再定义两个API来让库的使用者加载和卸载MaskLoader
void initMaskLoader(MaskLoader* maskLoader); void deinitMaskLoader();
小结:通过一个抽象类解耦了新库和读存图像库之间的依赖,从而让新库增加了更多的灵活性。