在程序開發的過程中,相同的功能往往有不同的實現方式。對於可以實現同樣功能的不同代碼,復雜度是用於比較其質量優劣的重要指標。
在本文中,代碼復雜度是指代碼被理解/修改的難易程度。越容易被理解、修改的代碼的復雜度越低;反之其復雜度越高。
復雜度低的代碼比復雜度高的代碼有更多好處,比如,
- 從代碼“查邏輯”變得簡單
- 可以節省修改的時間
- 降低在未來引入bug的幾率
- 新人會更容易上手現有代碼
- 幫助整個系統更加“長壽”
ABAP開發是在SAP系統中進行的,而SAP是企業的核心信息系統,其中會包含復雜的業務邏輯,通常由ABAP實現,並需要長期的維護。在這樣的工作中,ABAP代碼的復雜度對系統維護成本甚至項目的成敗有着重要的影響。
在下文中,我會介紹幾種有助於最小化代碼復雜度的通用思路,並嘗試把它們和實際的ABAP開發工作結合起來,幫助理解。
作者水平有限,如果讀者發現了任何問題,歡迎評論指出。
本文鏈接:https://www.cnblogs.com/hhelibeb/p/10871392.html
原創內容,轉載請注明
1, 模塊化設計
兩年前,我第一次參與的SAP項目剛完成不久。那時我對自己的技術相當自信,在項目中,我不僅完成了多種類型的功能開發,而且也讀完了整個項目的新開發代碼。即使對於一些沒做過的新的功能需求,我也往往能在不依賴乙方同事的情況下獨立查找資料完成。自信滿滿的我決定換份新工作,並得到了一個面試機會。第一輪面試考察的是一些常用功能的實現和對業務流程的了解程度,如我預想的那樣,自己順利通過。在第二輪面試中,對方問到"對模塊化的理解和實踐",這是個出人意料的問題,我努力地思考了一番,卻不知道該說什么,於是被客客氣氣請了出去…
經歷了這次失敗后的我,不斷地思考着模塊化設計。如果再次面對那個問題,也許我應該這樣回答:
定義
模塊是系統中獨立的、可替代的單元。模塊化設計,即是把系統分解為模塊的集合。模塊的形式多種多樣,可以是form、method、function Module、class、或report等。在理想的世界中,每個模塊都完全獨立於其它模塊:開發者在任何模塊中工作的時候,都不需要知道有關其它模塊的知識。在這種理想狀態下,系統復雜度取決於系統中復雜度最高的模塊。
當然,實踐與理想不同,系統模塊間總會多少有些依賴。當一個模塊變化時,其它模塊可能也需要隨之而改變。模塊化設計的目標就是最小化模塊間的依賴。
為了管理依賴,我們可以把模塊看成兩部分:接口和實現。
接口包含了全部的在調用該模塊時需要的信息。接口只描述模塊做什么,但不會包含怎么做。
完成接口所做出的承諾的代碼被稱為實現。
接口中包含2種信息:正式的和非正式的。
正式的信息在代碼中被顯式指定,程序語言可以檢查其中的部分正確性。比如,方法的簽名就是正式的信息,它包含參數的名稱和類型,返回值的類型,異常的信息。很多程序語言可以保證代碼中對方法的調用提供了與方法定義相匹配的參數值。
接口里面也包含非正式的元素。非正式部分無法被程序語言理解或強制執行。接口的非正式部分包含一些高層行為,比如函數會根據某個參數的內容刪除具有相應名字的文件。如果某個類的使用存在某種限制,比如方法的調用需要符合特定順序,那這也屬於接口的一部分。凡是開發者在使用模塊時需要了解的信息,都可以算作模塊接口的一部分。接口的非正式信息只能通過注釋等方式描述,程序語言無法確保描述是完整而准確的。大部分接口的非正式信息都比正式信息要更多、更復雜。
正式的信息和非正式的信息都是復雜度的來源,清晰的接口定義有助於開發者了解在使用模塊時需要知道的信息,從而避免一些問題。
示例
以function module為例,在function module編輯器中看到的function module名,和前6個標簽,加上function module文檔(如果有),都屬於它的接口。而source code中的代碼,則屬於實現。如下圖

當然,如果該function包含任何隱性的使用信息,它也算做接口的一部分。比如,如果使用SAVE_TEXT保存長文本,通常要有一個COMMIT操作來提交修改。“需要使用COMMIT提交修改”,同樣是SAVE_TEXT的接口的一部分。
進行模塊化設計,是減小代碼復雜度的第一步。因為,開發者在進行模塊內部開發時,只需要關注 當前模塊的接口+當前模塊的實現+其它相關模塊的接口。他只需要關注整個軟件系統的一小部分,接觸的東西變少,會使理解工作內容的速度大大增加,也會使犯錯的機會變小。對於試圖理解當前系統的部分功能而不做修改的人,這種設計同樣會減輕人們的負擔,因為人們通常只需要通過模塊們的接口來了解程序的功能。
2, 減少異常的影響
經驗較淺的程序員容易犯的一個錯誤是,只考慮程序中的正常情況,即所謂的happy path,而沒有(足夠多地)考慮異常情形。不周全的考慮可以讓程序員快速完成功能,但是接下來則會導致測試中的頻繁翻車,程序員不得不再對程序進行種種的修補,導致代碼整體的復雜度迅速升高。此外,即便是在一開始已經考慮到了各種異常情形,為了處理它們,也會給程序增加一定的復雜度。本節內容的主題是,如何合適地盡量減小由異常情形引起的復雜度。下面介紹具體的三種辦法,
從概念上消除異常
第一種辦法是從概念上消除異常。異常是相對正常而言的,功能的定義可以影響到異常的定義。ABAP SQL有插入語句,代碼如下,
INSERT ztable FROM TABLE @lt_something.
但是,開發者通常不得不考慮主鍵重復引起的異常處理。所以在這一語句后面,還要檢查sy-subrc返回值,根據判斷做進一步處理...代碼因此變得復雜。
如何避免此處的異常處理?假設我們對功能的設定做出一些改變,從“把內表的數據插入數據庫”改為“保證數據庫中存在內表的數據”,在這個新功能的內部判斷插入語句的執行情況,如果因為主鍵重復導致插入失敗,則改為按主鍵更新數據庫表,此時不再需要在調用時進行相關異常處理。
說到這里讀者已經知道,這個“新功能”就是ABAP中的關鍵字MODIFY,
MODIFY ztable FROM TABLE @lt_something.
使用MODIFY而不是INSERT的話,一種常見異常的定義便消失了,代碼的總量和復雜度因此會減少。當然,前提是MODIFY的功能和需求相匹配。有些資深開發者因為害怕新人不了解MODIFY的原理而禁止他們使用這個關鍵字,是因噎廢食的做法。
隱藏異常
把異常隱藏在較低層面是第二種做法,這種做法可以使高層的代碼在不需要了解異常存在的情況下進行工作,從而減少高層的復雜度。SAP系統中的一個例子是tRFC。
對於tRFC而言,遠端系統不需要在RFC客戶端程序運行tRFC的時候可用。tRFC組件將被調用的RFC函數和相關數據存儲在SAP系統的數據庫里,包含一個唯一的事務標識符(transaction identifier,TID)。如果調用發送了,接收系統卻是宕機狀態,調用會保留在本地隊列中一段時間。調用對話程序可以在不等待遠程調用成功/失敗的情況下繼續運行。如果接收系統在一段時間后仍然不可用,調用將被計划為后台作業運行。
在tRFC的例子中,高層調用者不需要了解對方系統的狀態,也不需要進行傳輸失敗的處理,這一切都由低層完成了。高層程序的復雜度也會因此得到控制。
聚合異常
與其分散地處理程序中不同部分產生的異常,不如把它們集中交給高層的程序,進行統一的處理。SAP系統中的一個例子是BAPI。絕大多數BAPI使用一個名為RETURN參數返回所有的錯誤,這樣一來就可以由高層調用者對可能產生的錯誤進行統一的處理,而不是在產生錯誤的地方進行分散、個別的處理。
3, 純函數

“Hi 氫氦,我在測試中遇到了這個錯誤消息,麻煩你看一下原因。”
“什么?這和我們的修改毫無關系,這個報錯屬於B功能,而你知道我們改的只是程序的A功能,!”
“是的,但是我得保證這次修改沒有影響到B功能,所以請你調試調查原因。”
測試的一個難題是測試者不知道看似單純的修改會帶來什么樣的復雜問題,於是只好求助於人工檢查,開發者往往就是那個不幸的工人,使用純函數可以幫助避免這類情況的發生。
定義
最近流行的函數式編程十分強調純函數的概念。純函數是指符合以下條件的函數,
- 對於相同的輸入,函數總有相同的輸出。
這要求函數內部不能存在“副作用”。
它的輸出結果的確定不應該依賴輸入參數外的任何內容,例如,不可以因為本地測試環境中沒有相應的數據庫就產生“連接數據庫異常”導致無法返回結果。
它也不應該改變除了返回結果以外的任何內容,例如,不可以改變全局可變狀態。
滿足以上條件的函數,可以被稱為純函數。
從模塊化的角度來看,全局狀態和對外部系統的連接都屬於接口的一部分。純函數不會與這些東西產生交互,因此它的接口會更簡單,復雜度更低。
雖然ABAP不是函數式語言,但它依然可以有純函數,並且開發者可以通過寫純函數而受益。
在上面的例子中,如果開發者可以證明A、B功能分屬2個模塊,而且它們都屬於純函數,那么只要證明A的變更不會改變B的輸入,即可證明修改沒有導致對方給出的錯誤。
4, 需求文檔與實現
系統中的模塊可以分為接口和實現,換一個角度思考的話,代碼也可以看作需求文檔的實現。需求語言的准確性,對代碼產物的質量有着直接的影響。在SAP開發中,業務邏輯的實現是首要目標,業務復雜度也往往是代碼復雜度的最主要來源。
信息丟失
程序是需求的實現,因此代碼應當盡量包含需求文檔中的信息。在需求文檔的質量可靠的前提下,這樣做可以有效提高程序的可讀性,從而降低其復雜度。信息丟失的一個極端例子是代碼混淆,顯然混淆后的代碼復雜度將大大升高。
當然,程序語言和系統內部的信息和需求文檔中的自然語言是有差別的,這也是程序員存在的意義。在實際的開發中,可以嘗試通過增加一個抽象層的方式來保留需求文檔中的信息。比如,在高層對需求語言進行建模,在低層實現實際的執行過程。
注意:如果需求文檔很長,代碼實現很少的話,意味着工作可能存在某些問題,可能是需求內容冗余、功能設計不合理、實現不完整、信息丟失等,此時要重新思考相關工作內容,以確保未出現這些問題。
詞匯表
保留信息的前提條件是明確信息,需求文檔中應當有詞匯表,幫助開發人員迅速捕捉和理解最重要的業務語言,以便把它們落實在程序中。
對於在中文環境下的工作而言,這點尤其重要。ABAP並不適合使用中文命名,而普通的程序員沒有能力把中文的業務語言轉換成英文的,需要由業務顧問來完成這項意義重大的工作。
5,工作轉移
最后,還有一種顯而易見的辦法是將某些業務邏輯轉移到ABAP之外實現,比如下推到數據庫層面、使用配置工具實現(BRF+)、交給中間件等。
優點:從ABAP角度來看,這種辦法最有效地避免了復雜度的增加。
缺點:從全局來看,復雜度並沒有真正消失,只是隨着工作量轉移到了另一個地方。
要從系統的整體復雜度的角度來考慮實現邏輯的位置,比如,人們常常使用配置表配合ABAP代碼來實現自定義邏輯,BRF+中的decision table可以實現相似的功能,如果需求希望得到用戶在實際使用程序時對不同邏輯的命中率的話,那么decision table可能會更合適一些,因為BRF+中包含跟蹤模式,可以被直接利用,這樣就可以通過寫簡單的代碼來得到結果,避免引入更多的復雜度。
后記
長期關注本博客的讀者可能會注意到,這個博客已經有段時間沒有更新ABAP相關內容,這是因為從今年開始,我的工作重心已經逐漸轉移到Spark和Dynamics等其它方面的開發,花在SAP上面的時間變得很少。恰好最近參加了一個新的SAP項目,它喚醒了我對ABAP的一些記憶,於是趁熱打鐵,寫了這篇文章。這篇文章算是在ABAP角度上將最近學習到的東西進行的一個總結復習。希望它能對讀者有所幫助,也真誠地希望能收到反饋,共同討論相關話題。
參考鏈接:《A Philosophy of Software Design》
