软件中存在本质复杂度和偶发复杂度,即对应软件中易变部分和稳定部分。对于本质复杂度,指的就是业务逻辑,这部分是无论如何消除不掉的,偶发复杂度指的是实现业务逻辑的一些实现框架、实现手法等,这些可以通过软件方法将复杂度降至最低,甚至完全消除。分清楚软件的本质复杂度和偶发复杂度,能够让精力聚焦在有价值的事情上。
注:上述指的消除不是不写代码,而是让代码实现不随着业务需求的变更而频繁变更。
下面通过一个例子来看下如何分离软件中易变部分和稳定部分,特别是对于C语言等非面向对象语言,有些抽象设计的落地往往比较困难,本文就以C语言来做需求的实现。
这个例子可以理解为一个设备的配置过程,分为几步进行,某步执行的结果如果有错误,则停止本次配置流程,同时回退掉之前已经配置成功的所有参数。
说明:这个例子为了简单起见,省掉了很多数据的定义。另外,一些方法的返回值设计为void也是为了示例的简化。仅仅是个示例代码,对于命名不要过于苛责,同时本示例代码不保证能够编译执行。
原始的代码实现如下:
enum {
STEP1_FLAG = 0x00000001,
STEP2_FLAG = 0x00000002,
STEP3_FLAG = 0x00000004,
};
int g_procResult = 0;
int Step1(void *para)
{
int ret = 0;
// do something
if (ret == 0) { // 执行成功则记录标记位
g_procResult |= STEP1_FLAG;
}
return ret;
}
int Step2(void *para)
{
int ret = 0;
// do something
if (ret == 0) { // 执行成功则记录标记位
g_procResult |= STEP2_FLAG;
}
return ret;
}
int Step3(void *para)
{
int ret = 0;
// do something
if (ret == 0) { // 执行成功则记录标记位
g_procResult |= STEP3_FLAG;
}
return ret;
}
int Proc()
{
Para para1;
Para para2;
Para para3;
int ret = 0;
// ... do something
ret = Step1(¶1);
if (ret != 0) {
return ret;
}
#ifdef PRODUCT1
ret = Step2(¶2)
if (ret != 0) {
return ret;
}
#endif
#ifdef PRODUCT2
ret = Step3(¶3);
if (ret != 0) {
return ret;
}
#endif
return ret;
}
// 这里对执行结果的检查和回退处理,必须跟Step的调用顺序相反。
void CheckError()
{
if (g_procResult & STEP3_FLAG != 0) {
// rollback step3 cfg;
}
if (g_procResult & STEP2_FLAG != 0) {
// rollback step2 cfg;
}
if (g_procResult & STEP1_FLAG != 0) {
// rollback step1 cfg;
}
}
int main() {
int ret = 0;
ret = Proc();
if (ret != 0) {
CheckError();
}
}
上述的实现思路很容易理解,几乎是对需求平铺直叙的表达。Stepn完成第n步的配置,如果成功,则记录执行成功bit位。如果失败则结束流程。在整个Proc执行完成后,调用CheckError根据已经执行成功的步骤反序进行配置回退操作。不同产品(如PRODUCT1、PRODUCT2)配置的步骤不同,使用编译宏在配置流程函数中隔开。
上述实现存在的问题: 如果新增步骤,则需要新增Stepn的定义,并且在Proc中的合适位置对进行调用,同时需要新增Flag标记的定义,并且在CheckError中按照严格的步骤执行顺序的反序进行检查和回退操作。可以看到上述修改几乎涉及到每一处现有代码的修改,这显然不符合开闭原则。
仔细分析代码规律,每增加一个新的配置,上述固定的几个步骤(配置函数的调用以及顺序的保证等),属于偶发复杂度,可以做成一个通用的框架。真正需要新增的只是一个参数配置函数的实现,这是业务的本质复杂度。
重构后的代码实现如下:
// infra.h
#define GET_CONTEXT(baseCtxt, productCtxType, ctxtMember) \
&((container_of(baseCtxt, productCtxType, ctxt))->ctxtMember)
// rollback.h
enum {
MAX_ROLL_BACK_NUM = 10,
};
typedef struct RollBackInfo {
} RollBacKInfo;
typedef void (*RollBackFunc)(RollBackInfo* rollbakData, void* obj);
typedef struct RollBackDesc {
RollBackInfo* rollBackInfo;
RollBackFunc rollBackFunc;
} RollBackDesc;
// action1.h
typedef struct Action1Context {
} Action1Context;
typedef struct Action1RollBackInfo {
RollBacKInfo commInfo;
} Action1RollBackInfo;
// action2.h
typedef struct Action2Context {
} Action2Context;
...
// context.h
typedef struct Context {
Action1Context action1Ctxt; // 公共的放在通用上下文
int rollBackNum;
RollBackDesc rollBackDesc[MAX_ROLL_BACK_NUM];
} Context;
typedef Context* (*PrepareContext)();
// framework.h
void AddRollBack(RollBackFunc rollBackFunc, RollBackInfo* rollBackInfo, Context *context);
// action.h
typedef int (*Action)(Context* ctxt, void* obj);
// action1.c
void Action1RollBack(RollBackInfo* rollBackInfo, void* obj)
{
}
int Action1(Context* ctxt, void* obj)
{
int ret = 0;
Action1Context *action1Context = &ctxt->action1Ctxt;
// do something
if (ret == 0) { // 如果执行成功,则注册回退函数,以便其它步骤执行失败后进行回退操作。
Action1RollBackInfo rollBackInfo;
AddRollBack(Action1RollBack, (RollBackInfo*)&rollBackInfo, ctxt);
}
return ret;
}
// product1.h
typedef struct Product1Context {
Action2Context action2Ctxt;
Context ctxt; // 继承
} Product1Context;
// action2.c
void Action2RollBack(RollBackInfo* rollBackInfo, void* obj)
{
}
int Action2(Context* ctxt, void* obj)
{
int ret = 0;
Action2Context *action2Context = GET_CONTEXT(ctxt, Product1Context, action2Ctxt);
// do something
if (ret == 0) {
Action1RollBackInfo rollBackInfo;
AddRollBack(Action2RollBack, (RollBackInfo*)&rollBackInfo, ctxt);
}
return ret;
}
// action3.c
int Action3(Context* ctxt, void* obj)
{
...
}
// product1.c
// action的编排顺序要根据实际业务斟酌。
Action g_product1Actions[] = {
Action1, Action2,
};
// product1特有的参数信息,流程框架中透传。
typedef struct Product1Para {
} Product1Para;
// product2.c
Action g_product2Actions[] = {
Action1, Action3,
};
// service.h
typedef struct PorcService {
int actionNum;
Action* actions;
PrepareContext prepare;
} PorcService;
// product1Context.c
Context* Product1PrepareContext()
{
Product1Context* product1Context = (Product1Context*)malloc(sizeof(Product1Context));
product1Context->ctxt.rollBackNum = 0;
return (Context*) product1Context;
}
// product1Service.c
PorcService g_product1Service = {
.actionNum = sizeof(g_product1Actions) / sizeof(Action),
.actions = g_product1Actions,
.prepare = Product1PrepareContext,
};
// framework.c
void AddRollBack(RollBack rollBack, RollBackInfo* rollBackInfo, Context *context)
{
if (context->rollBackNum >= MAX_ROLL_BACK_NUM) {
return;
}
context->rollBackDesc[context->rollBackNum].rollBack = rollBack;
context->rollBackDesc[context->rollBackNum].rollBackInfo = rollBackInfo;
context->rollBackNum++;
}
static void ExecRollBack(Context* context, void* obj)
{
// 倒序执行回退由框架保证,用户不需要关心,减轻用户负担。
for (int i = context->rollBackNum; i >= 0; i--) {
context->rollBackDesc[i].rollBack(context->rollBackDesc[i].rollBackInfo, obj);
}
}
void ExecFrameWork(PorcService* service, void* obj)
{
int ret = 0;
Context *context = service->prepare();
for (int i = 0; i < service->actionNum; i++) {
ret = service->actions[i](context, obj);
if (ret != 0) {
ExecRollBack(context, obj);
}
}
}
// main.c
void main()
{
#ifdef PRODUCT1
Product1Para obj;
ExecFrameWork(&g_product1Service, &obj);
#endif
#ifdef PRODUCT2
Product2Para obj;
ExecFrameWork(&g_product2Service, &obj);
#endif
}
类图待补充...
C语言实现面向对象的一个要点,将数据(例如Product1Para 和Product2Para )逼到流程之外,然后作为参数在流程框架中进行传递。
使用框架后的代码,相对于原来平铺直叙的代码,可读性上差了一些,这也是很多开发团队诟病和抵触框架的一个原因。但是,如果框架设计的足够稳定,那么框架部分是不应该需要经常修改变化的,开发人员更多需要理解框架是如何使用的即可。我曾经在一个使用极致框架的项目中工作过,其实理解了其设计原理,使用起来并非那么困难。框架使用学习的工程和守护的过程,也是团队成员软件能力提升的一个过程。