最近,忙于思考如何重构一个Unix系统。这个系统是由C++写的,重构的思路是希望能够抽取出一些公共的东西,作为Core。另外一些东西做成Plug-in。这样以后如果客户的规范更新了,我们只需要增加修改Plug-in就好了,Core的部分不需要再修改。
要满足这个要求,有些基本的原则:
[1] Core里面所有的东西不能依赖Plug-in里面。这种依赖可以理解为Build(Compiler + Link)Core里面模块的时候,不需要用到Plug-in里面的头文件或者object文件;
[2] 部署的时候,可以不用重新部署Core。只需要Update Plug-in,就可以获得新功能。
[3] 现有代码最好能够最大程度的复用。
因为这些要求,对C++设计编码在大型系统中的使用,有了一些新的体会和认识。为了简化问题,我抽象了一个简单的例子。
有这样一个需求: 给定一个整数数组,我们需要对这个数组作些运算,这个运算现在看到的有求和,求积,未来可以增加运算规则,譬如求最大值,最小值,平均值等等。现在不能预测未来需要那些运算,系统需要扩展的能力。
首先来看看运用面向对象和工厂模式给出的初始设计。
我们有一个ICalc的借口,接口里唯一一个需要Override的函数是: virtual void calc(int *p, size_t size, int *result) = 0 这里说唯一是从设计角度考虑的,因为它本身的析构函数也是纯虚函数。另一方面,这个函数没有要求是const的,这个不会影响我们的讨论。因为现有系统中并没有一致的使用const,所以我们设计这个接口的时候,没有把const考虑进去。
接下来,我们设计两个实体类CalcAdd和CalcMul,当然它们都Override了calc这个方法。我们又设计了一个CalcFactory,它有个静态函数getCalc(string calcType)。通过calcType的值,返回正确的Calc类型。
目录结构
// include/ICalc.h
#ifndef _ICALC_H_
#define _ICALC_H_
#include <stdlib.h>
class ICalc
{
public:
virtual void calc(int *start, size_t size, int *result) = 0;
virtual ~ICalc() = 0;
};
#endif //_ICALC_H_
//include/CalcFactory.h
#ifndef _CALCFACTORY_H_
#define _CALCFACTORY_H_
#include <string>
#include "ICalc.h"
class CalcFactory
{
public:
static ICalc* getCalc(std::string calcType);
};
#endif //_CALCFACTORY_H_
//include/CalcAdd.h
#ifndef _CALCADD_H_
#define _CALCADD_H_
#include "ICalc.h"
class CalcAdd : public ICalc
{
public:
virtual void calc(int *start, size_t size, int *result);
virtual ~CalcAdd();
};
#endif //_CALCADD_H_
给纯续函数提供函数体是可以的,在这种情况下也是必需的。
#include "../include/ICalc.h"
ICalc::~ICalc(){ }
// src/CalcFactory.cpp
#include "../include/CalcFactory.h"
#include "../include/CalcAdd.h"
#include "../include/CalcMul.h"
using std::string;
ICalc* CalcFactory::getCalc(string calcType)
{
if ("Add" == calcType)
{
return new CalcAdd;
}
else if ("Mul" == calcType)
{
return new CalcMul;
}
else
{
return 0;
}
}
// src/CalcAdd.cpp
#include "../include/CalcAdd.h"
void CalcAdd::calc(int *p, size_t size, int *result)
{
for (size_t i = 0 ; i < size; ++i)
{
(*result) += p[i];
}
}
CalcAdd::~CalcAdd() { }
// src/main.cpp
#include <iostream>
#include <string>
#include "../include/CalcFactory.h"
#include "../include/ICalc.h"
using namespace std;
int arr[] = { 1, 2, 3, 4, 5, 6 };
int result; // result = 0
int main(int argc,char* argv[])
{
if (argc < 2)
{
cerr << "Usage: calc Add|Mul" << endl;
return -1;
}
if (string("Add") == argv[1])
{
result = 0;
}
else if (string("Mul") == argv[1])
{
result = 1;
}
ICalc *calc = CalcFactory::getCalc(argv[1]);
if (!calc)
{
cerr << "Fail to calc." << endl;
return -2;
}
calc->calc(arr, sizeof(arr)/sizeof(int), &result);
cout << "result = " << result << endl;
delete calc;
return 0;
}
大致的程序如上述,上面没有给出CalcMul.h和CalcMul.cpp。因为这个和CalcAdd完全一致。大家可以下载v1.tar.bz2看最初设计的源代码。大家可以使用make -f Makefile.(mac|sun|lnx) all Build整个系统,最后会在bin下面生成一个可执行文件calc,运行就可以看到结果。
可以看到,这个版本其实是个不错设计的。因为如果我们要增加一个求最小值的算法,只需要设计一个CalcMin,实现ICalc接口,重新修改CalcFactory的getCalc函数,增加一个“Min”的分支,就OK了。这个设计也符合开闭原则。就是对修改开放,对整个工作流的修改是关闭的。可是,这样的设计还是有几个问题:
[1] CalcFactory.obj依赖于CalcAdd.h,CalcMul.h。如果增加一个新的算法,这个文件必须重新修改,编译,链接。
[2] calc虽然不依赖于CalcAdd.h,CalcMul.h。换言之,如果CalcAdd修改了,calc不需要重新编译,但是却需要重新链接。因为calc需要CalcAdd.o, CalcMul.o。
基于这两点,我们没有办法把CalcAdd这样的算法做成Plug-in。因为更新Plug-in,我的Core程序也需要更新。很多时候这不是一个严重的问题,但是有时候我们确实需要简单的更新Plug-in。因为如果只更新Plug-in,我们可以减少很多Core程序测试的时间和工作量。这个在大型程序带来的价值是巨大的。于是,我们继续改进我们的设计。