0%

程序的代码修改问题

什么是代码改动

在软件项目中都存在一个生命周期,无论周期长短,都会涉及到代码改动,不管是对以前缺陷的修复,还是在敏捷开发中(每一个故事的迭代),都会或多或少地产生代码修改。

为什么会发生代码改动

代码修改其实是很常见的,但代码修改却是要尽量避免的,我们可以使用极限思维,一种是无比兼容的代码,它不需要任何修改,即可满足业务需求,这种代码在我们生活中是可见的,如果把业务代码不算作真正的代码,而是业务逻辑流程,那么脚本驱动的服务框架,其底层是用高效的原生语言编写(例如:C++),其使用通用设计模型和抽象方法,这种底层很少改动,上层的脚本业务受之驱动,那么这种代码就是几乎不需要修改的,需要改的就是业务脚本(无代码方案也是一种极限思维);另外一种就是前者相反,它会把业务参杂到代码的各个角落,代码没有什么抽象层,这种代码如果对业务不熟,对于代码的阅读性就会下降,业务的修改都会直接引发代码修改,可维护性以及复用性都不高。

我们都不想修改代码,因为修改会花费成本,时间和精力,而我们之所以这么做,却仅仅为了弥补以前做的不足?那么为何不站在前期的角度为后期做点着想,做好预防性工作,更利于项目的健康发展,后期维护成本更低。

如何进行代码改动

我们可以尽量避免改动代码,但是又不得不改动代码,因此如何把代码改好,减少它的不足也可以提升项目代码质量。

对于如何写优雅的代码一直都是有迹可循的,计算机行业存在大量相关的资料文档。

代码常识性规则

常识性的问题,一般人都会知道,但是对于代码逻辑来说是无关紧要的,因为它其实和代码没有关系,但是有了它可以减少代码中犯错误。

代码对齐

一般来说,用空格键代替Tab键,可以明显改善此点,尤其在团队开发的时候,这点更加明显(代码对其,对于使用高级编辑器的来说,他可以自动格式化代码,几乎不用操心了)。

代码注释

代码注释也是代码格式的一种,合理的代码注释可以便于开发人员对于代码的理解,从而提高效率。如今各自格式的代码注释花样百出,支持生成丰富的api文档,等等,都是便于后期对于代码的查看与理解。

代码逻辑需要注意的地方

代码逻辑流程,不同的人因为不同的习惯而写出的代码可谓天差地别,但是有很多不错的代码逻辑是值得借鉴的,代码逻辑流程和上面的格式规则不同,它会直接影响到二进制代码的运行质量。

条件语句

很多的时候,代码中对于条件的判断可能很简单,程序也许只关心一种条件,所以很多时候会忽略掉了ELSE情形,这是一种不好的习惯,代码质量也不会很高。

如下这种代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void make_node(const std::unordered_map<std::string>& poll, std::unordered_map<std::string>& result)
{
if (is_wanted(poll, 0)) {
store_result(result, load_item(poll, 0));
}

if (is_wanted(poll, 1)) {
store_result(result, load_item(poll, 1));
}

if (is_wanted(poll, 2)) {
store_result(result, load_item(poll, 2));
}
}

这种代码就是典型的ELSE缺失,它将导致冗余执行,代码运行质量会下降。

我们可以进行适当的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void make_node(const std::unordered_map<std::string>& poll, std::unordered_map<std::string>& result)
{
if (is_wanted(poll, 0)) {
store_result(result, load_item(poll, 0));
return;
}

if (is_wanted(poll, 1)) {
store_result(result, load_item(poll, 1));
return;
}

if (is_wanted(poll, 2)) {
store_result(result, load_item(poll, 2));
return;
}
}

冗余执行即可消除,这是让所有的ELSE按默认往下走的意思,这种风格通常用来判断一个函数是否满足条件以执行时,很常用:

1
2
3
4
5
6
7
8
9
10
11
12
bool check_condition(int flags, std::vector<int>& result)
{
if (flags == 0) return false;

if (flags & 0xa0 && flags & 0x0c) return false;

if (flags & 0x80) return update_result_v1(result);

if (flags & 0x02) return update_result_v2(result);

return update_result(result);
}

循环语句

循环语句是用来迭代执行的,但是程序需要在有限的时间里得到确定性的结果,所以循环语句不恰当的使用也会导致低质量的代码,如下:

1
2
3
4
5
6
7
8
void maybe_infinite_loop(int flags, std::vector<int>& result)
{
for (;;) {
if (check_flags(&flags)) break;
if (check_flags_v1(&flags)) continue;
store_result(flags, result);
}
}

上述的几行代码,就有可能是缺陷代码,这里假设check_flagscheck_flags_v1不是基于状态模式的,那么代码就存在无限循环的可能。

有穷循环

对于上述的代码可以设计一个简单的状态转换表,使每条转化路径都是从开始状态走到终结状态,这样引入状态模式就变成这样:

1
2
3
4
5
6
7
8
void finite_loop(int flags, std::vector<int>& result)
{
for (auto it = state_start(&flags); it != state_end(&flags); it++) {
if (check_flags(&flags)) break;
if (check_flags_v1(&flags)) continue;
store_result(flags, result);
}
}

这样的逻辑会存在确定性终结点。

尽量不要打破循环层次

我们先看一个头都变大的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void bad_loop(int flags)
{
for (;;) {
if (check_flags_v1(&flags)) break;
___end1:
func_v1();
for (;;) {
if (check_flags_v2(&flags)) goto ___end1;
___end2:
func_v2();
for (;;) {
if (check_flags_v3(&flags)) break;
if (check_flags_v4(&flags)) goto ___end1;
if (check_flags_v5(&flags)) goto ___end2;
}
}
}
}

这种代码的可维护性特别差,为了理清代码逻辑,需要花更多的时间和精力,所以,还是那条规则,尽量避免使用goto语句,避免复杂混乱的流程。

改善后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void bad_loop(int flags)
{
int state = 0; // 状态开始
for (auto it = state_start(&state); it != state_end(&state); it++) {
if (check_flags(&state)) break;

if (check_flags_v1(&flags)) break;

func_v1();

if (check_flags_v2(&flags)) state_move(&flags, 5); // 5 - __end1

if (check_flags_v2(&flags)) state_move(&flags, 6); // 6 - __end2

if (check_flags_v3(&flags)) state_move(&flags, 4);

if (check_flags_v4(&flags)) state_move(&flags, 3);

if (check_flags_v5(&flags)) state_move(&flags, 2);
}
}

这样改良后的代码不仅容易理解,而且复杂度明显优化,代码运行效率更高。

对于深层次的循环代码,不宜跨层次跳转,这里还是避免使用goto语句,否则很容易打破逻辑层次,使代码变得混乱。

重复代码块

有时代码中存在多个小段代码片段,功能上相差无几(很可以是前人Ctrl C+Ctrl V,修修改改留下的),其使用频率也很高,这种代码可以进一步优化,使其成为一个功能性的调用函数。

用状态标记来跟踪执行

状态位标记实际上就是上面的状态模式的设计方法(用于确定性的for循环),其实很多时候,并不是循环才会用状态模式,任何情形都可以用,但是对于复杂的执行流程情形,状态模式更易抽象流程,也便于调试和排查,它带来的各种好处是显而易见的,也是各种优秀架构设计对于复杂流程处理经常选择该模式的原因。

代码哲学

代码方面,如果代码可以满足如下几点,那么就可以说这是优秀的代码:

  • 通过所有测试
  • 没有重复代码
  • 体现系统的全部设计理念
  • 包含尽量少的实体(例如:类型,函数)

命名方式

  • 蛇形命名
  • 大驼峰命名
  • 小驼峰命名

统一的变量命名方式,可以便于代码的沟通与理解,从而可以提高团队的生产效率。

代码格式问题

代码格式主要是空白字符,换行字符等等的编码或默认使用的规则,一般而言,编辑器是可以定制化这些格式的,然后团队可以共享一份编码规章,这样在同一项目中就不会产生代码格式不同而发生的显示风格差异。

代码抽象

代码抽象层面需要探讨一下,作为软件工程方面,必须理解的设计模式,不是说对所有的模式倒背如流,而是说对常用的设计模式能够理解,并知道在什么场景下要用它,它能解决什么问题。

为何要使用设计模式呢?

因为在解决同一问题时,不同的人就有可能存在不同的表达方法,在代码层表现出的差异就会更加明显,而对于团队项目来说,这样的代码要让同时几十人来维护,需要每个人去理清它的具体逻辑将会很耗时间和精力,那么为何不让团队们一起使用同种语言来交流表达呢?所以在逻辑层面就有了设计模式,这样的话,张三写的逻辑,他在表达一个外观设计模式,李四只要知道外观设计模式,看了他的代码之后便一眼明白他的逻辑,这样团队就可以节省大量的时间和精力成本,从而更加高效地投入有意义的工作价值之中。

而设计模式正是应用软件开发中常见的底层逻辑,因此作为开发首先就是需要抽象化功能逻辑到设计模式(但如果不能抽象化,一般来说就是对设计模式无法掌握)。

德墨忒耳定律: 模块使用者不应该了解对象的内部细节

单元测试

单元测试通常是用来对代码功能模块的正确性进行检验的工具,如果软件的所有功能模块都是正确的,那么软件集成后的代码也是可以方便调试的,并极有可能也是正确的。因为功能模块属于部分性问题,集成的软件属于整体性问题,如果整体性问题不满足,说明部分性问题在顺序或相互依赖上存在差错,而我们就不用再考虑模块内的事情了,只需调整功能模块间的顺序或依赖即可解决问题。

跌进规则

  • 通过所有的测试
  • 重构代码
  • 代码不可重复性
  • 表达程序员的意图
  • 尽量少的类和方法数量

并发问题

在多线程系统中,会出现并发问题,并发的意思指多个运行节点对于某个资源的使用,在时间上具有不确定性。

并发和并行: 并发是指多个执行节点对单个资源使用在时间上具有不确定性,无法断定谁先谁后的问题;而并行则是说多个执行节点对于单个资源的使用,可能存在同时使用的可能。 实际上解决好并发问题,也是解决了并行的问题。

互斥与同步的问题

正是多执行节点对单个资源使用在时间序列的不确定,需要一种互斥同步使用的机制。

首先,需要知道互斥操作,它是一种排他操作,用于在使用资源的时候,资源被锁定,其他使用者无法获取使用。

操作系统里面用PV术语来表示:

1
2
3
P()
do_resource()
V()

PV操作都是原子化的,当执行P操作后,若存已经在资源使用,P操作会被阻塞,直到资源使用完毕,V操作执行后,资源就被正式是否,阻塞在P操作的一个使用者会立即得到使用权,并继续获取/使用/释放的操作流程。

在C++中,也是用同样的方式机制:

1
2
3
4
5
6
7
8
9
static std::unordered_map<std::string> resource;
static std::mutex mut;

void cosume_resource() // 被多线程调用
{
mut.lock();
consume(&resource);
mut.unlock();
}

但C++里面还可以设计一种原子锁,这种是确定资源消耗时间是可终结的,它起到的作用就是线程挂起,使之只能一个线程消耗资源,适合资源消耗简短的逻辑,因其结构简单,效率往往比互斥锁高。

1
2
3
4
5
6
7
8
9
10
11
12
13
static int lck = 0;

void atomic_lock()
{
while (lck < 0) {}
lck--;
}

void consume_resource() // 被多线程调用
{
atomic_lock();
consume_resource(&resource);
}

同步问题,是从互斥问题推导出来的,他是两个互斥操作之间的同步问题,仍然采用PV术语来表达。

对于生产者而言:

1
2
3
4
5
P(S1)
P(N1)
do_produce()
V(N1)
V(S2)

这里M的互斥锁是用于锁容器池的(池子发生改变时一定要加锁),当生产完一个,同时计数一个到N对应的计数锁。

对应的消费者为:

1
2
3
4
5
6
P(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
24
static 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);
}

链接

Improving Computer Program Readability to Aid Modification

Clean Code

欢迎关注我的其它发布渠道