如何降低認(rèn)知負(fù)載
Hi,我是阿昌
,今天學(xué)習(xí)記錄的是 關(guān)于如何降低認(rèn)知負(fù)載
的內(nèi)容。
認(rèn)知負(fù)載。這個看似與軟件開發(fā)毫無瓜葛的知識,實際上卻決定了軟件系統(tǒng)的成敗。
因此在遺留系統(tǒng)現(xiàn)代化中,把“以降低認(rèn)知負(fù)載為前提”作為首要原則。
總說認(rèn)知負(fù)載如何如何,降低認(rèn)知負(fù)載又是多么重要,那怎么才能真正降低認(rèn)知負(fù)載呢?
有哪些方法能降低認(rèn)知負(fù)載。其中最重要的工具,就是·活文檔
。
一、什么是活文檔
活文檔(living document),顧名思義,就是指活著的文檔,也就是在持續(xù)編輯和更新的文檔,有時候也叫長青文檔或動態(tài)文檔。
比如維基百科中的一個詞條,隨時都有人更新和維護(hù),這就是一個活文檔。與之相對的是靜態(tài)文檔,也就是一旦產(chǎn)生就不會更新的文檔,比如大英百科全書中的一個條目。
可以想象一下,在軟件開發(fā)過程中,無論是瀑布模式還是敏捷,拿到的需求文檔或故事卡是“維基百科”還是“大英百科”呢?
想大多數(shù)情況可能是,在最終需求還沒有敲定時還是“維基百科”,也就是還會隨時更新,而一旦敲定開始開發(fā)后,就變成了“大英百科”,再也不會更新了吧。然而隨著需求的不斷疊加,“大英百科”作為當(dāng)時系統(tǒng)的一個“快照”,早就已經(jīng)失去了時效性。只有將不同時段、不同模塊的文檔片段合并在一起,才能得到當(dāng)前系統(tǒng)的快照。但這個合并放在現(xiàn)實中是很難操作的。
正是因為發(fā)現(xiàn)了這樣的問題,《實例化需求》
一書的作者 Gojko Adzic 將活文檔的概念引入到了軟件開發(fā)當(dāng)中;而去年出版的《活文檔——與代碼共同演進(jìn)》一書,又在此基礎(chǔ)上對活文檔如何落地做了系統(tǒng)指導(dǎo)。
二、如何用活文檔挖掘業(yè)務(wù)知識
那它是如何降低遺留系統(tǒng)的認(rèn)知負(fù)載的。
1、為遺留代碼添加注解
下面這段虛構(gòu)的遺留代碼(抱歉我實在編不出更糟糕的代碼了……),在沒有任何文檔的情況下,如何理解這段代碼的意思呢?
public class EmployeeService {
public void createEmployee(long employeeId) { /*...*/ }
public void updateEmployee(long employeeId) { /*...*/ }
public void deleteEmployee(long employeeId) { /*...*/ }
public EmployeeDto queryEmployee(long employeeId) { /*...*/ }
public void assignWork(long employeeId, long ticketId) {
// 獲取員工
EmployeeDao employeeDao = new EmployeeDao();
EmployeeModel employee = employeeDao.getEmployeeById(employeeId);
if (employee == null) {
throw new RuntimeException("員工不存在");
}
// 獲取工單
WorkTicketDao workTicketDao = new EmployeeDao();
WorkTicketModel workTicket = workTicketDao.getWorkTicketById(ticketId);
if (workTicket == null) {
throw new RuntimeException("工單不存在");
}
// 校驗是否可以將員工分配到工單上
if ((employee.getEmployeeType() != 6 && employee.getEmployeeStatus() == 3)
|| (employee.getEmployeeType() == 5 && workTicket.getTicketType() == "2")) {
throw new RuntimeException("員工類型與工單不匹配,不能將員工分配到工單上");
}
if (!isWorkTicketLocked(workTicket)) {
if (!isWorkTicketInitialized(workTicket)) {
throw new RuntimeException("工單尚未初始化");
}
}
// ...
}
public void cancelWork(long employeeId, long ticketId) { /*...*/ }
}
如果每個方法都很長,這樣一個類就會愈發(fā)不可讀,從中理解業(yè)務(wù)知識的難度也越來越大,這就是之前提到的認(rèn)知負(fù)載過高。
如果把這種代碼轉(zhuǎn)化為下面的腦圖,是不是一下子就清晰許多了呢?
閱讀代碼時,是以線性
的方式逐行閱讀的,這樣的信息進(jìn)入大腦后,就會處理成上面這樣的樹狀信息,方便理解和記憶。但當(dāng)代碼過于復(fù)雜的時候,這個處理過程就會需要更多的腦力勞動,導(dǎo)致過高的認(rèn)知負(fù)載。
可以通過在代碼中加入活文檔的方式,來降低認(rèn)知負(fù)載。
其實要得到上面的腦圖,只需要在代碼中加入一些簡單的注解:
@Chapter("員工服務(wù)")
public class EmployeeService {
@Doc("員工創(chuàng)建")
public void createEmployee(long employeeId) { /*...*/ }
@Doc("員工修改")
public void updateEmployee(long employeeId) { /*...*/ }
@Doc("員工刪除")
public void deleteEmployee(long employeeId) { /*...*/ }
@Doc("獲取員工信息")
public EmployeeDto queryEmployee(long employeeId) { /*...*/ }
@Doc("給員工分配工單")
public void assignWork(long employeeId, long ticketId) { /*...*/}
@Doc("撤銷工單分配")
public void cancelWork(long employeeId, long ticketId) { /*...*/ }
}
編寫一個工具,它可以基于這些注解來生成根節(jié)點和二級節(jié)點,并將方法中拋出的異常作為葉子節(jié)點。
這么做的原因是,雖然遺留系統(tǒng)中的很多文檔和代碼注釋已經(jīng)不是最新的了,但這些異常信息往往會直接拋出去展示給用戶看,是為數(shù)不多的、可以從代碼中直接提取的有效信息。
當(dāng)然這樣做也有一定局限性,因為異常信息中可能包含一些運行時數(shù)據(jù)。比如“ID 為 12345 的員工不存在”這樣的異常信息,是由“ID 為 + employeeId + 的員工不存在”這樣的字符串拼接而成,靜態(tài)掃描字節(jié)碼,是無法得出這些運行時數(shù)據(jù)的。但即使只在葉子節(jié)點中顯示“ID 為 %s 的員工不存在”這樣的信息,也已經(jīng)非常有用了。
通過這樣的工具,可以把一個非常復(fù)雜的業(yè)務(wù)代碼,轉(zhuǎn)化為下面這樣的腦圖(為了過濾掉敏感信息,故意將圖片做了模糊處理)。
這段業(yè)務(wù)代碼總共有 5000 多行,一行一行地去閱讀代碼會讓人抓狂,但有了這樣的腦圖,認(rèn)知負(fù)載簡直降低了一個數(shù)量級。
看到這里,你一定對這個工具十分感興趣了。但是很遺憾,這個自研的工具目前還沒有開源。
不過它的原理其實十分簡單,想必你已經(jīng)猜到了,就是掃描 Java 字節(jié)碼,獲取到用注解標(biāo)記的代碼,然后再進(jìn)一步分析得到異常信息,組織成樹形結(jié)構(gòu),再生成一些中間文檔,并通過一些繪圖引擎繪制出來。
在實際操作過程中,只需要有一個人通讀一次代碼,哪怕花上幾個禮拜的時間,但只要能理出一個業(yè)務(wù)模塊的基本邏輯,添加上注解,就可以通過圖形化的方式來展示代碼結(jié)構(gòu)。其他人不需要再次這么痛苦地閱讀代碼了,可謂一勞永逸,效率會大大提升。這么做還有一個好處是,當(dāng)新的需求來臨時,開發(fā)人員可以迅速定位到要修改的地方,不需要再去扒一遍代碼了。
傳統(tǒng)的代碼和文檔最大的問題是,代碼是代碼,文檔是文檔,彼此分離。代碼和文檔的關(guān)聯(lián)關(guān)系儲存在開發(fā)人員腦子里,這樣認(rèn)知負(fù)載比較高。當(dāng)開發(fā)人員看到一份新的需求文檔時,需要搜索一下腦子里的記憶,才能想起來這部分內(nèi)容是在代碼的什么位置。
然而人腦不是電腦,這種記憶是十分不靠譜的,搜索定位的過程也十分低效。而上面這樣的腦圖就和代碼很好地結(jié)合了起來,可以說找到文檔,就找到了代碼,非常有效地降低了認(rèn)知負(fù)載。這么做的第三個好處是有利于團(tuán)隊協(xié)作。
業(yè)務(wù)分析師、開發(fā)人員、測試人員都可以圍繞這樣一份文檔來討論需求、設(shè)計測試用例。
2、實例化需求最好的工件就是活文檔
用實例化需求的方式編寫的測試也是一種活文檔。所謂實例化需求,實際上指的是以現(xiàn)實中的例子來描述需求,而不是抽象的描述。
怎么理解呢?在生活中我們會遇到很多文字描述,比如產(chǎn)品說明書、合同文本、法律法規(guī)等。
這些描述大多數(shù)時候都是抽象的,普通人讀起來很難理解,甚至引起歧義。
如果抽象的說明能夠配幾個具體的示例,認(rèn)知負(fù)載就會大大降低。
軟件開發(fā)中的需求描述也是如此。讓我印象非常深刻的是,在剛加入 Thoughtworks 沒幾天的時候,曾經(jīng)跟著 BA 和其他開發(fā)人員找客戶對一個關(guān)于用戶權(quán)限的需求,大概是不同的用戶在不同的場景下,能看到一個頁面中的哪些字段。
那位 BA 沒有像我之前見過的 BA 那樣,寫一大篇文檔,而是簡單地把界面打印了出來了好幾張,每張紙上注明場景,用馬克筆把不能看到的字段打個大叉劃掉。
就這樣,他用最簡單的方式,在 5 分鐘內(nèi)就快速確認(rèn)了所有的需求,客戶也對這種直觀的方式非常滿意。這些紙隨后就給了我們開發(fā)人員,我們根本沒必要再去看需求文檔了,因為需求已經(jīng)以如此實例化的方式展示給我們了。這就是典型的實例化需求。
在開發(fā)時,可以將這種需求轉(zhuǎn)換為測試,這種以實例化方式描述的測試,也是一種活文檔。
它們不但很好地展示了業(yè)務(wù)知識,而且是隨代碼更新的。比如上面的給員工分配工單的例子,按實例化需求的方式,可以寫出一系列組織良好的測試,如下所示:
@Test
public void should_be_able_to_assign_work_to_an_employee() {}
@Test
public void should_not_assign_work_to_when_employee_not_exist() {}
@Test
public void should_not_assign_work_when_ticket_not_exist() {}
@Test
public void should_not_assign_work_when_employee_type_and_ticket_type_not_match() {}
@Test
public void should_not_assign_work_when_ticket_is_not_initialized() {}
其實就是將需求文檔的描述轉(zhuǎn)換成了測試的方法名。
讀到測試,就相當(dāng)于讀到了需求文檔;測試通過,就相當(dāng)于需求完成了。以后如果需求有了變更,只需要同步修改測試的名稱即可。
這時候,測試是和代碼共同演進(jìn)的,也就是活文檔。
在某些框架下運行上面的測試,還能幫我們?nèi)サ糁虚g的下劃線,這就更像是文檔了。
3、用依賴分析工具展示系統(tǒng)知識
經(jīng)過多年的腐化,類與類之間、包與包之間、模塊與模塊之間、服務(wù)與服務(wù)之間分別是什么樣的依賴關(guān)系呢?
這就好像我們來到一個陌生的城市時,對這個城市的行政區(qū)域、大街小巷都不了解。
如果想從一個地方到另一個地方,應(yīng)該怎么辦呢?最好的辦法就是搞一張當(dāng)?shù)氐牡貓D(當(dāng)然你也可以用地圖 App),有了地圖的指引,就不會迷路了。
同樣,可以通過依賴分析工具,建立一張遺留系統(tǒng)的地圖
,這樣就可以快速知道一個業(yè)務(wù)是由哪些模塊組成的。
市面上存在很多做系統(tǒng)依賴分析的工具,如 Backstage、Aplas、Honeycom、Systems、Coca 等等。感興趣的同學(xué)可以去了解一下。
但我們也會發(fā)現(xiàn),有時這些工具并不能解決我們的全部問題。
比如在做系統(tǒng)的數(shù)據(jù)拆分時,希望知道一個 API 調(diào)用都訪問了哪些表,從而評估工作量。
這種定制化的需求很多工具都無法滿足,不過不要灰心,發(fā)揮開發(fā)人員優(yōu)勢的時候又到了。沒有輪子,就造一個出來。其實這種根據(jù)入口點獲取表名的邏輯并不復(fù)雜,只需要遍歷語法樹,把所有執(zhí)行 SQL 語句的點都找出來,然后分析它的語句中包含哪些表就可以了。對于存儲過程或函數(shù),也可以找到執(zhí)行它們的點,獲得存儲過程或函數(shù)的名稱,然后再根據(jù)名稱找到對應(yīng)的 SQL 文件,再做類似的分析。
當(dāng)然,這要求首先要治理好編寫在數(shù)據(jù)庫中的存儲過程和函數(shù)治理,將 DDL(Data Definition Language)遷移到代碼庫中,進(jìn)行版本化。
這樣分析工具定位起來才方便。對于復(fù)雜的入口方法,你可能會得到一幅相當(dāng)大的列表或腦圖,它雖然能列出全部內(nèi)容,但讀起來仍然很費勁。這時候我們有兩個辦法。
-
一是重構(gòu)復(fù)雜的入口方法,抽取出若干小的方法,再以小方法為入口點做分析。
-
二是修改分析工具,直接分析存儲過程或函數(shù)。如果存儲過程或函數(shù)過大,也可以進(jìn)一步拆分。
繼續(xù)改進(jìn)分析工具。比如分析不同模塊之間所依賴的對方的表有哪些,這對于數(shù)據(jù)拆分也是非常有幫助的。
三、總結(jié)
雖然遺留系統(tǒng)中可能沒有太多的測試,但仍然可以通過向代碼中添加注解的方式來編寫活文檔,并通過工具來實現(xiàn)圖形化展示,將遺留系統(tǒng)中無處可尋的業(yè)務(wù)知識暴露在面前。
除此之外,還可以使用依賴分析工具來挖掘系統(tǒng)知識,同樣也可以用圖形化的方式來幫助我們理清系統(tǒng)內(nèi)的依賴關(guān)系。
這對開發(fā)新需求或推動代碼和架構(gòu)的現(xiàn)代化都非常有幫助。
《活文檔》這本書在介紹遺留系統(tǒng)的“文檔破產(chǎn)”時,是這樣描述遺留系統(tǒng)的,這段話:
遺留系統(tǒng)里充滿了知識,但通常是加密的,而且我們已經(jīng)丟失了秘鑰。沒有測試,就無法對遺留系統(tǒng)的預(yù)期行為做出清晰的定義。
沒有一致的結(jié)構(gòu),就必須猜測它是如何設(shè)計的、為什么這么設(shè)計以及應(yīng)該如何演進(jìn)。沒有謹(jǐn)慎的命名,就必須猜測和推斷變量、方法和類的含義,以及每段代碼負(fù)責(zé)的任務(wù)。文章來源:http://www.zghlxwxcb.cn/news/detail-421184.html
雖然遺留系統(tǒng)是“文檔破產(chǎn)”的,是“加密”的,但是只要我們掌握了活文檔這個“破譯工具”,就可以一步一步破解出那些隱匿在系統(tǒng)深處的知識。文章來源地址http://www.zghlxwxcb.cn/news/detail-421184.html
到了這里,關(guān)于Day952.如何降低認(rèn)知負(fù)載 -遺留系統(tǒng)現(xiàn)代化實戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!