舒服的代码和不舒服的代码,差距是怎样的?

范德成
2017年9月19日
 
转载本人回答:
谢邀。混血王子和匿名用户已经回答得很好了。再补充几点:
  1. 函数的接口注释(业务目的、输入输出参数、环境影响、前置后置条件),请参考2001版MSDN Library中的Platform SDK文档。其他的API文档我虽只读过一些,如.NET Framework、Dynamics AX、ABAP等,但没见过比MSDN 2001 Platform SDK文档更详尽的了(ABAP的语言文档详细程度仅次于它吧)。只要是公开的函数,文档写成这样是好习惯。私有函数如果业务复杂,也应该这样写接口注释。这么好的注释我可以说并不是很多人愿意写,甚至是大部分人不愿写得这么详细。但是,日后维护的时候,有这样详细注释的代码才能比较好地被快速理解和修改。当然,宏观设计还是要靠设计文档。另见本人文章《程序员的注释之道》
  2. 代码的逻辑。这个看个人功力了。数学素养、计算机科学的知识就在此体现。完成同一个目的,代码写得好不好会不一样的。功能是否完善?比如写一个文本编辑器,是否支持各种不同的文本编码?编码转换失败不会崩溃,并给用户显示提示信息?一行文字很长时的编辑体验好不好?查找替换是否支持正则表达式?有没有撤销功能?有没有“撤销树”功能?可否适用于编写程序代码?可以加语法高亮和输入补全等插件吗?等等,不赘述。
  3. 程序的组织。有没有重复代码,重复代码多不多,复杂算法有没有注释。类的组织好不好(高内聚、低耦合,除非必要才用高耦合设计)。类之间的依赖关系是否合理(底层类调用上层了吗?这个可以用回调或观察者模式来消除)。扇入扇出、可重用性等等。面向对象的设计模式适当使用,以满足需求为目的,而不要在非必要时使用太复杂的设计模式。
  4. (基于进化理念的)TDD和(基于团队协作的)PP。UT(单元测试)是持续集成的前提,而TDD则是在UT的基础上加上了测试用例的进化过程(部分UT=>红=>全绿=>加UT=>红=>全绿...=>直到实现完成、UT测试强度足够并全绿),这对于复杂业务尤其有效。但TDD不是完全取代code review的。TDD可以作为质量保证,但代码的可读性、可维护性还是要通过code review和重构来实现。测试强度方面,不要只用TDD来做简单的happy path测试,要知道TDD用得好时,可以做复杂的、测试强度很高的test case。学习一下传统的人工测试方法论对于写TDD的测试也很有用。
  5. 补充一点,UT对于有内存保护的高级语言(Java、C#、Python、JavaScript等)很有用,但对于内存不安全的语言(C、C++、汇编)来说,还需要在编写代码时使用结构化异常处理,对整个代码做人工复查来避免读取未初始化变量、内存越界、野指针、双重释放、释放后又使用等情况。并要在单元测试中加上模拟内存分配失败的情况。最后和内存检查工具(比如Application Verifier或Valgrind)一起测试,以便检查出常见的内存误用情况。另外,多线程程序无法用单元测试来模拟竞争条件,只能使用代码复查或者原型模型(比如把普通代码用coroutine写出来,加以单元测试)。
 
Discussion with Lincoln Yu about safeguarding wheels with TDD
 
Decheng "Robbie" Fan
2017-09-14
 
Yeah, making one's own wheels is a virtue of a programmer! I do believe in this. That's why I'm now looking into TDD as a part of the solution. You know for personal programs it takes a lot of time to test it. TDD can't replace code review--code quality itself still needs to be ensured manually--but it can replace a lot of the test work. This would help a lot when enhancing the wheels with new features.
 
Regarding how to use TDD effectively:
 
1. Do pair programming only for critical code. I like TDD but do not like pair programming with regards to final quality (not just because the programming efficiency is lower, but also because pair programming is a dynamic process, which involves little code review). Critical code can be worked through pair programming because feedback is important for it;
1.1 Do pair revision. Write code separately with your colleague, and finally review each other's code. Write review feedback, and do pair revision (update the code) based on the review feedback;
2. The "evolution" concept of TDD is valuable. It helps you grow the test cases and the implementation code gradually, accumulating a lot of valuable test cases and letting you think about the implementation logic from easy to difficult, and from incomplete to complete. Sometimes this evolution requires you to throw away a whole incomplete implementation, because it's incompatible with the complete solution. But it has already made you think more, which is good. Write unit tests--tests red--update the implementation--tests green--write more unit tests--repeat until implementation done. After all unit tests are built, add more test cases by using traditional techniques (Book: How We Test Software at Microsoft) to ensure better test strength, and perform a complete code review (implementation should be reviewed carefully; unit tests should be gone through. Reference: http://www.fandecheng.com/personal/interests/programming/program-error-prev.htm);
3. I have used some mock frameworks and have discovered that mock frameworks (aka mocks) are more evil than mock classes (aka fakes). Mock classes can be debugged and are flexible, while mock frameworks are rigid, introducing higher coupling between implementation and tests and can't be debugged. One step further, single layer mock classes are OK if we just use them to isolate the implementation details of the depended-on components (DOCs, including libraries and frameworks), but they are not enough if we want to do integration test. So I would do two things:
3.1 For unit tests that do use single layer mock classes, perform complete DOC tests first (manually or automatically) to verify their behavior, and record their behavior down (as comments in the unit tests). For example, write experiment code to test NTFS behavior, and write down how NTFS behaves. Then build the mock classes based on the behavior observed;
3.2 For unit tests that do integration test, it means that we need to implement "deep and real" mock classes. These mock classes are very like real implementations of the DOCs. The only difference is that they use virtualized storage (such as a database dedicated to unit test rather than a real one) and environment (such as a virtual network server dedicated to unit test, but not a real one). Such virtualized storage and environment are automatically initialized before each test case, so that the repeatability of the unit tests can be kept. For example, to do integration test of your application with FAT32, implement an in-memory version of FAT32 and perform the test.
 
编程世界中的“脚手架”——未完成代码白盒单元测试
 
范德成
2017年10月26日
 
什么叫“未完成代码白盒单元测试”呢?是这样的。当你写一个可测试的方法时,首先,它可测试,不硬编码地依赖外部环境。其次,它没有写完,从头开始往后写的过程中,可能由于逻辑比较复杂,或者包含复杂的算法,因此一次正确写完会有些困难。此时,“未完成代码白盒单元测试”就可以有所帮助。
 
以传统的、无单元测试的代码编写方式来看,这种复杂逻辑本身的质量保证可以通过两种方式来完成。一是代码复查。二是用调试器观察或者打日志。而有了“未完成代码白盒单元测试”后,代码复查将变得可选(建议写完整个方法后再复查),而调试器和打日志则完全没有必要了。
 
那么怎么来做呢?你可能已经猜到了,就是对写好的那部分代码产生的中间结果,用单元测试加以验证。等整个方法都写完后,这部分针对中间结果的单元测试都已经绿了,针对整个方法的单元测试就可以开始建立了。注意,由于要提取中间结果,很可能需要把一些数据保存在某些地方,比如一些公有变量里面,以允许单元测试来读取,所以实际上是破坏了程序本该有的封装的,并且那些保存语句对于功能来说也是多余的。
 
由于上述原因,当针对整个方法的单元测试开始建立以后,保存中间结果的语句和变量,以及“未完成代码白盒单元测试”就都可以被删除了。这和建筑领域的“脚手架”作用类似——在建房子的过程中用到,建完房子就拆除。总之,就我的经验来看,“脚手架”技术对于包含复杂逻辑(主要是复杂算法)的方法很有用。希望它对你也有帮助。
 

返回“编程天地”