2. 數據抽象

現在我們來實現一個完整的複數運算程序。在上一節我們已經定義了複數的結構體類型,現在需要圍繞它定義一些函數。複數可以用直角座標或極座標表示,直角座標做加減法比較方便,極座標做乘除法比較方便。如果我們定義的複數結構體是直角座標的,那麼應該提供極座標的轉換函數,以便在需要的時候可以方便地取它的模和輻角:

#include <math.h>

struct complex_struct {
	double x, y;
};

double real_part(struct complex_struct z)
{
	return z.x;
}

double img_part(struct complex_struct z)
{
	return z.y;
}

double magnitude(struct complex_struct z)
{
	return sqrt(z.x * z.x + z.y * z.y);
}

double angle(struct complex_struct z)
{
	return atan2(z.y, z.x);
}

此外,我們還提供兩個函數用來構造複數變數,既可以提供直角座標也可以提供極座標,在函數中自動做相應的轉換然後返回構造的複數變數:

struct complex_struct make_from_real_img(double x, double y)
{
	struct complex_struct z;
	z.x = x;
	z.y = y;
	return z;
}

struct complex_struct make_from_mag_ang(double r, double A)
{
	struct complex_struct z;
	z.x = r * cos(A);
	z.y = r * sin(A);
	return z;
}

在此基礎上就可以實現複數的加減乘除運算了:

struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
{
	return make_from_real_img(real_part(z1) + real_part(z2),
				  img_part(z1) + img_part(z2));
}

struct complex_struct sub_complex(struct complex_struct z1, struct complex_struct z2)
{
	return make_from_real_img(real_part(z1) - real_part(z2),
				  img_part(z1) - img_part(z2));
}

struct complex_struct mul_complex(struct complex_struct z1, struct complex_struct z2)
{
	return make_from_mag_ang(magnitude(z1) * magnitude(z2),
				 angle(z1) + angle(z2));
}

struct complex_struct div_complex(struct complex_struct z1, struct complex_struct z2)
{
	return make_from_mag_ang(magnitude(z1) / magnitude(z2),
				 angle(z1) - angle(z2));
}

可以看出,複數加減乘除運算的實現並沒有直接訪問結構體complex_struct的成員xy,而是把它看成一個整體,通過調用相關函數來取它的直角座標和極座標。這樣就可以非常方便地替換掉結構體complex_struct的存儲表示,例如改為用極座標來存儲:

#include <math.h>

struct complex_struct {
	double r, A;
};

double real_part(struct complex_struct z)
{
	return z.r * cos(z.A);
}

double img_part(struct complex_struct z)
{
	return z.r * sin(z.A);
}

double magnitude(struct complex_struct z)
{
	return z.r;
}

double angle(struct complex_struct z)
{
	return z.A;
}

struct complex_struct make_from_real_img(double x, double y)
{
	struct complex_struct z;
	z.A = atan2(y, x);
	z.r = sqrt(x * x + y * y);
}

struct complex_struct make_from_mag_ang(double r, double A)
{
	struct complex_struct z;
	z.r = r;
	z.A = A;
	return z;
}

雖然結構體complex_struct的存儲表示做了這樣的改動,add_complexsub_complexmul_complexdiv_complex這幾個複數運算的函數卻不需要做任何改動,仍然可以用,原因在於這幾個函數只把結構體complex_struct當作一個整體來使用,而沒有直接訪問它的成員,因此也不依賴于它有哪些成員。我們結合下圖具體分析一下。

圖 7.3. 數據抽象

數據抽象

這裡是一種抽象的思想。其實“抽象”這個概念並沒有那麼抽象,簡單地說就是“提取公因式”:ab+ac=a(b+c)。如果a變了,ab和ac這兩項都需要改,但如果寫成a(b+c)的形式就只需要改其中一個因子。

在我們的複數運算程序中,複數有可能用直角座標或極座標來表示,我們把這個有可能變動的因素提取出來組成複數存儲表示層:real_partimg_partmagnitudeanglemake_from_real_imgmake_from_mag_ang。這一層看到的數據是結構體的兩個成員xy,或者rA,如果改變了結構體的實現就要改變這一層函數的實現,但函數介面不改變,因此調用這一層函數介面的複數運算層也不需要改變。複數運算層看到的數據只是一個抽象的“複數”的概念,知道它有直角座標和極座標,可以調用複數存儲表示層的函數得到這些座標。再往上看,其它使用複數運算的程序看到的數據是一個更為抽象的“複數”的概念,只知道它是一個數,像整數、小數一樣可以加減乘除,甚至連它有直角座標和極座標也不需要知道。

這裡的複數存儲表示層和複數運算層稱為抽象層(Abstraction Layer),從底層往上層來看,複數越來越抽象了,把所有這些層組合在一起就是一個完整的系統。組合使得系統可以任意複雜,而抽象使得系統的複雜性是可以控制的,任何改動都只侷限在某一層,而不會波及整個系統。著名的計算機科學家Butler Lampson說過:“All problems in computer science can be solved by another level of indirection.”這裡的indirection其實就是abstraction的意思。

習題

1、在本節的基礎上實現一個打印複數的函數,打印的格式是x+yi,如果實部或虛部為0則省略,例如:1.0、-2.0i、-1.0+2.0i、1.0-2.0i。最後編寫一個main函數測試本節的所有代碼。想一想這個打印函數應該屬於上圖中的哪一層?

2、實現一個用分子分母的格式來表示有理數的結構體rational以及相關的函數,rational結構體之間可以做加減乘除運算,運算的結果仍然是rational。測試代碼如下:

int main(void)
{
	struct rational a = make_rational(1, 8); /* a=1/8 */
	struct rational b = make_rational(-1, 8); /* b=-1/8 */
	print_rational(add_rational(a, b));
	print_rational(sub_rational(a, b));
	print_rational(mul_rational(a, b));
	print_rational(div_rational(a, b));

	return 0;
}

注意要約分為最簡分數,例如1/8和-1/8相減的打印結果應該是1/4而不是2/8,可以利用第 3 節 “遞歸”練習題中的Euclid算法來約分。在動手編程之前先思考一下這個問題實現了什麼樣的數據抽象,抽象層應該由哪些函數組成。