日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作 者 | 王洋(古訓)

導語:本文重點圍繞軟件復雜度進行剖析,希望能夠幫助讀者對軟件復雜度成因和度量方式有所了解。

前言

 

大型系統的本質問題是復雜性問題。互聯網軟件,是典型的大型系統,如下圖所示,數百個甚至更多的微服務相互調用/依賴,組成一個組件數量大、行為復雜、時刻在變動(發布、配置變更)當中的動態的、復雜的系統。而且,軟件工程師們常常自嘲,“when things work, nobody knows why”。

 

本文將重點圍繞軟件復雜度進行剖析,希望能夠幫助讀者對軟件復雜度成因和度量方式有所了解,同時,結合自身的實踐經驗談談我們在實際的開發工作中如何盡力避免軟件復雜性問題。

導致軟件復雜度的原因

導致軟件復雜度的原因是多種多樣的。

宏觀層面講,軟件復雜是伴隨著需求的不斷迭代日積月累的必然產物,主要原因可能是:

1.對代碼腐化的退讓與一直退讓。

2.缺乏完善的代碼質量保障機制。如嚴格的CodeReview、功能評審等等。

3.缺乏知識傳遞的機制。如無有效的設計文檔等作為知識傳遞。

4.需求的復雜性導致系統的復雜度不斷疊加。比如:業務要求今天A這類用戶權益一個圖標展示為??,過了一段時間,從A中切分了一部分客戶要展示。

對于前三點我覺得可以通過日常的工程師文化建設來盡量避免,但是隨著業務的不斷演化以及人員的流動、知識傳遞的缺失,長期的疊加之下必然會使得系統越發的復雜。此時,我覺得還需要進行系統的重構。

從軟件開發微觀層面講,導致軟件復雜的原因概括起來主要是兩個:依賴(dependencies) 和 隱晦(obscurity)。

依賴會使得修改過程牽一發而動全身,當你修改模塊一的時候,也會牽扯到模塊二、模塊三等等的修改,進而容易導致系統bug。而隱晦會讓系統難于維護和理解,甚至于在出現問題時難于定位問題的根因,要花費大量的時間在理解和閱讀歷史代碼上面。

軟件的復雜性往往伴隨著如下幾種表現形式:

修改擴散

修改時有連鎖反應,通常是因為模塊之間耦合過重,相互依賴太多導致的。比如,在我們認證系統中曾經有一個判斷權益的接口,在系統中被引用的到處都是,這種情況會導致一個嚴重問題,今年這個接口正好面臨升級,如果當時沒有抽取到一個適配器中去,那整個系統會有很多地方面臨修改擴散的問題,而這樣的變更比較抽取到適配器的修改成本是更高更風險的。

@Override
public boolean isAllowed(Long accountId, Long personId, String featureName) {
boolean isPrivilegeCheckedPass = privilegeCheckService.isAllowed(
accountId, personId, featureName);
return isPrivilegeCheckedPass;
}

認知負擔

當我們說一個模塊隱晦、難以理解時,它就有過重的認知負擔,開發人員需要較長的時間來理解功能模塊。比如,提供一個沒有注釋的計算接口,傳入兩個整數得到一個計算結果。從函數本身我們很難判斷這個接口是什么功能,所以此時就不得不去閱讀內部的實現以理解其接口的功能。

int calculate(int v1, int v2);

不可知(Unknown Unknowns)

相比于前兩種癥狀,不可知危險更大,在開發需求時,不可知的改動點往往是導致嚴重問題的主要原因,常常是因為一些隱晦的依賴導致的,在開發完一個需求之后感覺心里很沒譜,隱約覺得自己的代碼哪里有問題,但又不清楚問題在哪,只能祈禱在測試階段能夠暴露出來。

軟件復雜度度量

Manny Lehman教授在軟件演進法則中首次系統性提出了軟件復雜度:

軟件(程序)復雜度是軟件的一組特征,它由軟件內部的相互關聯引起。隨著軟件的實體(模塊)的增加,軟件內部的相互關聯會指數式增長,直至無法被全部掌握和理解。

軟件的高復雜度,會導致在修改軟件時引入非主觀意圖的變更的概率上升,最終在做變更的時候更容易引入缺陷。在更極端的情況下,軟件復雜到幾乎無法修改。

在軟件的演化過程中,不斷涌現了諸多理論用于對軟件復雜度進行度量,比如,Halstead 復雜度、圈復雜度、John Ousterhout復雜度等等。

Halstead 復雜度

Halstead 復雜度(霍爾斯特德復雜度量測) (Maurice H. Halstead, 1977) 是軟件科學提出的第一個計算機軟件的分析“定律”,用以確定計算機軟件開發中的一些定量規律。Halstead 復雜度根據程序中語句行的操作符和操作數的數量計算程序復雜性。針對特定的演算法,首先需計算以下的數值:

 

  •  

    為不同運算子(操作符)的個數。

     

  •  

    為不同運算元(操作數)的個數。

     

  •  

    為所有運算子合計出現的次數。

     

  •  

    為所有運算元合計出現的次數。

     

 

上述的運算子包括傳統的運算子及保留字,運算元包括變數及常數。

依上述數值,可以計算以下的量測量:

舉一個,這是一段我們當前應用中接入AB實驗的適配代碼:

try {
DiversionRequest diversionRequest = new DiversionRequest();
diversionRequest.setDiversionKey(diversionKey);

if (MapUtils.isNotEmpty(params)) {
DiversionCondition condition = new DiversionCondition();
condition.setCustomConditions(params);
diversionRequest.setCondition(condition);
}
ABResult result = xsABTestClient.ab(testKey, diversionRequest);
if (result == null || !result.getSuccess()) {
return null;
}

return result.getDiversionResult();
} catch (Exception ex) {
log.error("abTest error, testKey:{}, diversionKey:{}", testKey, diversionKey, ex);
throw ex;
}

我們梳理這段代碼中的預算子和運算元以及分別統計出其個數:

運算子(操作符)

運算子出現次數

運算元(操作數)

運算元出現次數

1

try

1

diversionRequest

4

2

catch

1

params

2

3

if

2

condition

3

4

MapUtils.isNotEmpty

1

testKey

2

5

1

result

2

6

3

result.getSuccess

1

7

1

result.getDiversionResult

1

8

1

diversionKey

2

9

return

2

null

2

10

throw

1

abTest error, testKey:{}, diversionKey:{}

1

11

xsABTestClient.ab

1

ex

3

12

log.error

1

根據統計上面統計得到的對應的數據我們進行計算:

Halstead 方法優點

1.不需要對程序進行深層次的分析,就能夠預測錯誤率,預測維護工作量;

2.有利于項目規劃,衡量所有程序的復雜度;

3.計算方法簡單;

4.與所用的高級程序設計語言類型無關。

Halstead 方法的缺點

1.僅僅考慮程序數據量和程序體積,不考慮程序控制流的情況;

2.不能從根本上反映程序復雜性。給我的直觀感受是他能夠對軟件復雜性進行度量,但是很難講清楚每一部分代碼是好還是壞。

圈復雜度

圈復雜度(Cyclomatic complexity)是一種代碼復雜度的衡量標準,在1976年由Thomas J. McCabe, Sr. 提出。

在軟件測試的概念里,圈復雜度用來衡量一個模塊判定結構的復雜程度,數量上表現為線性無關的路徑條數,即合理的預防錯誤所需測試的最少路徑條數。圈復雜度大說明程序代碼可能質量低且難于測試和維護,根據經驗,程序的可能錯誤和高的圈復雜度有著很大關系,一般來說,圈復雜度大于10的方法存在很大的出錯風險。

圈復雜度

代碼狀況

可測性

維護成本

1~10

清晰

10~20

復雜

20~30

非常復雜

>30

不可讀

不可測

非常高

計算方法:

計算公式1:V(G)=e-n+2。其中,e表示控制流圖中邊的數量,n表示控制流圖中節點的數量。

計算公式2:V(G)=區域數=判定節點數+1。圈復雜度所反映的是“判定條件”的數量,所以圈復雜度實際上就是等于判定節點的數量再加上1,也即控制流圖的區域數。

計算公式3:V(G)=R。其中R代表平面被控制流圖劃分成的區域數。

舉個,以前面AB實驗的代碼片段為例子,畫出流程圖如下,通過計算得出其圈復雜度為4:

流程圖

John Ousterhout的復雜度定義

John Ousterhout(約翰歐斯特霍特),在他的著作《A Philosophy of Software Design》中提出,軟件設計的核心在于降低復雜性。他選擇從認知的負擔和開發工作量的角度來定義軟件的復雜性,并且給出了一個復雜度量公式:

子模塊的復雜度乘以該模塊對應的開發時間權重值,累加后得到系統的整體復雜度C。系統整體的復雜度并不簡單等于所有子模塊復雜度的累加,還要考慮開發維護該模塊所花費的時間在整體時間中的占比(對應權重值)。也就是說,即使某個模塊非常復雜,如果很少使用或修改,也不會對系統的整體復雜度造成大的影響。

如何避免復雜度問題

軟件復雜度問題可以完全避免么?我覺得不可能,但是這并不能成為我們忽視軟件復雜度的理由,有很多措施可以幫助我們盡量避免自身的需求開發或工作中引入問題代碼而導致軟件復雜。這里結合日常的開發理解談一下自己的認知:

1.開發前:我們可以通過需求梳理沉淀需求分析、架構設計等文檔作為知識傳遞的載體。

2.開發中:我們需要強化系統架構理解,戰略優先于戰術,系統分層架構清晰統一,開發中接口設計要做到高內聚和低耦合同時保持良好代碼注釋的習慣。

3.維護階段:我們可以進行代碼重構,針對之前存在設計問題的代碼,以新的思維和架構實現方案進行重構使得代碼越來越清晰。

戰略先于戰術

在戰術編程中,開發者主要關注點是能夠work,比如修復一個bug或者增加一段兼容邏輯。乍一看,代碼能夠work,功能也得到了修復,然而,戰術編程已經為系統設計埋下了壞的味道,只是還沒人察覺,當相同的代碼交接給后人的時候,經常會聽到一句“屎山一樣的代碼”,這就是以戰術編程長期累積的結果,是短視的,缺乏宏觀設計導致系統不斷的引入復雜性問題以至于代碼很容易變得隱晦。

 

成為一名優秀的軟件設計師的第一步是認識到僅僅為了完成工作編寫代碼是不夠的。為了更快地完成當前的任務而引入不必要的復雜性是不可接受的。最重要的是這個系統的長期結構。 --John Ousterhout(約翰歐斯特霍特),《A Philosophy of Software Design》

 

目前我們所維護的系統往往都是在前人代碼的基礎上進行升級和擴展,日常需求開發工作中,一個重要的工作是借助需求開發的契機,推動需求所涉及到壞味道的設計能夠面向未來擴展,而非僅僅著眼于完成當前的需求,這就是我理解的戰略編程。

舉一個,有一個消息監聽的處理邏輯,根據不同的業務執行對應的業務處理,其中一部分關鍵代碼如下,可以猜想按照戰術編程的思路以后會還會有無數的else if在后面進行拼接實現,而這里完全可以通過策略模式的方式進行簡單的重構,使得后續業務接入時更加清晰和簡單。


public void receiveMessage(Message message, MessageStatus status) {
if(StringUtils.equals(authType, .NETouchChangeTypeParam.IC_INFO_CHANGE.getType())
|| StringUtils.equals(authType, OnetouchChangeTypeParam.SUB_COMPANY_CHANGE.getType())){
if(StringUtils.equals("success", authStatus)){
oneTouchDomainContext.getOneTouchDomain().getOnetouchEnableChangeDomainService().notifySuccess(userId.toString(), authRequestId);
} else if(StringUtils.equals(authType,AUTH_TYPE_INTL_CGS_ONSITE)){
// XXXXXX
} else if(StringUtils.equals(authType,AUTH_TYPE_INTL_CGS_ONSITE_CHANGE)) {
// XXXXXX
} else if (AUTH_TYPE_VIDEO_SHOOTING.equals(authType)) {
if (AUTH_STATUS_SUCCESS.equals(authStatus)) {
// XXXXXX
} else if (AUTH_STATUS_PASS.equals(authStatus)) {
// XXXXXX
} else if (AUTH_STATUS_SUBMIT.equals(authStatus)) {
// XXXXXX



 

短期來看戰略編程的成本會高于戰術編程,但是從上面的案例長期來看,這樣的成本是值得的,他能夠有效的降低系統的復雜度,從而長期來看最終能降低后續投入的成本。開發同學在需求迭代的過程中應該先通過戰略編程的思維進行設計和思考,然后再進行戰術實現,所以我的觀點是戰略設計要優先于戰術實現。

高內聚低耦合設計

高內聚低耦合,是判斷軟件設計好壞的標準,主要用于程序的面向對象的設計,主要看類的內聚性是否高,耦合度是否低。目的是使程序模塊的可重用性、移植性大大增強。通常程序結構中各模塊的內聚程度越高,模塊間的耦合程度就越低,當模塊內聚高耦合低的情況下,其內部的腐化問題不容易擴散,從而帶給系統本身的好處就是復雜度的降低。

內聚是從功能角度來度量模塊內的聯系,好的內聚模塊應當做好一件事情,它描述了模塊內部的功能聯系;而耦合是軟件結構中各模塊之間相互連接的一種度量,耦合強弱取決于模塊間接口的依賴程度,如調用一個模塊的點以及通過接口的數據等。那么如何實現一個高內聚低耦合的接口呢?

簡化接口設計

簡單的接口往往意味著調用者使用更加方便,如果我們為了實現簡單,提供一個復雜的接口給外部使用者,此時往往帶來的是耦合度增大,內聚降低,繼而當該接口出現升級等場景時會產生修改擴散的問題,進而影響面發生擴散,帶來一定的隱患。

因此,在模塊設計的時候,要盡量遵守把簡單留給別人,把復雜留給自己的原則。

比如這樣一個例子,下面兩段代碼實現的是同樣的邏輯,方法一的設計明顯要由于方法二,為什么?方法一更簡單,而方法二明顯違背了把簡單留給別人,把復雜留給自己的原則。如果你是接口的使用者當你使用方法二的時候,你一定會遇到的兩個問題:第一,需要傳遞哪些參數要靠問方法的提供方,或者要看方法的內部實現;第二,你需要在取到返回值后從返回值中解析自己想要的結果。這些問題無疑會讓系統復雜度提升。

所以,我們要簡化接口設計,把簡單留給別人,把復雜留給自己,從而保證接口的高內聚和低耦合,進而降低系統的復雜度。


@Override
public boolean createProcess(StartProcessDto startProcessDto) {
// XXXXXXX

 

@Override
public HashMap createProcess(HashMap dataMap) {
// XXXXXXX
}


隱藏實現細節

隱藏細節指的就是只給調用者暴露重要的信息,把不重要的細節隱藏起來。接口設計時,我們要通過接口告訴使用者我們需要哪些信息,同時也要通過接口告訴使用者我會給到你哪些信息,至于內部如何實現使用者不需要關心的。

還是以上面的接口的實現為例子,方法一對內部實現細節達到了屏蔽,使得當前接口具備更好的內聚性,當內部實現的服務需要調整時只需要修改內部的實現即可,而方法二則不然。通過這個案例也能夠實際體會到,把內部的實現細節隱藏在實現方的內部能夠有效的提升接口的內聚性降低系統耦合,隨之帶來的是系統復雜度的降低。


@Override
public boolean createProcess(StartProcessDto startProcessDto) {
Validate.notNull(startProcessDto);
try {
HashMap dataMap = new HashMap<>(8);
dataMap.put(MEMBER_ID, startProcessDto.getMemberId());
dataMap.put(CUSTOMER_NAME, startProcessDto.getCustomerName());
dataMap.put(GLOBAL_ID, startProcessDto.getGlobalId());
dataMap.put(REQUEST_ID, startProcessDto.getAvRequestId());
String authType = startProcessDto.getAuthType();
String taskCode = getTaskCode(authType);

 

HashMap resultMap = esbCommonTaskService.createProcess(AV_ORIGIN_AV, taskCode, dataMap);
return (MapUtils.isNotEmpty(resultMap) && TRUE.equals(resultMap.get(IS_SUCCESSED)));
} catch (Exception e) {
LOGGER.error("createProcess error. startProcessDto:{}",
JSON.toJSONString(startProcessDto), e);
throw e;
}
}

@Override
public HashMap createProcess(HashMap dataMap) {
Validate.notNull(dataMap);
try {
HashMap process = esbCommonTaskService.createProcess(ORIGIN_AV, TASK_CODE, dataMap);
return process;
} catch (Exception e) {
LOGGER.error("createProcess error. dataMap:{}", JSON.toJSONString(dataMap), e);
throw e;
}
}


通用接口設計

通用接口設計并不是說所有的場景都為了通用而設計,而是針對具有同樣能力的多套實現代碼而言,我們可以抽取成通用的接口設計,通過業務類型等標識區分實現一個接口完成。

舉一個例子,有一個需求是同時實現多種會員的權益列表功能,由于不同會員的權益并不完全相同,所以剛開始的想法是分開設計不同的接口來承接不同會員的權益內容的獲取,但是本質上實現的是同樣的內容:查詢會員權益,所以最終通過對領域模型的重構抽取了統一的模型從而實現了通用的權益查詢的接口。


public List getRights(RightQueryParam rightQueryParam) {
// 參數校驗
checkParam(rightQueryParam);
Locale locale = LocaleUtil.getLocale(rightQueryParam.getLocale());
// 查詢商家權益
RightHandler rightHandler = rightHandlerConfig.getRightHandler(rightQueryParam.getMemberType());
if (rightHandler == null) {
log.error("getRightHandler error, not found handler, rightQueryParam:{}", rightQueryParam);
throw new BizException(ErrorCode.NOT_EXIST);
List rightEList = rightHandler.getRights(rightQueryParam.getAliId(), locale);
return rightEList;

 

分層架構

從經典的三層架構到領域驅動設計都有涉及到分層架構,分層架構的核心其實我理解是隔離,將不同職責的對象劃分到不同的層中實現,良好的分層能夠實現軟件內部復雜度問題的隔離,降低“洪泛”效應。

端口適配器架構將系統劃分為內部(業務邏輯)和外部(客戶請求/基礎設施層/外部系統)。主動適配器(Driving adapters)承接了外部請求,系統內部業務邏輯能對其進行主動適配,獨立于不同的調用方式提供通用的接口。被動適配器(Driven adapters)承接了內部業務邏輯調用外部系統的訴求,為了避免外部系統污染內部業務邏輯,通過適配屏蔽外部系統的底層細節,有利于內部業務邏輯的獨立性。在復雜軟件的開發過程中,很容易出現分層的混淆,逐漸出現分層不清晰,系統業務邏輯和交互UI/基礎設施等代碼邏輯逐漸耦合,導致業務邏輯被污染的問題,而端口適配器正是要解決該類問題。

六邊形架構

Onion Architecture(洋蔥架構,于2008年)由杰弗里 · 帕勒莫提出,洋蔥架構是建立在端口適配器架構的基礎上,將領域層放在應用的中心,外部化UI和基礎設施層(ORM,消息服務,搜索引擎)等,更進一步增加內部層次劃分。洋蔥模型將應用分層細化,抽取了應用服務層、領域服務層、領域模型層等,并且也明確了應用調用依賴的方向:

1.外層依賴于內層。

2.內層對外層無感知。

洋蔥架構

注釋與文檔

注釋與文檔往往在開發過程中會被忽視,作為知識傳遞的載體,其實是很重要的存在,他們能夠幫助我們更快速的理解實現邏輯。

注釋能夠幫助理解邏輯;注釋是開發過程中思維邏輯最直接的體現,因為其和代碼綁定在一起,相對于文檔閱讀更方便,查看和理解代碼時有助于理解。

文檔能夠幫助理解架構設計,在團隊的合作或者交接過程中,很難用幾句話就能夠講清楚,此時需要通過文檔幫助合作方來更好的理解每一處細節以及整體的架構設計方案的全貌。

重構

如果日常開發過程中已經很注意了,但是多年之后發現其實之前的實現并不是最優的,此時,就可以通過系統重構來解決。

當你維護一個多年生長成的系統時,一定會發現系統中一些不合理的地方,這是軟件復雜度問題長期積聚的結果,此時就需要我們在日常的開發過程中對系統內部的實現邏輯進行適當的重構以使得系統對未來具備更好的擴展性和可維護性。

 

重構:對軟件內部結構的一種調整,目的是在不改變軟件可觀察行為的前提下,提高其可理解性,降低其修改成本。使用一系列重構手法,在不改變軟件可觀察行為的前提下,調整結構。傻瓜都能寫出計算機可以理解的代碼。唯有能寫出人類容易理解的代碼的,才是優秀的程序員。 -- Martin Fowler 《重構 改善既有代碼的設計》

 

看一個簡化版本的例子,下面的代碼部分是一個查詢升金報告詳情數據的接口,會發現中間有一大段的信息是在轉換aliId,但實際上這個行為并不是當前方法的重點,所以這里的單純針對這一段我覺得應該單獨抽取一個公用的方法出來。


public ReportDetailDto getDetail(ReportQueryParam queryParam) {
if (null == queryParam) {
log.error("queryParam is null");
throw new BizException(PARAM_ERROR);
Long aliId = queryParam.getAliId();
if (null == aliId) {
if (StringUtils.isBlank(queryParam.getToken())) {
log.error("aliId and token are both null. queryParam: {}",
JSON.toJSONString(queryParam));
throw new BizException(PARAM_ERROR);
aliId = recommendAssistantServiceAdaptor.getAliIdByToken(queryParam.getToken());
if (null == aliId) {
log.error("cannot get aliId by token. queryParam: {}", JSON.toJSONString(queryParam));
throw new BizException("ALIID_NULL", "aliId is null");

 

// 獲取同步數據
// 數據結構轉換
return convertModel(itemEList);
}

總結

本文主要闡述了個人對軟件復雜度的思考,分析了導致軟件復雜度的原因、軟件復雜度的度量方式以及闡述了自我理解的如何避免軟件復雜度的問題。

只要每個人在每一個需求的開發中秉持匠心,持續提升自身架構設計的能力,先戰略設計后戰術實現,并針對開發過程中遇到的問題代碼能夠積極的進行重構,相信軟件復雜度的問題也會不斷的被我們擊潰,勝利的旗幟永遠屬于偉大的程序員。

參考:

《A Philosophy of Software Design》:https://www.amazon.com/-/zh/dp/173210221X/ref=sr_1_1?qid=1636246895

《Clean Architecture》:https://detail.tmall.com/item.htm?spm=ata.21736010.0.0.2e637536hX3Gji&id=654392764249

分享到:
標簽:軟件
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定