分离软件中易变部分和稳定部分

软件中存在本质复杂度和偶发复杂度,即对应软件中易变部分和稳定部分。对于本质复杂度,指的就是业务逻辑,这部分是无论如何消除不掉的,偶发复杂度指的是实现业务逻辑的一些实现框架、实现手法等,这些可以通过软件方法将复杂度降至最低,甚至完全消除。分清楚软件的本质复杂度和偶发复杂度,能够让精力聚焦在有价值的事情上。

注:上述指的消除不是不写代码,而是让代码实现不随着业务需求的变更而频繁变更。

下面通过一个例子来看下如何分离软件中易变部分和稳定部分,特别是对于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 )逼到流程之外,然后作为参数在流程框架中进行传递。
使用框架后的代码,相对于原来平铺直叙的代码,可读性上差了一些,这也是很多开发团队诟病和抵触框架的一个原因。但是,如果框架设计的足够稳定,那么框架部分是不应该需要经常修改变化的,开发人员更多需要理解框架是如何使用的即可。我曾经在一个使用极致框架的项目中工作过,其实理解了其设计原理,使用起来并非那么困难。框架使用学习的工程和守护的过程,也是团队成员软件能力提升的一个过程。

你可能感兴趣的:(分离软件中易变部分和稳定部分)