设计哲学:职责单一还是接口单一
今天跟康神讨论设计,他提到一个设计哲学(这个词就很让人崇拜),很是有道理。大概意思是说,在设计中,有人喜欢职责单一,所有东西分的很细,有人喜欢接口单一,保证接口稳定,这是个设计哲学问题,各有优缺。
我自己是一个极度喜欢保持职责单一的人,面对康神设计的接口单一但内部逻辑较复杂的模块,多少有点想重构的冲动。其实这两种设计哲学都没太大问题,确实得看需求来确定到底用什么。
接口单一的设计,最典型例子的就是大家都知道的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 [...]
什么是“Nabialek trick”?
现在是时候了,经过这些天的学习和尝试,我终于知道boost.spirit里谈到的Nabialek trick是什么东西了,当然应该大方的与大家分享。
这个Nabialek trick其实并不是编译期间的技巧,而是利用查表的方式将一些静态的类型推导变成简单的运行时调用,用性能损失换取编译期间的复杂,是一种相反的过程。可以说,Nabialek trick给人的感觉是完完全全的没有意义,要知道C++的template就是为了提升运行时性能而增加编译期间运算,这个反其道而行意义何在?其实说白了,意义就在于:消除模板技巧带来的疯狂的自动类型推导,以及解除对某些类型的静态绑定。前者意义在于可以提升编译速度,而且也可以避免达到模板嵌套上限(比如MSVC 7.1,最多只能嵌套256层,这已经算不错了),后者意义在于可以让复杂的表达式变得更容易匹配、更快的执行和更加灵活(所谓的将“线性非决定式”转化为“决定式”,将“或”逻辑从表达式中剔除,避免不必要回溯以加快速度),但是由于创建查找表的过程涉及到内存分配,所以后者并不能总是达到很好效果。
我们再从正面来想想Expression Templates给编程带来的困难,请看下面的示例代码(来自boost.spirit的帮助/libs/spirit/doc/techniques.html,我做了一些简化和修改):
r =
“//” >> *(anychar_p – ‘n’) >> ‘n’
| “/*” >> *(anychar_p – “*/”) >> “*/”
;
在这里,r的类型就是:
alternative<sequence<sequence<strlit<const char*>, kleene_star<difference<anychar_parser, chlit<char> > > >, chlit<char> >, sequence<sequence<
strlit<const char*>, kleene_star<difference<anychar_parser, strlit<const char*> > > >, strlit<const char*> > >
是的,我没指望任何人能够看懂……不过假如做一下的赋值:
line1 = “//” >> *(anychar_p – ‘n’) >> ‘n’;
line2 = “/*” >> *(anychar_p – “*/”) >> “*/”;
r = line1 | line2;
这样的代码应该好理解一些,但是没有本质不同。再来改改:
line1 [...]
