計算機程式設計發展至今,一共只有三個程式設計正規化:
軟體架構的三大關注重點:功能性、組建獨立性以及資料管理,和程式設計正規化不謀而合
限制控制權的直接轉移,禁止 goto,用 if/else/while 替代
限制控制權的間接轉移,禁用函數指標,用多型替代
把一組關聯的資料和函數管理起來,外部只能看見部分函數,資料則完全不可見。
封裝並不是物件導向語言特有的,C 語言也支援:
point.h
struct Point;
struct Point* makePoint(double x, double y);
double distance(struct Point *p1, struct Point *p2)
C 語言的封裝是完美的封裝:利用 forward declaration,Point 的資料結構、內部實現對 point.h 的使用者完全不可見。
而後來的 C++ 雖然是物件導向的程式語言,但卻破壞了封裝性:
point.h
class Point {
public:
Point(double x, double y);
double distance(const Point& p1, const Point& p2);
private:
double sqrt(double x);
private:
double x;
double y;
};
C++ 編譯器需要知道類的物件大小,因此必須在標頭檔案中看到成員變數的定義。雖然 private 限制了使用者存取私有成員,但這樣仍然暴露了類的內部實現。(C++ 的 PIMPL 慣用法可以在一定程度上緩解這個問題)
Java 和 C# 拋棄了標頭檔案、實現分離的程式設計方式,進一步削弱了封裝性,因為無法區分類的宣告和定義。
C 語言也支援繼承:
namedPoint.h
struct NamedPoint;
struct NamedPoint* makeNamedPoint(double x, double y, char* name);
void setName(struct NamePoint *np, char* name);
char* getName(struct NamedPoint *np);
namedPoint.c
#include "namePoint.h"
struct NamedPoint {
double x;
double y;
char* name;
};
// 或者
#include "point.h"
struct NamePoint {
Point parent_;
char* name;
};
// 省略其他函數實現
main.c
#include "point.h"
#include "namedPoint.h"
int main() {
struct NamePoint* p1 = makeNamedPoint(0.0, 0.0, "origin");
struct NamePoint* p2 = mameNamePoint(1.0, 1.0, "upperRight");
// C 語言中的繼承需要強制轉換 p1、p2 的型別
// 真正的物件導向語言一般可以自動將子類轉成父類別指標/參照
distance((struct Point*)p1, (struct Point*)p2);
}
在 main.c 中,NamePoint 被當作 Point 來使用。之所以可以,是因為 NamePoint 是 Point 的超集,且共同成員的順序一致。C++ 中也是這樣實現單繼承的。
在物件導向語言發明之前,C 語言也支援多型。
UNIX 要求每個 IO 裝置都提供 open、close、read、write、seek 這 5 個標準函數:
struct FILE {
void (*open)(char* name, int mode);
void (*close)();
int (*read)();
void (*write)(char);
void (*seek)(long index, int mode);
};
這裡的 FILE 就相當於一個介面類,不同的 IO 裝置有各自的實現函數,通過設定函數指標指向不同的實現來達到多型的目的。上層的功能邏輯只依賴 FILE 結構體中的 5 個標準函數,並不關心具體的 IO 裝置什麼。更換 IO 裝置也無需修改功能邏輯的程式碼,IO 只是功能邏輯的一個外掛。
C++ 中每個虛擬函式的地址都記錄在一個叫 vtable 的資料結構中,帶有虛擬函式的類會有一個隱式的、指向 vtable 的虛表指標。每次呼叫虛擬函式都會先查詢 vtable,子類建構函式負責將子類虛擬函式地址載入到物件的 vtable 中。
多型本質上就是函數指標的一種應用。用函數指標實現多型的問題在於函數指標的危險性。依賴人為遵守一系列的約定很容易產生難以跟蹤和偵錯的 bug。物件導向程式設計使得多型再不需要依賴人工遵守約定,可以更簡單、更安全地實現複雜功能。物件導向程式設計的出現使得「外掛式架構」普及開來。
此外,物件導向程式設計的帶來的另一個重大好處是依賴反轉:通過引入介面,原始碼的依賴關係不再受到控制流的限制,軟體架構師可以輕易地更改原始碼的依賴關係。這也是物件導向程式設計正規化的核心本質(關於依賴反轉,後面會單獨用一篇來介紹)。
回到開始的問題,物件導向到底什麼?有許多不同的說法和意見,但是對於軟體架構師來說,物件導向程式設計就是以多型為手段,控制原始碼依賴的能力。這種能力可以讓軟體架構師構建某種外掛式架構,讓高層策略和底層實現元件相分離,底層元件作為外掛可以獨立開發和部署。
限制賦值操作
函數語言程式設計中的變數是不可變的
不可變性是軟體架構需要考慮的重點,因為所有的並行、死鎖、競爭問題都是可變變數導致的,如果變數不可變,就不會有這些問題
架構設計良好的程式應該拆分成可變、不可變兩種元件,其中可變狀態元件中的邏輯越少越好
事件溯源:只儲存事務記錄,不儲存具體狀態;需要狀態時,從頭計算所有事務。
從 1946 年圖靈寫下第一行程式碼至今,軟體程式設計的核心沒有變:計算機程式無一例外是由順序結構、分支結構、迴圈結構和間接轉移這幾種行為組合而成的,無可增加, 也缺一不可。
所有三個正規化都是限制了編碼方式,而不是增加新能力!
三個程式設計正規化都是在 1958 - 1968 年間提出,此後再也沒有新的正規化提出,未來幾乎不可能再有新的正規化。因為除了 goto 語句、函數指標、賦值語句之外,也沒有什麼可以限制的了。