首页 > 程序人生 > 谈谈程序的封装

谈谈程序的封装

2011年12月11日 发表评论 阅读评论

近半年来,团队加入了不少新同学,陆陆续续也看过不少新同学写的代码,一个比较大的感受是,新同学程序的逻辑上问题不会太大,但是代码的封装方面,还是有明显的不足。具体主要包括以下几个方面,一是命名,包括类、函数、变量等的,一个好的名字,可以让读程序的人一看到它就大概知道它是做什么用的;二是函数、类的划分,哪些实体应该抽象为一个类,这个实体的那些功能该抽象成函数,这是程序设计中最为重要的内容;三是线程安全,在多线程环境下,线程安全是一个很重要的话题,很多时候因为封装的不合理,导致使用上的不方便,更为严重的是造成死锁等的问题。下面就分别从这三个方面,谈一些自己的看法,希望能够对初入职场的新同学有所帮助。

第一个方面是命名,这个是最为基本的内容。可以这样讲,把写程序比做写文章,命名对于程序的作用,就相当于措词对于文章,好的措词对于一篇好的文章的作用,不用我再多说。关于程序中的命名,主要包括三种,一是类型名,二是函数名,三是变量名。类型名在C++中应对的包括class, struct, enum等,它们都是抽象出来的一个实体,因此它们的名字最好是一个名词,或者名词词组,比如PCManager,它是一个PC管理器类型。函数通常是对某个操作的封装,如果是类成员函数,它就是该类所封装的实体的一个特定的功能,因此它们的名字最好是一个动词,或者动词短语,比如PCManager有扫描磁盘的功能,我们可以取名为ScanDisk();普通的全局函数的命名基本规则和类成员函数差不多,但有时候可能需要加上一个名词主体,来说明这个函数是作用在这个主体上。举个例子,我们有个File类,它的打开函数应该是Open(),而如果我们要封装一个全局的打开文件的函数,我们直接取Open()可能还不够直观,如果叫OpenFile()就比较直观了,这时候说明Open的是File,而不是其它的什么。这里又引出另外一个问题,前面提到的File类,它的打开函数,如果叫OpenFile(),就显得有点画蛇添足了,所以这里也需要注意一下。关于变量的命名,这个具体情况得具体对待,不过有一个总的原则,不要吝惜字母,尽量用全拼,比如ScanDisk()函数中记录扫描文件数的变量叫scan_file_count,OpenFile()打开的文件名变量叫file_name,这样就都比较直观。这里比较容易犯的问题是,用一些单词、字母的缩写,导致程序的可读性变差,记住千万不要吝惜几个字母。另外,还有一些对于类型名,函数名,变量名都适用的通用规则,就是尽量不要使用不常用的缩写,名字长一些问题不会太大,直观为主。

第二个方面的是函数、类的划分。这个方面要做好,确实需要一定的积累,我在这里也很难几句话就把所有的问题讲得清清楚楚,但是可以分享一些基本的原则性的东西,给大家引导一个正确的方向,更多的需要大家多多阅读一些前人优秀的代码来悟。通常来讲,类封装的是一个实体,一个具有若干相关功能的实体,可以抽象成为一个类,类的成员函数是对这个实体的功能的封装。通常判断一个类封装的是否合理,可以想象把这个类对应到客观存在的实体,看它是否是一个“四不像”,如果是,那就说明我们的封装是不合理的。比如我们有一个类叫FileAndDiskUtility,封装的是对File和Disk相关的操作的工具函数,想象一下,在客观世界中,有File,Disk这两类实体,并不存在一个FileAndDisk这样的怪物,从这个角度上讲,上面的封装是不合理的,比较好的做法是分别封装一个FileUtility类和一个DiskUtility类,如果它们有一些共同的功能,可以抽象出一个公共的Utility类,然后FileUtility和DiskUtility从Utility继承。其实关于类的封装,还是有很多技巧的,建议感兴趣的同学好好研读一下GOF的设计模式。关于函数的封装,有一个最为基本的原则,就是每个函数尽可能做独立的事情,不要一个函数完成所有的任务,这个是新同学最容易犯的毛病。这里有一个很简洁的方法来判断你的函数的封装是否是合理,看一下你的一个函数的行数,通常建议一个函数控制在100行以内,如果实在觉得100行有点困难的话,可以放宽到200行,如果你的函数的代码行数超过这个建议的行数,那么你就要考虑,你这个函数是不是做的事情过多了,可以考虑拆分成多个子函数了。另外,还有一个原则,就是看你的代码中有没有对同一段类似的代码重复使用的,如果有的话,这样的部分最好是封装成一个函数,通过传的参数来抵消差异性。

第三个方面是线程安全。前面讲的两个方面,做得好与坏,主要的差别在代码的结构、可读性方面,关于线程安全的方面,做得好与坏,差别不仅仅在代码的结构、可读性,如果做得不好,会导致程序死锁等的问题,这就是设计的缺陷了。通常,我们是通过对数据加锁来实现线程安全的。前段时间看一些新同学的代码,比较常见的情况的,把锁加在数据操作外面,由使用者来控制,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class File
{
public:
    void Write(...)    {}
 
    std::mutex  m_mutex;
    std::string m_name;
};
 
File file("test.txt");
// thread 1
file.m_mutex.Lock();
file.Write(...);
file.m_mutex.Unlock();
 
// thread 2
file.m_mutex.Lock();
file.Write(...);
file.m_mutex.Unlock();

上面是一个多线程写同一个文件的例子,需要使用者在外面显式的Lock/Unlock,这样,如果使用者在线程1处Lock了,但是忘了Unlock, 那么在线程2处就写入不了。这是一个不好的设计的例子。通常,我们建议把锁封装到数据上,对使用者透明,请看下面改进后的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class File
{
public:
    void Write(...)
    {
        m_mutex.Lock();
        // ...
        m_mutex.Unlock();
    }
 
    std::mutex  m_mutex;
    std::string m_name;
};
 
File file("test.txt");
// thread 1
file.Write(...);
 
// thread 2
file.Write(...);

上面的例子中,我们把锁加到了文件的操作里面,用户在外面直接使用相关的操作即可,这个类本身是一个线程安全的类,这样就比较好的解决了上面的问题。

  1. 2011年12月13日16:42 | #1

    gof还是不错的,就是看起来没有java的head first的设计模式读起来有趣

  2. 侧面bt
    2012年4月8日00:10 | #2

    类的封装有个简单的原则,以相对集中的数据和该数据上的操作封装为一个类,切忌不要以功能来划分类。
    线程加锁的时候只锁最基本的操作,锁的范围不要太大,能不加锁就不加。

  3. 2012年4月8日10:44 | #3

    恩,兄台提到的两点,都是非常重要的。@侧面bt

  4. rainxu
    2012年5月31日21:35 | #4

    受益匪浅~

  1. 本文目前尚无任何 trackbacks 和 pingbacks.
您必须在 登录 后才能发布评论.