T上西北

软件开发规则

最后版本: rev 5
最后更新: 2017-04-21

设计

  1. 注重软件设计。
  2. High Available需要考虑。
  3. Geo Redundancy也需要考虑,如果设计web的话。
  4. 考虑软件的需求,有无可能复杂化,定制化,这决定了软件架构,
  5. 将需求详细化,但实现时简单化。
  6. 数据库主从结构,Replication,数据一直等。
  7. web的安全性方面是必须考虑的。
  8. 软件都是逐步迭代的,为了让后期迭代的成本更小,起点最好高一些。
  9. 事先定义软件的版本管理。
  10. 站在使用者的角度去思考。
  11. 解放思想,考虑问题站在高一点的位置。
  12. 防止过度设计。


Coding

  1. 一个文件代码不要太多,1K行左右,一个函数也不同太多,100行左右,如果太多的话,就应该考虑如何去分拆了。
  2. 代码一开始就要考虑模块化。
  3. 代码可重用性。
  4. 代码不能太耦合。
  5. 对于异常case务必写日志,不要认为捕获了就OK了。
  6. 良好的代码目录结构。
  7. 对于SQL等一些reserved keywords建议用大写,例如SELECT * FROM <tbl-name> WHERE field1=v AND field2 = v2
  8. 代码格式上须易于阅读,空格,空行等是必要的。Java有Java的代码规范,可以作为参考准则。
  9. 注释需要合理,不在于多,而在于精。对业务复杂的业务逻辑,加适当的注释会锦上添花。
  10. 代码格式可以利用一些专业化的工具进行检测,例如Sonar,JSLint等。
  11. 写代码是软件开发里非常简单的事情,但是做好不容易。基础非常重要。态度谦逊,借鉴优秀人员的代码,避免自己的不足。
  12. 一开始就要写好,不要以后面可以重构当借口。
  13. 对于web开发,安全性非常重要。可以写完后用安全扫描工具(例如AppScan等)进行扫描。掌握经验,在后面的coding种尽量去避免。
  14. 尽量将问题或需求想的全面,实现时根据实际情况可以取舍。
  15. 设计准备好了才可以写代码。
  16. 对于异常case多处理,不要认为发生的可能性不大。
  17. 尽量让代码的适应性强一些,考虑一些配置来适应未来的需求。
  18. 代码用Maven,Gradle等进行build
  19. 拼接SQL语句需要考虑的SQL安全性,SQL 的 保留关键字或字符,建议用PreparedStatement去完成。

Log

  1. 清晰的日志易于做troubleshooting
  2. 清晰的日志易于team之间的交流和学习。
  3. 日志注意拼写错误,英文合适。
  4. 日志需要将业务分步描述清楚。
  5. 建议developers多做Ops的工作,以便于了解到底什么样的日志才是优美的。
  6. 对于正常的业务也许打log,对于异常的case务必打log。常遇见的情况是正常的业务逻辑少,只有一些exception的case 才打。导致正常业务出问题没法进行troubleshooting。
  7. log的级别。什么样的信息适合什么样的log level,取决于业务。

SCM(SVN/GIT)

  1. 保证仓库里的代码是可以编译通过的。
  2. 代码尽量每天提交。
  3. 一次只提交一个功能或者bug fix代码。
  4. 提交代码时,写上comment描述当次修改。
  5. 如果是bug fix,在comment里注明bug id,如果是功能,注明requirement id。
  6. 安装合适的工具,例如Trac, Phabricator,以及Bitbucket等

Code Review

  1. 强烈建议team之间内部相互review。
  2. 条件允许的话,只有代码进行code review过了,才可以commit到仓库里。
  3. 开Code Review会议前,相关人员提前review一遍。在会议上只提问。
  4. Code Review时只提及修改的部分。
  5. Code Review可以用一些工具,例如Phabricator,ReviewBoard,以及Bitbucket等。

Test

  1. 写单元测试(Unit Test), 可以简单理解白盒测试。
  2. 写功能测试(Functional Test),可以简单黑盒测试。
  3. 一般项目都会用daily build,将单元测试和功能测试加入daily build的task中。
  4. 代码覆盖率也很重要。
  5. 代码必须测试过才可以发出去(内部或者外部)
  6. Test Cases需要详尽才可以,认真写。
  7. Test Cases不是应付,不是交代,而是软件质量的一小步。
  8. 用测试框架去Test case,。例如,在Java里,很多人有个习惯是在class里增加一个main方法去驱动测试某个函数。

 

修改历史记录

  1. 2017-03-08, 第一版起稿。
  2. 2017-03-09, 增加测试部分,以及部分coding规则。
  3. 2017-3-13,增加设计章节。增加测试部分(>=6),log部分(>=6),代码部分(>=14)等
  4. 2017-3-16,增加设计部分(>10)
  5. 2017-4-21, 增加coding 的18,19,修改Log的6

文档

在工作中,一份好的文档对于project起到非常积极的作用,过多的益处我就不再赘述,很多书籍已经说明。

但是,一份质量差的文档呢?恐怕会适得其反。

很多时候,每个项目都有固定的流程,所以写文档就是为了走流程这样一个目的。无论你文档写的再好,也是没有人看的。写的好也就罢了,但是写的不好呢?我想是没有人看的,这样的情况下不写也罢。

很多时候,每个公司都有自己的文档规范。所以新员工不知道是怎么回事情,如果不培训的话,不要期望在短时间内写出好的文档。但是这样的公司还是挺多的。这样的情况下,不写也罢。

很多时候,即使你知道文档格式,但是文档内容有时也不太清楚,所以也不要太指望写出好的文档,只有在自己熟悉的基础上才能写出好文档。如果你不能清楚知道内容,这种情况下,不写也罢。

我倒不是反对写文档,相反,我是非常支持的。但是如何写出好文档,至少需要以下几个方面:
1. 良好的文档格式
2. 良好的写作能力
3. 良好的掌握所写内容
4. 良好的培训
5. 良好的公司氛围来写文档

我写这些,知道意味着什么吗?

Hello, Memory Leak

已经有3年没有认认真真写过C/C++程序。即使是写,也是写些小程序。

因为项目的需要,与C/C++再续前缘。令人崩溃的是,我写的程序居然出现内存泄漏,直接吐血。以前,也有一次出现过内存泄漏问题,记得当时写的是一个monitor工具,一直运行在机器上,有意思的是每次执行泄漏4个字节。原因后来找到了,都是CString惹的祸啊。

这次的情况和上次不一样,没那么简单,一直用到其他库,二是因为自己对这门语言开始生疏了,造成了很大的问题。今天一口气将其解决掉,经验分享如下。

如何知道内存泄漏?

我是用VS.NET 2003开发的,该工具挺好的,当我调试时然后关闭应用程序,在output窗口就会显示,例如,

Detected memory leaks!
Dumping objects ->
{10381} normal block at 0x01C76F40, 4 bytes long.
Data: <  t > EC 9C 74 00
{9443} normal block at 0x01C37D68, 4 bytes long.
Data: <  t > EC 9C 74 00
Object dump complete.

说实话,上面提示只能告诉你内存泄漏存在,而且泄漏多少,但是没有告诉你在哪里泄漏。还好,一个开源的小工具能够帮助我们定位到具体位置。这个工具就是Visual Leak Detector , 至于如何使用,网上文章特多,在这里我还是简要说明一下。

  1. 将include文件放在系统的include下,如果使用VS,那就放在VS的include下。
  2. 将lib文件放系统的lib下,如果使用VS,就放在VS的lib下。
  3. 在需要的源文件引用一下就可以了,例如#include<vld.h>
  4. 将dll放在执行文件的目录那里。

debug应用程序,然后关闭,在output下会出现:

No memory leaks detected.
‘xxx.exe’: Unloaded  c:\xxxx\Debug\dbghelp.dll’
Visual Leak Detector is now exiting.

上面是最简单的方法有个缺点,要是我不想讲include等文件放在系统目录下,而想将其跟我的工程一起放怎么办?很简单,将include,lib等文件拷到自己的project下,如果使用VS,请在设置里将include,lib将这些目录加上,然后再lib的那里加上vld.lib即可。不同的IDE不一样,也许不用加上。

当然还有很多更好的工具,不过VLD已经够用了。

如何避免?

避免内存泄漏的方法还是在于防范。

我觉得最根本的原理就是请求内存,然后释放内存。往往我们就是只请求不释放。

原则可以如下:

  1. new一个,一定要delete
  2. malloc一个,一定要free
  3. 有时候并没有new/malloc,但是还是请求了资源,这个时候也要free。
  4. 不要返回一个对象或者一个对象的指针,代替的是在参数里进行传址。
  5. 如果你非得返回一个对象或指针,那引用该函数的人一定有义务要删除。

1、2不多说了,太简单,举个很有意思的例子。CString我们都熟悉,但是用好还真不容易,new了一个一定要删除,有时候会调用GetBuffer,事后是否需要ReleaseBuffer呢?

说第3点。先举个例子:

char* val = XMLString::transcode(subNode->getFirstChild()->getNodeValue());
//do your thing here
  XMLString::release(&val);

上面val其实分配了内存,所以要在下面释放,XML::release就是干这个事情的。也许我这么说很明白,按时真到了关键时刻,可能就忘了,也许有这样的写法:

std::string msg(XMLString::transcode(subNode->getFirstChild()->getNodeValue()));

说不定我们写完了以为就没事了,但是事情往往就出现在这里。

第4点,很多人都会犯,包括我自己。

先看这样如下代码:

Class A
{
  public:
  A();
  ~A()
}
class B
{
public:
B();
~B();

A f1(){return new A();};
A* f2(){A* a = new A(); return a;};
}
B b;
A a1 = b.f1()
A* a2 = b.f2();

上面的代码就是模拟第4点写的。第一看没问题,这在C#,Java里,这种思路在普遍不过了,但是在C/C++这里需要注意,返回一个临时对象是不安全的,即使访问指针,也是不安全的。如果要变的安全,需要费劲,而且你必须遵照第5点,手动删除它。

什么是安全的使用方法?我们可以改写一下:

Class A
{
  public:
  A();
  ~A()
}
class B
{
public:
B();
~B();

int f1(A* a);
}
B b;
A* a1 =new A();
b.f2(a1);

这样做就安全许多。因为f1将指会存入a1指向的地址那里。

其他

为了避免内存泄漏,有个比较好的方法就是RAII(Resource Acquisition Is Initialization),一般用在类里,思想就是在析构函数里将资源释放掉,即使在使用的过程中忘了删除,最后对象销毁时也会释放的。举个例子:

#include  
#include  
class file 
{ 
public: 
    file (const char* filename) 
        : file_(std::fopen(filename, "w+")) 
    { 
        if (!file_) 
            throw std::runtime_error("file open failure"); 
    } 
    ~file() 
    { 
        if (0 != std::fclose(file_)) // failed to flush latest changes? 
        { 
            // handle it 
        } 
    } 
    void write (const char* str) 
    { 
        if (EOF == std::fputs(str, file_)) 
            throw std::runtime_error("file write failure"); 
    } 
private: 
    std::FILE* file_; 
    // prevent copying and assignment; not implemented 
    file (const file &); 
    file & operator= (const file &); 
};

上面~file()里,也会把file_干掉的,即使字啊write里没有释放。

程序员是很懒的,所以有时候不会主动释放掉,更不用说粗心大意的人了,即使是谨慎的人,也不会时时刻刻去关注。即使是,后期维护人员难道知道处处到要释放吗?不能保证啊。

上面是方法之一,接下来就是只能指针,auto_ptr,当这个指针销毁时,其对象也销毁。看看下面的代码:

using namespace std;

class MyClass {

public:

  MyClass() {} // nothing

  ~MyClass() {} // nothing

  void myFunc() {} // nothing

};

int main() {

  auto_ptr<MyClass> ptr1(new MyClass), ptr2;

  ptr2 = ptr1;

  ptr2->myFunc();

  MyClass* ptr = ptr2.get();

  ptr->myFunc();

  return 0;

}

如果ptr1销毁后,那么在new MyClass()也会销毁。

关于智能指针我不多说了,毕竟我也不是特别精通啊。

暂时就写这么多,以后在更新吧。