软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

正确理解Less is Better

在编码时,我们一般都遵守“Less is Better”教条。按照代码行数,大部分时候也是对的。但是,对整个系统而言,并不是所有的场景都满足“Less is Better”。敏捷宣言中的“简单设计”不是没有设计,编码之前进行适当的软件架构设计是非常有必要的。

1. 先看一个故事

在我指导C++桌面软件做Web服务化改造的时候,自己参照boost::serialization做一个对象与JSON互转库。C++没有像Java那样的反射机制,每个对象必需实现其成员serialize()函数,每个属性必需对应一行代码ar & MAKE_NVP(myMember),如下:

class MyClass {
    friend class boost::serialization::access;
    OtherClass myMember;

    template<class Archive>
    void serialize(Archive & ar, const unsigned int version) {
        ar & MAKE_NVP(myMember);
    }
};

1.1. 清晰流程的例子1

我们一个大的业务组下面有3个小组,我指导其中的两组上Web。那时我也只看过敏捷软件开发原则与模式、重构这些书籍,还没有深入到软件架构设计。我指导这两组的代码流程都是这样的:

// 例子:易懂的流程
Error XXXRequestHandler(const string &inputJson, string &outputJson) {
    // STEP 1: 获取输入参数
    InputXXXParam input;
    parser::fromJson(inputJson, input);

    // STEP 2: 加载所需数据到DC

    // STEP 3: 业务处理
    ...

    // STEP 4: 组装前端所需的数据
    OutputXXXParam output;
    ...

    // STEP 5: 转换成输出JSON
    outputJson = parser::toJson(output, outputJson);
    return ERROR_OK;
}

class DataCenter {};
class XXXCoreBusiness {};
class XXXBusiness {};

STEP 2的DC部分,包含业务模型、数据加载等,Handler中只需调用少量函数。STEP 3业务处理部分,主要是调用相对稳定的业务规则核心层。在InputXXXParam、OutputXXXParam中,只进行数据转换与JSON转换。大家都按照这个模式去实现更多的请求处理,沟通非常方便,一切进展比较顺利。

1.2. 有瑕疵的例子2

另一小组也由一位C++技能也比较强的同事带领上Web,其中用到了PIMPL模式、RAII机制等。我已经到前端组之后,回去帮修改两个问题与优化JSON转换性能时,发现其中的代码有些问题。其代码流程大概如下:

// 不好的例子
class OutputXXXParam {
    friend class boost::serialization::access;
    DataModel1 data1;

    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        // 该函数不满足“单一职责原则”
        SomeThing st;
        data1 = st.doFunc();
        ar & MAKE_NVP(data1);

        DataModel2 data2;
        ...
        ar & MAKE_NVP(data2);
    }
};

class SomeThing {
public:
    SomeThing() {
        LoadData();
    }

    doFunc() {
        ...
    }
};

class XXXDataCenter {};
class XXXCoreBusiness {};

Error XXXRequestHandler(const string &inputJson, string &outputJson) {
    // STEP 1: 获取输入参数
    InputXXXParam input;
    parser::fromJson(inputJson, input);

    // STEP ...

    // STEP ...
    OutputXXXParam output;
    output.setInput(input); // 可能是这样传入参的

    // STEP end: 转换成输出JSON
    outputJson = parser::toJson(output, outputJson);
    return ERROR_OK;
}

刚开始跟踪这样代码的时候,见到serialize()函数里面还干了一些与序列化无关的事情,非常吃惊!

1.3. 小结

对比上面的两个例子,例子1的边界比例子2划分清晰,没有把不同的功能柔和到一块,更加满足单一职责原则。

例子1跨越边界时(输入/输出与业务模型之间的转换),需要单独函数做数据转换,总代码行数比例子2的多,但是它有拥抱变化的灵活性。

在我未提交离职之前,去帮忙优化JSON转换性能,例子1小组的负责人跟我聊到这领域的代码思路比较清晰,说明我以前给他们打的基础还不错,我感到比较欣慰。

在我提交离职之后,例子2小组的新负责人也跟我聊了很多,也想让我跟他再合作一把,把这领域的软件问题解决掉;但是,那时的我心已不在此,他们的软件问题应该不止我看到的这一点,要解决掉大概需要一年的时间,只能跟他聊一下“业务专家与软件专家的关系,业务专家如何也能够成为软件专家”。

2. 满足Less is Better的场景

在设计高内聚低耦合的系统时,我们必需遵守5大设计原则。在满足5大设计原则的前提下,以下场景满足“Less is Better”:

  • 单个函数的代码量。
  • 单个类的代码量。
  • 单个文件的代码量。

少更好,但是也不要少到极端(比如很多函数都是一个函数一行代码),过犹不及。

3. 不满足Less is Better的场景

随着系统复杂性的增加,我们需要把系统分解成各个部分,再把它们组装到一块构成一个稳定的系统。这时,我们还需要考虑沟通成本、扩展性、复用性等,很多场景就不再是“Less is Better”,如下:

  • 项目的总代码行数。
  • 接口、类、组件的个数。
  • 软件架构设计。

只有划分出复用的组件之后,项目代码的增长率才会减小。接口、类、组件的个数,需要根据实际情况来定夺,当其混杂功能或内部的体量大到难以理解时,我们会分出新的接口、类、组件。最好在编码之前做一些软件架构设计,只是不需要详细的设计,能满足当前的需求且容易沟通即可。