最开始写代码的时候总是拿到一个问题就捋起袖子开干,基本上就是属于边写代码边排错顺便在写代码中设计解决问题的流程,但是这样效率实在是太慢,有很大的可能就是边写边删,等同于设计出来的蹩脚的就重构了,浪费了很多时间。
其实写代码最重要的不是coding这一步,而应该是Think/Design,coding只是对设计出来方案的实现,等同于我们已经知道了Why而寻求How,所以说最重要的其实是思考和设计的过程。
目前来看,我所认为的写代码大概可以分为以下几个步骤:
第一步:也是最重要的一步,是尽可能仔细地分析要处理的问题,其实也就是如何把一个复杂的问题拆分成一个个简单容易实现的问题。写下来各个模块的用途,实现优先级,以及将它们组合到一块的方式(另一种说法也是就写伪码啦)。
第二步:使用上一步的设计先简单地通过一组参数来测试设计的逻辑是否存在问题,最好能找出几个奇葩的范例来修正第一步中可能存在的问题。
第三步:使用上两步的设计简易地搭出来一个实现的框架,这部分的主要工作是实现(逐步填充)设计中的那些小模块。
第四步:组合那些小模块为一个整体,详细测试并逐步优化各个模块。
然后是以下几个编码习惯
一、模块化
我是强烈推崇将代码模块化的,哪怕最简单地只是把一个功能封装成一个函数,也比把一坨逻辑堆在一块要强得多,而且,这样模块化编程最重要的是提升代码复用度,这样就算前两步的设计存在问题,修正逻辑上存在的问题,重构也可以用到这些小模块,可以节省很多时间。造了很多轮子之后就是面向Ctrl+C与Ctrl+V编程啦 ~(哈哈),不可否认的是扩充自己的代码库确实会提升很大效率。
模块化的意义在于降低整体的复杂度——用清晰的接口把若干简单的模块组合成一个复杂的软件。
Unix的哲学是:一个程序只做一件事,并做好。我觉得模块化也是,一个小模块只负责一件事,并能够保证处理好其分内的事。
二、尽快实现一个方案,不应过早优化
过早优化也是大弊端之一,在一个良好的设计下应当先尽快实现一个可行的方案再逐步优化,而不是期望一步到位写出来性能极好的方案(避免在前期浪费时间),所以应当在实现中逐步发现存在的问题再有针对性地优化。
在实现之初,很多人都会陷入一个优化泥潭——“这段代码怎么能这么写,效率不行,应该这样balabala…”,我觉得应该在已经快速实现了一个方案之后确定设计的逻辑没有太大的硬伤的情况下再逐步优化各个模块(因为此时设计逻辑和各个模块的作用已经可以明确确定,所以我们可以在这样的框架下修改/优化我们的代码,前提是需要保证其会正确地工作)。
其实就一句话——不要过早炫技!不要过早炫技!不要过早炫技!
三、提防依赖实现和副作用
另外,养成一个良好的编码习惯是非常重要的。其中一点是一定要警惕那些依赖于实现环境或者带有副作用的操作。比如下面这样:
1 | vector<string> args={"1","111"}; |
这样的代码是很烂的,应当竭力避免之。
在写代码时应该避免使用依赖于实现环境的特性,比如C++中class的内存布局,以及直接操作vptr等等。应该尽量保证写出来的代码是没有歧义的并且应该在各平台下都应该良好地工作。不要为了炫技而去写出来一些能跑但是看着很奇怪也很烂(难读)的代码。
其次,对于语言的各种操作符的优先级应该了然于胸的(基本功),不然看到一串操作符组合时很容易一脸懵逼。
四、避免特例化
这也是很重要的一点。有时候在写代码中会出现这样一种情况:设计的方案,大多数情况下都符合,但是总会出现几个特例不能通过测试。我认为这是编码中最让人头痛的部分,因为经常就是设计好了一种解决方案后当中总要嵌套数个if语句来检测特例,这样写出来的代码就极其丑陋。
bug通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作的代码中。应该保证代码的透明性——就是一眼能够看出来是怎么回事。所以说一个好的设计方案应该是简洁的透明的。
五、接口设计要避免标新立异
这点主要是为了吐槽我今天看到的Unreal中的FString.
在C++中STL中各个的容器的基本接口是一致的——或者说实现相同功能的成员函数的命名都是一致的。这样可以清楚地记得size()
是获取容器中元素的个数,begin()
是获取首个元素的迭代器或者swap()
交换两个容器等等,都体现了设计一致性,不会造成混乱。
当我们自订一个类(抽象类型)时,首先最应该要避免的是标新立异的接口,应该参照一些最常用的接口(最好是标准库)来命名这样会降低学习和使用成本,统一使用风格。
六、出现异常时立即退出并给出足量的错误
在C/C++中,最常见的一种建议是,将回收后的动态内存指针赋值为NULL(nullptr)
,防止对悬垂指针操作造成错误。但是这里存在一个很严重的问题——创建的动态内存已经释放了,为何还会对其进行访问?如果出现这种情况一定是设计上存在的硬伤。我是不建议将释放后的动态内存指针手动赋为NULL的,因为这样会在真正可能出错的地方迷惑到你——让你误以为不会出现错误而不去纠正设计上存在的问题。这种类似于拆东墙补西墙的方案是不值得提倡的。
所以,在测试时如果碰到非预期问题应该立即退出并给出足够多的错误信息,这样才可以真正的从设计上解决问题,而不是对一个深藏暗疾的系统修修补补。
七、列出实现优先级,优先实现核心功能
最开始在学习C++的时候自己做的一些小console demo,花费了很大的精力来处理用户可能会错误的输入——因为往往可能使用的人的用法是不规范的。那时候做东西在刚开始实现之初(获取数据)时就把所有可能造成错误的读入数据考虑到并一一排除之(其实也就是过早优化),这样浪费了很多时间。
现在觉得,在设计之初应该列出一个实现优先级,通俗的来说就是应该首先实现哪一部分,那些相比较而言是不重要的。这样可以以最快的速度实现一个方案,而不是陷入过早优化的泥潭。
另外,有必要说的一点是:好的注释负责指明一段代码应该实现什么功能(代码的意图),而代码本身负责完成该功能(完成的方式)。最好的方式是,注释的语言应该保持在一个较高层次的抽象水平,这样便于理解而无需纠结过多的技术细节。