编写可读代码的艺术 (10)

第十章 抽取不相关的子问题

编码在初级阶段的时候,往往把一大堆代码写在一个函数里。慢慢的经验多了,就开始注意写子函数了。要如何写子函数呢?就是这章的重点。这儿主要关注如何积极地发现并抽取出不相关的子逻辑。

1、介绍性的例子

下列函数的高层次目标是“找到距离给定目标最近的位置”:

//return which element of 'array' is closest to the given latitude/longitude.
//models the Earth as a perfect sphere.
var findClosestLocation = function (lat, lng, array)
{
	var closest;
	var closest_dist = Number.MAX_VALUE;
	for(var i = 0; i < array.length; i += 1)
	{
		//Convert both points to radians
		var lat_rad = radians(lat);
		var lng_rad = radians(lng);
		var lat2_rad = radians(array[i].latitude);
		var lng2_rad = radians(array[i].longitude);
		
		//use the "apherical law of cosines" formula
		var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
						     Math.cos(lat_rad) * Math.cos(lat2_rad) *
							 Math.cos(lng2_rad - lng_rad));
		
		if(dist < closest_dist)
		{
			closest = array[i];
			closest_dist = dist;
		}
	}
	return closest;
};
这段代码中主要工作是“计算两个经纬坐标点之间的球面距离”。如果把这部分代码抽取到一个子函数中呢:

var spherical_distance = function (lat1, lng1, lat2, lng2)
{
	//Convert both points to radians
	var lat1_rad = radians(lat1);
	var lng1_rad = radians(lng1);
	var lat2_rad = radians(lat2);
	var lng2_rad = radians(lng2);
	
	//use the "apherical law of cosines" formula
	var dist = Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) +
						 Math.cos(lat1_rad) * Math.cos(lat2_rad) *
						 Math.cos(lng2_rad - lng_rad));
};
这样一来,主函数就变成了这样:

var findClosestLocation = function (lat, lng, array)
{
	var closest;
	var closest_dist = Number.MAX_VALUE;
	for(var i = 0; i < array.length; i += 1)
	{
		var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
		
		if(dist < closest_dist)
		{
			closest = array[i];
			closest_dist = dist;
		}
	}
	return closest;
};

这段代码是不是好读多了。这段代码隐藏了具体的实现细节,让读者只需要关最终结果,而不必为复杂的几何公式分心。另外,spherical_distance()函数很容易做单独测试,它完成是独立的,字包含的,不关于外部程序如何使用它。

2、纯工具代码

有很多操作经常被人用到,比如操作字符串、使用哈希表以及读写文件等等。

这些常见的操作都被以库的形式提供出来了。在编程时要时刻注意这类自相关的操作是不是已经有可使用的库API了。如果没有的话,可以自己写一个类似的库API,以便以后可以随时使用。

3、其他多用途代码

考虑一个结构体中,存在一个链表中,该链表内可以包含许多的元素。结构体中有一个变量记录了元素的个数。

struct element_test
{
	int element_num;
	list_test *p_head;
}
如果我们在代码中,需要获得列表内元素的个数,要如何做呢?有许多人,直接使用了element_num;还有许多人,写了这样的一个api:

int get_element_num(element_test *p_test)
{
	return p_test->element_num;
}
这两种使用方法,我更推崇第二种,因为第二种将获取元素个数的实现细节隐藏了起来,提示外部只需要关注结果,不需要关注过程。

第二种做法,还带来了意外的好处。如果有一天,需要获取的不是所有元素的个数,而是获取大于某一个值的元素的个数的话,只需要修改这个API函数即可,其他地方不用修改。而第一种方式里,需要在每一个直接使用element_num的地方都做修改,这样代码量会大大增加。

第二种做法通常叫做创建通用代码,它将某些代码完全地从项目中解耦出来,它负责的代码可以被独立的测试、使用,并易于理解。

有时候,我们需要像第二种做法一样,从上到下的进行编程,优先考虑高层次模块和函数,先考虑需要解决的问题,然后在每个对与需要解决问题的API进行编码支持。这样编码完成时,你会发现,好多问题都已经被单独处理了。

4、过犹不及

编码过程中,抽取API函数是一个很好地习惯。不过,如果过分沉迷于抽取API,往往会导致代码量复杂,小函数过多,在阅读代码过程中,需要不停地在各个小函数来回跳转,这是不提倡的。

抽取API函数时,应该遵循“保持统一的抽象层”原则。就像目录一样,在一个函数中,函数内部的各部操作应该保持在一个抽象层面。如果都是流程函数,就不要引入细节实现。如果都是细节实现,就不要再过多抽取API,封装成流程。


注意要把一般代码和项目专有的代码分开。一般来讲,一个项目中,大部分的代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的就是让你的程序与众不同的核心部分。抽取API函数,拆分不相关的逻辑处理,这使程序员关注小而定义良好的问题,使得这些问题与项目的其他部分脱离,也更有利于以后重用。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值