什么是代码改动
在软件项目中都存在一个生命周期,无论周期长短,都会涉及到代码改动,不管是对以前缺陷的修复,还是在敏捷开发中(每一个故事的迭代),都会或多或少地产生代码修改。
为什么会发生代码改动
代码修改其实是很常见的,但代码修改却是要尽量避免的,我们可以使用极限思维,一种是无比兼容的代码,它不需要任何修改,即可满足业务需求,这种代码在我们生活中是可见的,如果把业务代码不算作真正的代码,而是业务逻辑流程,那么脚本驱动的服务框架,其底层是用高效的原生语言编写(例如:C++),其使用通用设计模型和抽象方法,这种底层很少改动,上层的脚本业务受之驱动,那么这种代码就是几乎不需要修改的,需要改的就是业务脚本(无代码方案也是一种极限思维);另外一种就是前者相反,它会把业务参杂到代码的各个角落,代码没有什么抽象层,这种代码如果对业务不熟,对于代码的阅读性就会下降,业务的修改都会直接引发代码修改,可维护性以及复用性都不高。
我们都不想修改代码,因为修改会花费成本,时间和精力,而我们之所以这么做,却仅仅为了弥补以前做的不足?那么为何不站在前期的角度为后期做点着想,做好预防性工作,更利于项目的健康发展,后期维护成本更低。
如何进行代码改动
我们可以尽量避免改动代码,但是又不得不改动代码,因此如何把代码改好,减少它的不足也可以提升项目代码质量。
对于如何写优雅的代码一直都是有迹可循的,计算机行业存在大量相关的资料文档。
代码常识性规则
常识性的问题,一般人都会知道,但是对于代码逻辑来说是无关紧要的,因为它其实和代码没有关系,但是有了它可以减少代码中犯错误。
代码对齐
一般来说,用空格键代替Tab键,可以明显改善此点,尤其在团队开发的时候,这点更加明显(代码对其,对于使用高级编辑器的来说,他可以自动格式化代码,几乎不用操心了)。
代码注释
代码注释也是代码格式的一种,合理的代码注释可以便于开发人员对于代码的理解,从而提高效率。如今各自格式的代码注释花样百出,支持生成丰富的api文档,等等,都是便于后期对于代码的查看与理解。
代码逻辑需要注意的地方
代码逻辑流程,不同的人因为不同的习惯而写出的代码可谓天差地别,但是有很多不错的代码逻辑是值得借鉴的,代码逻辑流程和上面的格式规则不同,它会直接影响到二进制代码的运行质量。
条件语句
很多的时候,代码中对于条件的判断可能很简单,程序也许只关心一种条件,所以很多时候会忽略掉了ELSE情形,这是一种不好的习惯,代码质量也不会很高。
如下这种代码:
1 | void make_node(const std::unordered_map<std::string>& poll, std::unordered_map<std::string>& result) |
这种代码就是典型的ELSE缺失,它将导致冗余执行,代码运行质量会下降。
我们可以进行适当的修改:
1 | void make_node(const std::unordered_map<std::string>& poll, std::unordered_map<std::string>& result) |
冗余执行即可消除,这是让所有的ELSE按默认往下走的意思,这种风格通常用来判断一个函数是否满足条件以执行时,很常用:
1 | bool check_condition(int flags, std::vector<int>& result) |
循环语句
循环语句是用来迭代执行的,但是程序需要在有限的时间里得到确定性的结果,所以循环语句不恰当的使用也会导致低质量的代码,如下:
1 | void maybe_infinite_loop(int flags, std::vector<int>& result) |
上述的几行代码,就有可能是缺陷代码,这里假设check_flags
和check_flags_v1
不是基于状态模式的,那么代码就存在无限循环的可能。
有穷循环
对于上述的代码可以设计一个简单的状态转换表,使每条转化路径都是从开始状态走到终结状态,这样引入状态模式就变成这样:
1 | void finite_loop(int flags, std::vector<int>& result) |
这样的逻辑会存在确定性终结点。
尽量不要打破循环层次
我们先看一个头都变大的代码段:
1 | void bad_loop(int flags) |
这种代码的可维护性特别差,为了理清代码逻辑,需要花更多的时间和精力,所以,还是那条规则,尽量避免使用goto
语句,避免复杂混乱的流程。
改善后的代码:
1 | void bad_loop(int flags) |
这样改良后的代码不仅容易理解,而且复杂度明显优化,代码运行效率更高。
对于深层次的循环代码,不宜跨层次跳转,这里还是避免使用
goto
语句,否则很容易打破逻辑层次,使代码变得混乱。
重复代码块
有时代码中存在多个小段代码片段,功能上相差无几(很可以是前人Ctrl C+Ctrl V,修修改改留下的),其使用频率也很高,这种代码可以进一步优化,使其成为一个功能性的调用函数。
用状态标记来跟踪执行
状态位标记实际上就是上面的状态模式的设计方法(用于确定性的for循环),其实很多时候,并不是循环才会用状态模式,任何情形都可以用,但是对于复杂的执行流程情形,状态模式更易抽象流程,也便于调试和排查,它带来的各种好处是显而易见的,也是各种优秀架构设计对于复杂流程处理经常选择该模式的原因。
代码哲学
代码方面,如果代码可以满足如下几点,那么就可以说这是优秀的代码:
- 通过所有测试
- 没有重复代码
- 体现系统的全部设计理念
- 包含尽量少的实体(例如:类型,函数)
命名方式
- 蛇形命名
- 大驼峰命名
- 小驼峰命名
统一的变量命名方式,可以便于代码的沟通与理解,从而可以提高团队的生产效率。
代码格式问题
代码格式主要是空白字符,换行字符等等的编码或默认使用的规则,一般而言,编辑器是可以定制化这些格式的,然后团队可以共享一份编码规章,这样在同一项目中就不会产生代码格式不同而发生的显示风格差异。
代码抽象
代码抽象层面需要探讨一下,作为软件工程方面,必须理解的设计模式,不是说对所有的模式倒背如流,而是说对常用的设计模式能够理解,并知道在什么场景下要用它,它能解决什么问题。
为何要使用设计模式呢?
因为在解决同一问题时,不同的人就有可能存在不同的表达方法,在代码层表现出的差异就会更加明显,而对于团队项目来说,这样的代码要让同时几十人来维护,需要每个人去理清它的具体逻辑将会很耗时间和精力,那么为何不让团队们一起使用同种语言来交流表达呢?所以在逻辑层面就有了设计模式,这样的话,张三写的逻辑,他在表达一个外观设计模式,李四只要知道外观设计模式,看了他的代码之后便一眼明白他的逻辑,这样团队就可以节省大量的时间和精力成本,从而更加高效地投入有意义的工作价值之中。
而设计模式正是应用软件开发中常见的底层逻辑,因此作为开发首先就是需要抽象化功能逻辑到设计模式(但如果不能抽象化,一般来说就是对设计模式无法掌握)。
德墨忒耳定律: 模块使用者不应该了解对象的内部细节
单元测试
单元测试通常是用来对代码功能模块的正确性进行检验的工具,如果软件的所有功能模块都是正确的,那么软件集成后的代码也是可以方便调试的,并极有可能也是正确的。因为功能模块属于部分性问题,集成的软件属于整体性问题,如果整体性问题不满足,说明部分性问题在顺序或相互依赖上存在差错,而我们就不用再考虑模块内的事情了,只需调整功能模块间的顺序或依赖即可解决问题。
跌进规则
- 通过所有的测试
- 重构代码
- 代码不可重复性
- 表达程序员的意图
- 尽量少的类和方法数量
并发问题
在多线程系统中,会出现并发问题,并发的意思指多个运行节点对于某个资源的使用,在时间上具有不确定性。
并发和并行: 并发是指多个执行节点对单个资源使用在时间上具有不确定性,无法断定谁先谁后的问题;而并行则是说多个执行节点对于单个资源的使用,可能存在同时使用的可能。 实际上解决好并发问题,也是解决了并行的问题。
互斥与同步的问题
正是多执行节点对单个资源使用在时间序列的不确定,需要一种互斥同步使用的机制。
首先,需要知道互斥操作,它是一种排他操作,用于在使用资源的时候,资源被锁定,其他使用者无法获取使用。
操作系统里面用PV术语来表示:
1 | P() |
PV操作都是原子化的,当执行P操作后,若存已经在资源使用,P操作会被阻塞,直到资源使用完毕,V操作执行后,资源就被正式是否,阻塞在P操作的一个使用者会立即得到使用权,并继续获取/使用/释放的操作流程。
在C++中,也是用同样的方式机制:
1 | static std::unordered_map<std::string> resource; |
但C++里面还可以设计一种原子锁,这种是确定资源消耗时间是可终结的,它起到的作用就是线程挂起,使之只能一个线程消耗资源,适合资源消耗简短的逻辑,因其结构简单,效率往往比互斥锁高。
1 | static int lck = 0; |
同步问题,是从互斥问题推导出来的,他是两个互斥操作之间的同步问题,仍然采用PV术语来表达。
对于生产者而言: 1
2
3
4
5P(S1)
P(N1)
do_produce()
V(N1)
V(S2)
这里M的互斥锁是用于锁容器池的(池子发生改变时一定要加锁),当生产完一个,同时计数一个到N对应的计数锁。
对应的消费者为: 1
2
3
4
5
6P(S2)
P(N2)
resource = get()
V(N2)
V(S1)
do_consume(resource)
生产者和消费者概念
生产者和消费者是对互斥同步问题抽象而来的,而且对于资源的取用使用更通用的方式,通常为资源容器池,这样只要生产者没有把容器池放满,就一直可以存放资源,消费者也是可以在容器池未消耗完前一直可以消耗资源,提高互斥同步的效率。
可以用C++这样表示,对应生产者和消费者: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24static std::mutex producer_lock;
static std::mutex consumer_lock;
static std::mutex poll_lock;
static std::unordered_map<std::string> poll;
void produce() // 多线程生产
{
producer_lock.lock();
poll_lock.lock();
do_produce(&poll);
poll_lock.unlock();
consumer_lock.unlock();
}
void consume() // 多线程消费
{
consumer_lock.lock();
poll_lock.lock();
auto resource = get(&poll);
poll_lock.unlock();
producer_lock.unlock();
do_consume(resource);
}