软件编码规范
1. 代码风格
- 代码风格是帮助开发者更加高效的开发以及后期维修更新迭代的基础,良好的代码风格可以提升团队的开发效率。
1.1 为什么需要管理代码风格
- 团队开发不是个人开发,进行个人开发时代码风格并不重要,因为,除了你自己基本无他人会欣赏你的代码。但是,团队开发就大不相同了,你写的代码大概率会被不同的人欣赏,正所谓一千个读者,一千个哈姆雷特。如果每个人的代码风格都各不相同,就会给阅读者造成相当大的困难。
- 好的代码风格能降低你犯错误的概率,因为在书写代码时,你可能会对写过的代码多看一遍,检查其风格,检查风格的同时大概率会检查出低等的错误。
- 善待强迫症患者,对大多数强迫症来说,看风格迥异的代码是种煎熬,在他们心中,可能不止一次想过改变他人的代码风格。
1.2 代码风格规范
- c/c++:华为C++语言编程规范
- python:Google Python风格指南
2. 正交性
- “正交性”是从几何学中借来的术语。如果两条直线相交成直角,它们就是正交的。用向量术语说,这两条直线互不依赖。沿着某一条直线移动,你投影到另一条直线上的位置不变。
- 在计算机技术中,该术语用千表示某种不相互依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的:你可以改动界面,而不影响数据库;更换数据库,而不用改动界面。
- 当任何系统的各组件互相高度依赖时,就不再有局部修正 (local fix) 这样的事情。
- 如果组件是相互隔离的,你就知道你能够改变其中之一,而不用担心其余组件。只要你不改变组件的外部接口,你就可以放心:不会造成波及整个系统的问题。
- 编写正交的系统,得到两个主要好处——提高生产率与降低风险。
2.1 提高生产率
- 改动得以局部化,所以开发时间和测试时间得以降低。
- 正交的途径还能够促进复用,如果组件具有明确而具体的、良好定义的责任,就可以用其最初的实现者未曾想象过的方式,把它们与新组件组合在一起。
- 如果你对正交的组件进行组合,生产率会有相当微妙的提高,假定某个组件做M件事情,而另一个组件做N件事情,如果它们是正交的,而你把它们组合在一起,结果就能做 MxN 件事情。但是,如果这两个组件是非正交的,它们就会重叠,结果能做的事情就更少。通过组合正交的组件,你的每一份努力都能得到更多的功能。
2.2 降低风险
- 正交的途径能降低任何开发中固有的风险。
- 有问题的代码区域被隔离开来,如果某个模块有毛病,它不大可能把病症扩散到系统的其余部分,要把它切掉,换成健康的新模块也更容易。
- 正交系统很可能能得到更好的测试,因为设计测试、并针对其组件运行测试更容易。
2.3 设计
- 分层的途径是设计正交系统的强大方式。因为每层都只使用在其下面的层次提供的抽象,在改动底层实现、而又不影响其他代码方面,你拥有极大的灵活性。分层也降低了模块间依赖关系失控的风险。
- 对于正交设计,有一种简单的测试方法。一旦设计好组件,问问你自己:如果我显著地改变某个特定功能背后的需求,有多少模块会受影响?在正交系统中,答案应该是“一个“(在现实中,这有点天真.除非你非常幸运 ,否则大多数实际的需求变动郝会影响系统中的多个功能)。移动 GUI 面板上的按钮,不应该要求改动数据库。
2.4 工具箱与库
- 在你引入第三方工具箱和库时,要注意保持系统的正交性。要明智地选择技术。
- 在引入某个工具箱时(甚至是来自你们团队其他成员的库),问问你自己,它是否会迫使你对代码进行不必要的改动。如果它要求你以一种特殊的方式创建或访问对象,那么它就不是正交的。让这样的细节与代码隔离具有额外的好处 它使得你在以后更容易更换供应商。
2.5 编码
- 让你的代码保持解耦。编写"羞怯"的代码——就是没有必要地向其他模块暴露任何事情、也不依赖其他模块的实现的模块。
- 避免使用全局数据,每当你的代码引用全局数据时,它都把自己与共享该数据的其他组件绑在了一起。一般而言,如果你把所需的任何语境 (context) 显式地传入模块,你的代码就会更易于理解和维护。
- 避免编写相似的函数。
- 养成不断地批判自己的代码的习惯。
2.6 测试
- 正交地设计和实现的系统也更易于测试,因为系统的各组件间的交互是形式化的和有限的,更多的系统测试可以在单个的模块级进行。
- 构建单元浏试本身是对正交性的一项有趣测试,要构建和链接某个单元测试,都需要什么? 只是为了编译或链接某个测试,你是否就必须把系统其余的很大一部分拽进来?如果是这样,你已经发现了一个没有很好地解除与系统其余部分耦合的模块。
2.7 练习
-
你在编写一个叫做split的函数,其用途是把输人行拆分为字段。下面的两个函数中,哪一个是更为正交的设计?
int Split1(File *fp) {} int Split2(char *str) {} // Split2,它专注于自己的任务,拆分输入行,同时忽略像输人行来自何处这样的细节, // 这不仅使代码更易于开发,也使得代码更为灵活, Split2拆分的 // 行可以来自文件、可以由另外的例程生成、也可以通过环境传入
3. 断言式编程
-
如果它不可能发生 用断言确保它不会发生。
-
无论何时你发现 自己在思考”但那当然不可能发生”,增加代码检查它。最容易的办法是使用断言。在大多数 C 和 C++ 实现中,你都能找到某种形式的检查布尔条件的 assert 或_assert宏。如果传入你的过程的指针决不应该是 NULL, 那么就检查它:
void WriteString(cbar *str) { assert(str ! = NULL); ... }
-
不要用断言代替真正的错误处理。断言检查的是决不应该发生的事情。
3.1 让断言开着
- 关于断言的常见误解:断言给代码增加了一些开销。因为它们检查的是决不应该发生的事情,所以只会由代码中的 bug 触发.一旦代码经过了测试并发布出去,它们就不再需要存在,应该被关闭,以使代码运行得更快,断言是一种调试设施。
- 这里有两个明显错误的假定,首先,他们假定测试能找到所有的 bug。现实的清况是,对于任何复杂的程序,你甚至不大可能测试你的代码执行路径的排列数的极小一部分。其次,别忘了你的程序运行在一个危险的世界上,在测试过程中,老鼠可能不会噬咬通信电缆、某个玩游戏的人不会秏尽内存、日志文件不会塞满硬盘,这些事情可能会在你的程序运行在实际工作环境中时发生。
- 即使你确实有性能问题,也只关闭那些真的有很大影响的断言。
4. 怎样配平资源
- 只要在编程,我们都要管理资源:内存、事务、线程、文件、定时器——所有数量有限的事物。大多数时候,资源使用遵循一种可预测的模式:你分配资源,使用它,然后解除其分配。
4.1 嵌套的分配
- 以与资源分配的次序相反的次序解除资源的分配。这样,如果一个资源含有对另一个资源的引用,你就不会造成资源被遗弃。
- 在代码的不同地方分配同一组资源时,总是以相同的次序分配它们。这将降低发生死锁的可能性。(如果进程A申请了 resource 1, 并正要申请 resource2,而进程B 申请了 resource2, 并试图获得 resource 1, 这两个进程就会永远等待下去。)
- 不管我们在使用的是何种资源——事务、内存、文件、线程、窗口——基本的模式都适用:无论是谁分配的资源,它都应该负责解除该资源的分配。
5. 得墨忒(te四声)耳法则
- 把你的代码组织成最小组织单位(模块),并限制它们之间的交互,如果随后出于折中必须替换某个模块,其他模块仍能够继续工作。
5.1 使耦合减至最少
-
假定你在改建你的房子,或是从头修建一所房子,典型的安排涉及到找一位 “总承包人” 你雇用承包人来完成工作,但承包人可能会、也可能不会亲自进行建造;他可能会把工作分包给好几个子承包人。但作为客户,你不用直接与这些子承包人打交道,总承包人会替你承担那些让人头疼的事情。
-
在软件中应遵循同样的模型。当我们要求某个对象完成特定服务时。我们想要它替我们完成该服务,我们不希望这个对象给我们一个第三方对象。我们必须对其加以处理才能获得所需服务。
-
假定你在编写一个类,生成科学记录仪数据图。你的数据记录仪分散在世界各地;每一个记录仪对象都含有一个地点对象,给出其位置及时区。你想要让你的用户选择记录仪,绘制其数据,并标上正确的时区,你可以编写:
void PlotDate(Date d, Selection s) { TimeZome tz = s.GetRecorder().GetLocation().GetTimeZone(); }
-
但现在绘制例程不必要地与三个类耦合在起——selection、recorder、 location。这种编码风格极大地增加了我们的类所依赖的类的数目。这为何是一件坏事?因为它增加了系统别的地方的一个无关改动影响你的代码的风险。例如,如果 Fred 对 location 做出改动,使它不再直接包含 TimeZone, 你也必须改动你的代码。
-
应该直接要求提供你所需的东西,而不是自行“挖通”调用层次:
void PlotDate(Date d, TimeZone tz)
-
对象间直接的横贯关系有可能很快带来依赖关系的组合爆炸。
-
有许多不必要的依赖关系的系统非常难以维护(而且很昂贵),往往高度地不稳定。
5.3 练习
-
根据得墨忒耳法则,确定所示方法调用是否允许。
void ProcessTransaction(BankAccount acct, int) { Person *who; Money amt; amt.SetValue(l23.45); acct.SetBalance(amt); who = acct.GetOwner(); MarkWorkflow(who->name(), SET_BALANCE); } // ProcessTransaction 拥有 amt — 它是在栈上创建的。 acct 是传入的, // 所以既可以调用 SetValue, 也可以调用 SetBalance 。但 ProcessTransaction // 不拥有 who, 所以调用 who->name 违反了法则 得墨忒耳法则建议把这行代码改写成: // MarkWorkflow(acct.name(), SET_BALANCE); // ProcessTransaction 没有必要了解是 BankAccount 中的哪个子对象待有账户名——这一 // 结构知识不应通过 BankAccount 显露出来。相反,我们请求 BankAccount 提供账 // 户名。它知道自己把账户名放在何处(也许是在 person 中、在 business 中)
6. 时间耦合
- 作为软件自身的一种设计要素,时间有两个方面对我们很重要:并发(事情在同一时间发生)和次序(事情在时间中的相对位置)。
- 我们在编程时,通常并没有把这两个方面放在心上,当编写程序时,事情往往是线性的,那是大多数人的思考方式一一总是先做这个,然后再做那个,但这样思考会带来时间耦合:在时间上的耦合。方法 A 必须总是在方法 B 之前调用。同时只能运行一个报告;在接收到按钮点击之前,你必须等待屏幕重画。“嘀”必须在“咯”之前发生。
- 需要容许并发,并考虑解除任何时间或次序上的依赖。
6.1 总是为并发进行设计
- 编写线性代码,我们很容易做出一些假定,把我们引向不整洁的编程。但并发迫使你仔细地对事情进行思考——这不再是你一个入的舞会,因为事情现在可能会在“同一时间”发生,你可能会突然看到某些基于时间的依赖关系。
- 首先,必须对任何全局或静态变量加以保护,使其免于并发访问,现在也许是问问你自己、 你最初为何需要全局变量的好时候。
- 此外,不管调用的次序是什么,你都要确保你给出的是一致的状态信息。例如,何时查询你的对象的状态才是有效的,如果你的对象在某些调用之间处在无效状态,你也许就是在依赖一个巧合:没有人会在那个时间点调用你的对象。
- 对并发和时序依赖进行思考还能够引导你设计更整洁的接口。
7. 避免靠巧合编程
- 传统智慧认为,项目一旦进入编码阶段,工作主要就是机械地把设计转换为可执行语句,然而这种态度正是许多程序丑陋、低效、结构糟糕、不可维护和完全错误的最大一个原因。
- 编码不是机械工作,每一分钟都需要做出决策——如果要让所得的程序享有长久,无误和富有生产力的“一生”,就必须对这些决策进行仔细的思考和判断。
- 不主动思考他们的代码的开发者是在靠巧合编程——代码也许能工作,却没有特别的理由说明它们为何能工作。
- 避免靠巧合编程——依靠运气和偶然的成功,而要深思熟虑地编程。
7.1 深思熟虑地编程
- 总是意识到你在做什么。
- 不要盲目地编程。试图构建你不完全理解的应用,或是使用你不熟悉的技术,就是希望自己被巧合误导。
- 按照计划行事,不管计划是在你的头脑中,在鸡尾酒餐巾的背面,还是在某个 CASE 生成的墙那么大的输出结果上。
- 依靠可靠的事物,不要依靠巧合或假定。如果你无法说出各种特定情形的区别,就假定是最坏的。
- 不要只是测试你的代码,还要测试你的假定。
- 为你的工作划分优先级,把时间花在重要的方面;很有可能,它们是最难的部分,如果你的基本原则或基础设施不正确,再花哨的铃声和口哨也是没有用的。
- 不要做历史的奴隶。不要让已有的代码支配将来的代码。如果不再适用,所有的代码都可被替换。即使是在一个程序中,也不要让你已经做完的事情约束你下一步要做的事情。
8. 易于测试的代码
- 你编写的所有软件都将进行测试——如果不是由你和你们团队测试,那就要由最终用户测试——所以你最好计划好对其进行彻底的测试。
- 从一开始就把可测试性 (testability) 构建进软件中,并且在把各个部分连接在一起之前对每个部分进行彻底的测试。
- 软件的单元测试是对模块进行演练的代码,在典型情况下,单元测试将建立某种人工环境,然后调用被测试模块中的例程,然后,它根据已知的值,或是同一测试先前返回的结果(回归测试),对返回的结果进行检查。
- 当你设计模块时,你应该既设计模块,也设计测试该模块的代码。
- 对于小型项目,可以把模块的单元测试嵌入在模块自身里,对于更大的项目,可以把每个测试都放进子目录。但是不管是哪种方法,要记住,如果你不容易找到它,也就不会使用它。
- 通过使测试代码易于找到,你给使用你代码的开发者提供两样无价的资源:
- 一些例子,说明怎样使用你的模块的所有功能。
- 用以构建回归测试、以验证未来对代码的任何改动是否正确的一种手段。
- 每个测试都应该通过内存错误检查工具valgrind的测试。
9. 准备好再开始
- 如果你坐下来,开始敲键盘,在你的头脑里反复出现某种疑虑,要注意它。
- 倾听反复出现的疑虑——等你准备好再开始。
- 作为开发者,你在整个职业生涯中都在做同样的事情 你一直在试验各种东西,看哪些可行,哪些不可行,你一直在积累经验与智慧。当你面对一件任务时,如果你反复感觉到疑虑,或是体验到某种勉强,要注意它,你可能无法准确地指出问题所在,但给它时间,或者查询资料,或同他人一起商讨,你的疑虑很可能就会结晶成某种更坚实的东西,某种你可以处理的东西。
- 但是不要一直处于未准备好状态中,那很可能是一种拖延。