所遇不良设计(二)
Table of Contents
软件设计就好比造房子。当一个软件bug重重,扩展很困难时,还不如推到了,再重新做一个。在博客上,看到陌陌的劲舞手游,因为在设计上不到位,导致整个手游重新开发; 同时负责这个游戏的项目经理也随之离职。
1 文件的层叠
现在我们在用eclipse开发软件,在切换文件的时候很麻烦; 因为一个系统模块的文件分布到不同的文件目录里面去,就像珍宝,用层层的布裹着,你需要一层层的打开,才能知其庐山真面目。以下是用tree工具打印的目录树:
├── project
│ ├── include
│ │ ├── module1
│ │ │ └── submod1
│ │ ├── module2
│ │ └── module3
│ └── src
│ ├── module1
│ ├── module2
│ └── module3
这是我用tree打印的工程文件目录,在这个工程.h和.cpp文件用include和src来分开。如果你要开发一个系统模块,你需要在include的dir1、dir2、dir3和src的dir1、dir2、dir3里面分别新建.h和.cpp文件,因为一个模块又分为了几个子模块。如果你使用一种可视化开发工具中的project explorer来浏览文件,都要把头切换晕了; 要是可能的话module1下面可能还有submodule1文件夹。我觉得系统的模块的层级结构并不是特别需要用文件目录来表现,而且这样子也让Makefile写起来很麻烦; 可能有人会说可以不需要写,自动生成的,用automake。但是当要写一些跨平台的工程的时候,不仅仅在linux下面,还包括windows以及mac,我更加倾向于用CMake。曾经我下载过redis的源码,这是一个用C语言开发的NOSQL数据库,其所有的.c、.h文件都放到了src目录下,简单极了,切换文件很方便; 而且其就只有一个Makefile文件。 在这里,我并不是批评那些在一个工程里面放有目录的人,我是希望尽量能减少目录,尽量把头文件和源文件放到一起; 因为我们在开发项目的时候,同时要兼顾头文件和源文件。我们可以这样子:
├── project │ ├── mod1 │ │ ├── submod1.cpp │ │ ├── submod1.h │ │ ├── submod2.cpp │ │ └── submod2.h │ ├── mod2 │ └── mod3
2 命名的规范
我见过好多搞C/C++的人,比较讨厌Java。首先Java隐藏了指针了,搞得这些同胞们,对人生失去了乐趣; 其次Java的效率问题,觉得始终没有C/C++高; 再者觉得搞Java有点脑残,太简单,垃圾什么的都不用管,有点乱扔垃圾的意思,不文明。但是我要说的时,Java有许多好的设计模式。在文件命名方面,Java做得比较好。java文件名必须要和里面的类必须同名,我觉得C++也应该保持这样的方式,虽然编译器没有强制你这样做,但是很有必要,这样子通过文件名称就能看到里面装的是什么类。其次Java用到了包,在Java编程思想里面包名和你的网络名称一样,这样便于网络传输,不会出现包的冲突,因为网络地址是唯一的。C++里面有namespace,用namespace封装自己的类库,可以防止自己的类库,和第三方的库冲突,不过namespace的名称必须是标识符。 我见过的工程里面,每个程序员都有自己的编程规范,公司的编程规范没有很好的去执行。首先,我觉得好多公司的编程规范不够详细,模棱两可; 其次好多开发人员可能之前在其他公司呆过,保留了上一家公司的编程习惯。如果公司能够确定了好的编码规范,大家还是愿意遵守的。这里我比较推荐google的C/C++编程规范,网上都有电子文档的。还有就是用脚本语言检测程序定期检查编写的代码,是否有不符合规定编码的规范,这些都需要项目经理去认真的执行的。
3 宏的臃肿
3.1 宏的好处
宏的替换是无与伦比,没有其他可以替代的。我看到宏在好多地方合理有效的利用,其能有效的减少繁琐的重复的代码、解决跨平台系统系统API不兼容的问题(通过系统的宏定义,来判断是什么操作系统,从而来调用相应的系统API,这些事情都在编译的时候就完成了)
3.1.1 抛出异常
为了调试代码,我要抛出异常,异常的内容包括文件名、行号、错误码:
throw new Exception(FILE, LINE, error);
每次我都要加入__FILE__、__LINE__ 比较啰嗦, 使用宏更加方便:
#define THROW_EXCEPTION(error) \ throw new Exception(__FILE__, __LINE__, error)
这里不能用函数来替代的,要是那样,_FILE__, _LINE_就显示的是所用函数的文件和行,造成异常的错误提示有误。
3.1.2 解决系统的跨平台问题。
1 #ifdef _WIN32 2 xxxx 3 #elif _LINUX 4 xxxx 5 … 6 #else 7 xxxx 8 #endif
Poco库是一个优秀的C++库,跨平台。其结构也很清晰,可以去了解,里面有很多这样设计的。
3.2 宏的坏处
但是宏在编译的时候要替换代码,会生成很多重复的代码。如果你的宏要是很长,会很糟糕。有人可能说宏很快,我觉得为了追求那点速度,而违背代码的简约设计,是没有说服力的。而且宏也带来调试的不方便(在宏里面下断点无效)。在代码中尽量减少宏的使用比较好。大多时候,我们可以用函数来代替宏,这样能够复用,即使宏很短。
3.2.1 使用带有const类型的常量
当我们定义常量的时候并且常量不是在编译的时候使用,可以使用带const的类型变量声明宏,例如const int len = 10。因为这样便于我们确定常量的类型。可能宏定义整形的时候,默认就是整形。要是宏定义一个小于255的数字,依旧使用整型,如果是一个浮点型,其默认是单精度,除非你把小数点加长,让float不能识别,这时候编译器才将其当做double了。其实用define确定常量,让常量的类型产生了歧义,让类型不够明了。
3.2.2 使用工厂模式来通过名字获取对象
在C++里面没有反射机制(即通过类名来创建类),用宏是很容易做到的。如下: //根据名字来定义一个类:
1 //根据名字来定义一个类: 2 #define DEF_CLASS(class_name) \ 3 class class_name##Mgr {\ 4 public:\ 5 static class_name##Mgr* CreateClass() {\ 6 return new class_name##Mgr;\ 7 }\ 8 ....\ 9 } 10 11 //创建类 12 #define CREATE_CLASS(class_name) \ 13 class_name##Mgr::CreateClass()
以上使用##宏来连接类名,根据名字定义一个以Mgr结尾的类,以及创建对象。要实现这样的方式,我们只能用宏。其实我们可以其他的方法,即利用多态的特性来模拟这种特性。
class Base { public: .... }; class Derive:public Base{ public: .... }; class Factory { public: //注册各个类 void RegisterClass(const string& classname, Base* pBase) { classes[classname] = pBase; } //创建 Base* CreateClass(const string& classname) { if find return classes[classname]; retur NULL; } .... private: hash_map<std::string, Base*> classes; }
工厂模式可以根据类名来取出对象来,我觉得已经够用了。虽然实际上没有根据类名来自由的创建类,你还得手动的去创建各个类,我们只是在做存和取的工作。工厂模式一般被用来总领整个框架,就像是写作文的提纲似的。 使用宏不够好,同时也可能造成代码难以阅读; 应当尽量避免宏。