理解關聯
在工作中,常常看到一些工程師對關聯的本質缺乏認識,浪費了不少討論時間,本文細化一下這裡的邏輯。
當我們說兩個物件有“關聯”:

直接感覺,是A和B總是以某種方式發生關係,比如一方或者兩方擁有對方的指標(甚至是記憶體本身),例如:
struct A { struct B *b; ... }
或者:
struct A { struct B { struct A *a; } }
這種關係可以變得更加複雜,比如A中有方法可以呼叫B:
class A { void dealWithB(struct B *b); ... }
或者另一個物件把兩者關聯起來:
class C { Map<class A, class B> ab_map; }
甚至可以從表面上看不出來的,下面是一個從表面上看不出的關聯:
class A { void storeStudentData(FILE *file); } class B { void takeStrudentRecords(FILE *file) }
A把特定的資料存到檔案中,B從這個檔案中拿這些資料,A和B就有了關聯。
嚴格來說,你可以說一個系統中任何兩個模組有關聯。所以,我們不是為了說明兩個模組有“關聯”,所以就要在架構說明的時候就說兩個模組有“關聯”,我們是因為這種關聯對我們的的下一層設計有影響,所以我們才需要在架構定義的時候說明它們有“關聯”。
所以,關聯其實關心的是兩個模組的“同步”關係。
所謂同步關係,可以表現為多種形式,比如:
A的邏輯發生了修改,B也必須同步修改,整個系統才能是正常的。前面這個檔案的關聯就是一個例子,如果你修改了A儲存檔案的方式,雖然整個程式編譯一點問題沒有,但如果你不修改B,這個程式就不可能正常。
所以,很多人覺得,把函式介面變成訊息介面,檔案介面,這個系統就“解耦”了,這完全是在騙外行,我們從架構上從來不這樣看問題。關聯是因為兩者在業務上有關係,程式碼的邏輯鏈在邏輯上對另一方的設計方法有“依賴”,不消除這種邏輯上的“依賴”,就不可能“解耦”,這你怎麼玩各種定義都是玩不出花來的。而消除解耦的唯一方式,是消除多餘的“依賴”,比如我只需要你是個指標,不需要你是個unsigned long int,你就不要說這個變數是unsigned long int;你列印指標就用"%p",你不要用"%xl";你要給我傳遞“學生資料”,你就給我"interface student",不要給我“class student_implement_on_file”,這樣就能消除耦合,但每個消除耦合的動作都是額外的工作量,所謂你必須在聚合了很多關聯的地方建立有限的解耦設計,這才能在工作量和解耦方面取得平衡,架構設計從來不是可以被簡單描述的工作,否則,它就變成一種“編碼”了。
再說遠一點,昨天評審了一個設計,談到一個軟硬體之間的FIFO介面,設計者興高采烈地給我介紹了一個精妙的設計,叫做xxx_id,我聽了半天,原來邏輯是這樣的:軟體要寫一個請求到硬體上,不能直接向Ring Buffer(迴圈佇列)的尾巴上直接寫這個請求的內容,而要在裡面找一個已經釋放的BD(Buffer Descriptor),再把這個BD的下標(稱為xxx_id),寫到另一個叫Queue的資料結構中,硬體會按FIFO的形式從Queue裡面讀xxx_id,然後得到這個BD……
我一聽,WTF,這不就是把FIFO實現到Queue裡面嗎,從我架構的角度來說,這就是一個FIFO,這些什麼Ring Buffer之類的細節,統統都是浪費我的時間。這根本不在我這一層考慮。
我這個例子很簡單,但想想你們平時的設計,這種情況無處不在,我給你抽出來了,你覺得這東西顯而易見,拿著你的具體設計,你就又暈菜了。我強迫我們的工程師必須寫設計文件,因為你寫出來,你就會強迫自己抽取邏輯鏈,否則,你以為你“設計精妙”,其實就是個傻逼設計。
越扯越遠了,我們回到同步關係的例子。另一種我們常常考慮關聯的場景是執行緒和鎖設計。兩個物件(物件不一定是面嚮物件語言中說的“物件”)有關聯,就意味著我修改一個物件的時候,必須和另一個關聯物件發生同步關係。
比如我們常常面對的一種情形:A是B的一個集合,當我們單獨訪問A的時候,只需要鎖住A就可以了,當我們訪問B的時候,就需要鎖住集合中的所有A。這種鎖設計很容易發生問題,因為A,B上的鎖的訪問順序很容易就會在不同的流程下不一樣,很容易就死鎖了。
討論這種問題的時候,不少工程師會陷入換各種不同的鎖實現,加入各種無鎖演算法的陷阱。但他們都沒有注意一個關鍵的問題,A和B之所以同步有問題,是因為A和B有關聯,是因為我們在修改其中一方的時候,需要把本方和另一方的其他相關操作“停下來”。鎖設計本身不會改變這種“需求”,你要解決這個問題,唯一的方法是“解耦”,也就是確定“僅僅什麼修改的時候,才需要等待僅僅什麼操作完成”。不討論這種問題,而去討論把B的指標放到A裡還是B裡放一個數組指向A,乃至加上各種RCU操作,都不解決問題的啊。