设计哲学:职责单一还是接口单一

今天跟康神讨论设计,他提到一个设计哲学(这个词就很让人崇拜),很是有道理。大概意思是说,在设计中,有人喜欢职责单一,所有东西分的很细,有人喜欢接口单一,保证接口稳定,这是个设计哲学问题,各有优缺。
我自己是一个极度喜欢保持职责单一的人,面对康神设计的接口单一但内部逻辑较复杂的模块,多少有点想重构的冲动。其实这两种设计哲学都没太大问题,确实得看需求来确定到底用什么。
接口单一的设计,最典型例子的就是大家都知道的printf,这个东西博大精深,可以组合打印任意格式的文字,如果C标准委员会乐意,还可以通过扩展格式字符串来实现更多的打印选项,同时保证100%向下兼容。还有很多接口,它们通过传递一个结构来传递参数,这个结构拥有众多成员,每次调用接口时只用其中一部分。这些接口也能像printf一样易扩展,康神设计的就是后面这种接口。
这样设计的好处是保持整个系统接口的统一和稳定,这对于一个大型工业系统来说至关重要,因为牵一发动全身,改变任何一个通用接口都会付出很大代价。但它的缺点也十分明显,主要体现是效率不高和模块职责不明。
可以想象,printf的运行效率并不高,对于%以及后面参数的解析是在运行时完成的,每次运行都会在解析格式字符串上花时间。传结构的缺点是内存使用效率不高,并且随着业务逻辑不断变复杂,结构会大到不能接受的地步,最终肯定在结构中设置一个类似于格式字符串或者表达式的东西,逐渐转变成printf的设计模式。
同时,由于接口过于强大,在接口背后隐藏的逻辑也必定复杂无比。想想解析格式字符串或解析结构时该需要多少烦人的if…else,就知道要实现这种设计将会写出多少逻辑复杂的代码,会产生多少的bad smell,给后来维护的人造成不少困扰,甚至会引入不少潜在的错误。
反观职责单一的设计,没有接口单一设计的缺点,但劣势也很明显,就是没有统一的接口,使得整个系统模块之间的调用方式始终难以稳定。如果整个系统由一个人开发还好,如果多人协作,则会大大降低开发效率,会将大量时间用在沟通接口和解决联调问题上面。公平的说,职责单一的设计也可以用facade模式来提供统一接口,但是facade模式的实现代码又会变得和接口统一时一样,没有根本改变,只是一个折中和权衡。
那是否说明“完美”的职责单一的设计不适合大型开发呢?也不一定,因为我们有C++和无比变态的模板技巧。
比如我们可以设计出一个这样的print,功能类似于printf。注意,这个函数设计成只在print的时候才输出,expression类不会造成任何输出。

pattern p = builder() << “uid: ” << uid << ” uname: ” << name;
uid = 1000;
name = “realdodo”;
print(p); // 打印出”uid: 1000 uname: realdodo”

如果熟悉boost.spirit的朋友肯定觉得这个代码很眼熟,没办法,谁要我是这个库的忠实粉丝呢。在这里,pattern_builder类只是一个模板生成器,而不进行任何计算(不同于sstream这样的类),它用<<运算符针对各种类型生成各种特化的模板类,类似于一个嵌套的pair<>。这些模板类层层嵌套,但是pattern类可以轻松搞定,方法就是设计一个concrete_pattern类。

struct concrete_pattern_base
{
    concrete_pattern_base() {}
    virtual ~concrete_pattern_base() {}
    virtual string to_string() const = 0;
};

tempate <typename builder>
struct concrete_pattern : public concrete_pattern_base
{
    builder builder_;
    concrete_pattern(const builder & b)
        : builder_(b)
    {}

    virtual string to_string() const
    {
        // 用模板特化的方法实现to_string()方法,这里略去
    }
};

这样,pattern的实现就非常简单。

class pattern
{
    concrete_pattern_base * pattern_;

public:
    template <typename builder>
pattern(const builder [...]