Archive for the ‘design’ tag
接口的定义
忘记了是谁说的一句话了:一个好的接口的定义最主要不是为了易用,而是为了使用的时候尽量不出错。
大嘴牛深以为然。
这不,大嘴牛最近就犯了一个看起来简单的错误,假设有这样的一个接口:
void process_data(const char* ptr, size_t len);
而作为存储数据的地方使用的是标准库的string,因此很显然会这样调用 process_data(str.data(), str.size()),如果一切都正常,那自然你好我好大家好,但是错误总是无处不在的,在这个接口的设计中,对于同一个变量str,我们重复了两次,而真是这重复的两次,会使得用户出错,你可能一不小心写成了process_data(str.data(), str1.size()),而你自己却浑然不知。
有人建议直接把参数只制定一个string类型,把它重载一下,当需要直接使用string类型的时候,我们可以直接调用这个接口,而这个接口可能下面只做一件事情,那就是取出data()和size(),然后调用前一个接口。
这都是可以的设计。
肯定会有人对此不屑,觉得自己不会犯这样的错误。
但是:设计的接口不光只是给你自己使用的,在一个项目团队中,各个人员的水平参差不齐,需要估计到其他的使用感受。
后记:Ken Tompson在设计Unix系统调用的时候使用了creat这个名字,后来它在谈及后悔的设计时感慨,如果有重来一次的机会,他会把省略的e补上,因为这不符合“不易于犯错”的设计特点,人们总是习惯性的加上写成create,但是发现原来错了。于是在Go语言的设计中,我们看到了他专门提交了一个commit,把create的接口设计成如此。
再后记:你觉得memset这个接口设计的如何呢?你是不是也曾经疑惑第二个参数到底是表示字节数呢还是值呢?
阅读一个大型代码
无论你的项目的代码量有多大,其中一定有两种类型的代码:
- 一种是底层基础设施的,用来提供最基本的比如内存操作,文件操作,网络功能等等。
- 一种是在此之上的用以实现项目逻辑的代码。
如果你是在项目的后期进入到这个项目来的,那么这个时候可能由于进度问题,会没有足够的时间让你去研究第一类的代码,那么你只需要知道第一类代码的接口方式是如何的,随后直接调用即可,当以后有足够的时间可以再反过来阅读第一类的具体实现代码。
如果你从项目的一开始就是主要成员,那么恭喜你,一般来说,在这种情况下,你对整个项目会比较了解,对于为什么设计成这样比较清楚,因此当出现问题的时候更加容易定位问题。
消息流的方向
- 哪些消息的产生只能在某个模块产生。
- 对于需要进行资源分配的消息更是要慎重,需要制定分配在那个模块出现,而解构在哪个模块进行,一般比较合适的做法是将对于同一种资源的分配和释放放在同一个模块中。
- 模块之间有没有层次性,哪些模块永远处于一个消费者、被动的地步。
…需要有一个从进程管理模块到进程间通信模块的信息通信链,具体的做法就是当进程管理模块在感知到某个进程收到信号的时候先发消息到虚拟内存管理模块,然后由虚拟内存管理模块再发送到进程间通信模块。上述只是粗线条的描述,细节就是当虚拟内存管理模块发送到进程间通信模块的时候,不能直接使用send函数发送消息,而是必须先要利用notify函数通知这个进程间通信模块,使得该模块了解到当前进程的状态发生了改变,因为notify的内在机制是非阻塞的,而send则有可能阻塞,因而当两个进程互相发送消息时,就有可能导致死锁的发生。这里就需要有一个从进程管理模块到进程间通信模块的信息通信链,具体的做法就是当进程管理模块在感知到某个进程收到信号的时候先发消息到虚拟内存管理模块,然后由虚拟内存管理模块再发送到进程间通信模块。上述只是粗线条的描述,细节就是当虚拟内存管理模块发送到进程间通信模块的时候,不能直接使用send函数发送消息,而是必须先要利用notify函数通知这个进程间通信模块,使得该模块了解到当前进程的状态发生了改变,因为notify的内在机制是非阻塞的,而send则有可能阻塞,因而当两个进程互相发送消息时,就有可能导致死锁的发生。
这里还有一个需要注意的地方,因为进程间通信模块是动态启动的,所以它的endpoint无法预先获知,当然,它的endpoint值可以通过数据存储管理模块来获得。按照最早的实现,虚拟内存管理模块在收到进程管理模块的时候先去查询数据存储管理服务器,但问题恰恰出在这里:进程管理模块,虚拟内存管理模块,数据存储管理模块这三个模块之间会导致死锁。
最终的选择就是在进程管理模块中预先获知进程间通信模块的endpoint的值,然后封装成一个消息一起发送到虚拟内存管理模块,这样虚拟内存管理服务器就可以直接发送到正确的进程间通信服务器,而不需要再去查询数据存储管理服务器。进程间通信模块在获得了notify消息之后,反过来查询虚拟内存管理服务器,获得正确的进程将要退出的消息之后将这些进程从等待的队列上解除。
这里其实说明了消息传递方向的重要性,消息流动的主题方向是从用户进程到库函数,再到文件系统,虚拟内存模块,最终如果有机会的话被传递到底层的内核中。如下图所示:
如果两个部分在消息传递上产生了产生了互相依赖的情况,那么就会产生回环,就可能会在系统的运行过程中产生死锁。
类的设计
在软件工程中,一个理想的类的设计应该是“高耦合,低内聚”的。但现实和理想是有差别的,有时候把一大堆东西都放在一个主要的类中会更加方便。其中一部分的内容是用来完成功能A,而另外一部分的内容是用来完成功能B的,这样就需要把所有的AB功能的接口都塞到这个类的整个接口中,十分的臃肿。
这个时候可以将A,B功能所需要的功能实现放到另外一个类中,比如叫做ACtrlClass,BCtrlClass,其中都不存放任何的数据,而只是静态的函数,具体的数据存放仍然存放到原来的类中,随后将这两个新的类都作为原来类的友类,这样就可以直接读取原来类中的私有成员。
有人会说为什么不直接使用全局的函数,但是利用名字空间把各个不同的功能给包起来,这样的话,从 syntax 上也可以达到类似的形式,比如 ACtrlClass::Foo(); ,你直接看这个 ACtrlClass 其实你无法确定这到底是一个类还是一个名字空间。但是直接函数的话,就不能直接私有成员,除非你将接口设计的小巧便捷,否则很多时候会变的很庞大,和完成预定的目标没有帮助;而使用前面类的方式,因为可以通过友类而化解这个问题的产生。
当然还有其他的一些方法。