可维护性我认为主要由两个方面构成, 一是可读性, 也就是代码要能让人看懂; 二是可调试性, 出了问题可以很快的找到原因. 市面上讲设计的书很多, 但大部分侧重于灵活性和可复用性, 比如面向对象设计和设计模式等. 灵活和可复用并没有什么错, 但我认为可维护要更重要一些, 试想如果一个模块非常灵活并被大量复用, 却不可维护, 岂不是不出问题则已, 一出就是灾难性的吗?
再看 C++ 语言, C++ 是一个提供了太多特性的语言, 每一件事情都可以用好几种可选的特性去实现, 但我们应该选择哪一种呢? 显然应该是最合适, 最实用的, 而不是最新最酷的.
从 05 年末到 09 年初, 我得到了一个非常难得的机会: 把一个项目做了两遍. 第一遍的时候用了很多 C++ 的高级特性, 也有意无意的引入了一个设计模式的思想, 项目也还成功, 但后续的维护却越来越难. 后来在做第二遍的时候, 受《UNIX 编程艺术》等的影响(也就是在这时候我成了把 C++ 当 C 用的程序员), 开始学习用最简单直接的方法解决问题. 而结果也相当好, 不光项目成功, 整个系统的可维护性也不错. 而且虽然设计时完全没考虑面向对象, 设计模式, 但最终的系统却又带着这些东西的影子, 只是实现方法和书上写的不完全一样(呵呵, 吹大了 欢迎大家对这一段扔几个西红柿鸡蛋之类的).
现在由于工作变动开始维护另一个系统, 这是一个很 C++, 很面向对象, 也很设计模式的系统, 可是维护起来却无比困难, 出问题后简直无从下手. 所以更体会到了可维护性的重要, 进而想到应该把自己的这点经验总结一下, 写出来. 目前总共有四五篇的题材, 都是很细节的问题, 希望自己能坚持写完. 我的方法也许不那么漂亮, 但应该还实用, 毕竟也算是真刀真枪的实战中总结出来的.
因为一发出来就被批了个体无完肤, 所以加上了这些文字, 说明一下背景. 不太喜欢口水仗, 后续的批评我将不再回复, 因为软件开发是工程而不是艺术, 工程讲究实用, 没有绝对的对和错, 一切都应该根据实际情况具体问题具体分析. 我写的东西只是供大家参考, 没有也无法强迫大家一定要用.
下面开始正题. 如果我们要实现一个类, 用于从流式缓冲区读出数据(典型应用是网络通讯中的数据包分析), 你会用下面哪种实现呢(错误处理用的是异常, 与主题关系不大, 故不详述)?
// A实现
class CBufferReaderA
{
...
template<class T>
CBufferReaderA& operator>>( T& v );
...
};
// B实现
class CBufferReaderB
{
...
char ReadChar();
short ReadShort();
int ReadInt();
...
};
我想不少人会选择 A, 因为看起来更酷一些, 而且只写了一个模板函数就可以处理一大堆数据类型了, 但实际上, 如果从可维护性和实用性来说, B 却更好一点. 下面就来对比分析一下.
1. 像 cin/cout 一样, A 实现能把多个操作写在一起.
CBufferReaderA br;
br >> a >> b >> c >> d;
这一点 B 是作不到的, 因为它的返回值被用来返回实际读取的内容了. 可是当我们调试 A 实现支持的那一串代码时, 问题就出现了, 整个代码虽然是好几个函数调用, 但一下就执行过去了, 根本没法看到中间结果(VC 是这样, 其他调试器不清楚). 为了避免这个问题, 只好把这一串操作拆成单个的, 但这样一来 A 和 B 也就没什么区别了.
2. 如果需要跳过一段数据, 需要怎么做呢? 如果用 A 实现, 肯定是类似下面的方法:
int tmp;
br >> tmp;
而 B 实现则可以直接:
br.ReadInt();
对比可见, A 实现不光多用了一个没有什么实际用处的变量, 而且多写了一行代码. 只看这一点也许没多大问题, 但如果程序很大, 类似需求很多, 它带来的混乱就不可忽视了.
3. 缓冲区中是 char
型, 但我想用 int
保存读出的数据, 应该怎么办?
// A实现:
char c;
int i;
br >> c;
i = c
// B实现:
int i = br.ReadChar();
我想 B 的优势不用我说了吧.
4. 前面说到的 A 缺点也许还不算太严重, 下面这个应该就有足够的说服力了.
// A实现:
br >> a;
br >> b;
br >> c;
br >> d;
br >> e;
// B实现:
a = br.ReadChar();
b = br.ReadShort();
c = br.ReadChar();
d = br.ReadInt();
e = br.ReadInt();
看出问题了吗? 没错, 在 B 实现中, 我们很容易的知道每步操作从缓冲区中读了多少数据. 而如果用 A 实现, 这些信息就不那么明显, 必须去检查各个变量的定义, 也许你会说 VC 里面把鼠标放上去就能看到定义, 但也别忘了一次只能看一个, 而 B 则可统观全局. 如果是个很大的程序, 那 B 的可读性和可调试性要高很多.
5. 对 B 的一个批评是暴露了实现细节, 把读了几个字节清晰的写出来了. 但我认为这恰恰是它的优点, 因为只有应该隐藏的细节才需要隐藏, 而这里, 知道读几个字节对缓冲区分析来说非常重要的, 是不应该被隐藏掉的. 无限制的隐藏细节只会给自己找麻烦. 打个比方, 把路的细节隐藏起来, 方法之一是把眼睛蒙上, 我们又怎么走路呢?
6. B 相对于 A 也有一个缺点, 就是 A 可以通过重定义 >>
运算符, 让自定义类型和原生类型使用看起来完全相同的方法被读出来, 但一般来说, 这一点的艺术性远大于实用性, 而且考虑到前面所有的缺点, 它不足以成为我们选择 A 的理由.
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于