農林漁牧網

您現在的位置是:首頁 > 畜牧業

學會這10個設計原則,離架構師又進了一步!

2021-10-18由 酷扯兒 發表于 畜牧業

架構指的是什麼

「來源: |菜根老譚 ID:CGLT_TAN」

閒言碎語:

一個懂設計原則的程式猿,寫出來的程式碼可擴充套件性就是強,後續的人看程式碼如沐春風。相反,如果程式碼寫的跟流水賬似的,完全一根筋平鋪下來,後續無論換誰接手維護都要罵娘。

做軟體開發多年,CRUD彷彿已經形成一種慣性,深入骨髓,按照常規的結構拆分:表現層、業務邏輯層、資料持久層,一個功能只需要個把小時程式碼就擼完了。

再結合CTRL+C和CTRL+V 絕世秘籍,一個個功能點便如同雨後春筍般被快速克隆實現。

是不是有種雄霸天下的感覺,管他什麼業務場景,大爺我一梭到底,天下無敵!!!

學會這10個設計原則,離架構師又進了一步!

可現實真的是這樣?

答案不言而喻!!!

初入軟體行業,很多人都會經歷這個階段。時間久了,很多人便產生困惑,能力並沒有隨著工作年限得到同比提升,焦慮失眠,如何改變現狀?

悟性高的人,很快能從一堆亂麻中找到線索,並不斷的提升自己的能力。

什麼能力?

當然是軟體架構能力,一名優秀的軟體架構師,要具備複雜的業務系統的吞吐設計能力、抽象能力、擴充套件能力、穩定性。

如何培養這樣能力?

學會這10個設計原則,離架構師又進了一步!

我將常用的軟體架構原則,做了彙總,目錄如下:

學會這10個設計原則,離架構師又進了一步!

當然這些原則有些是相互輔助,有些是相互矛盾的。實際專案開發中,要根據具體業務場景,靈活應對。千萬不能教條主義,生搬硬套

單一職責

我們在編碼的時候,為了省事,總是喜歡在一個類中新增各種各樣的功能。未來業務迭代時,再不斷的修改這個類,導致後續的維護成本很高,耦合性大。牽一髮而動全身。

為了解決這個問題,我們在架構設計時通常會考慮單一職責

定義:

單一職責(SRP:Single Responsibility Principle),面向物件五個基本原則(SOLID)之一。每個功能只有一個職責,這樣發生變化的原因也會只有一個。透過縮小職責範圍,儘量減少錯誤的發生。

單一職責原則和一個類只幹一件事之間,最大的差別就是,將變化納入了考量。

程式碼要求:

一個介面、類、方法只負責一項職責,簡單清晰。

優點:

降低了類的複雜度,提高類的可讀性、可維護性。進而提升系統的可維護性,降低變更引起的風險。

示例:

有一個使用者服務介面UserService,提供了使用者註冊、登入、查詢個人資訊的方法,主要還是圍繞使用者相關的服務,看似合理。

public interface UserService{

// 註冊介面

Object register(Object param);

// 登入介面

Object login(Object param);

// 查詢使用者資訊

Object queryUserInfoById(Long uid);

}

過了幾天,業務方提了一個需求,使用者可以參加專案。簡單的做法是在UserService類中增加一個joinProject()方法

又過了幾天,業務方又提了一個需求,統計一個使用者參加過多少個專案,我們是不是又在UserService類中增加一個countProject()方法。

這樣導致的後果是,UserService類的職責越來越重,類會不斷膨脹,內部的實現會越來越複雜。既要負責使用者相關還有負責專案相關,後續任何一塊業務變動,都會導致這個類的修改。

兩類不同的需求,都改到同一個類。正確做法是,把不同的需求引起的變動拆分開,單獨構建一個ProjectService類,專門負責專案相關的功能

public interface ProjectService{

// 加入一個專案

void addProject (Object param);

// 統計一個使用者參加過多少個專案

void countProject(Object param);

}

這樣帶來的好處是,使用者相關的需求只要改動UserService。如果是專案管理的需求,只需要改動ProjectService。二者各自變動的理由就少了很多。

開閉原則

開閉原則(OCP:Open-Closed Principle),主要指一個類、方法、模組 等

對擴充套件開放,對修改關閉

。簡單來講,一個軟體實體應該透過擴充套件來實現變化,而不是透過修改已有的程式碼來實現變化。

個人感覺,開閉原則在所有的原則中最重要,像我們耳熟能詳的23種設計模式,大部分都是遵循開閉原則,來解決程式碼的擴充套件性問題。

實現思路:

採用抽象構建框架主體,用實現擴充套件細節。不同的業務採用不用的子類,儘量避免修改已有程式碼。

優點:

可複用性好。在軟體完成以後,仍然可以對軟體進行擴充套件,加入新的功能,非常靈活。因此,這個軟體系統就可以透過不斷地增加新的元件,來滿足不斷變化的需求。

可維護性好。它的底層抽象相對固定,不用擔心軟體系統中原有元件的穩定性,這就使變化中的軟體系統有一定的穩定性和延續性。

示例:

比如有這樣一個業務場景,我們的電商支付平臺,需要接入一些支付渠道,專案剛啟動時由於時間緊張,我們只接入微信支付,那麼我們的程式碼這樣寫:

class WeixinPay {

public Object pay(Object requestParam) {

// 請求微信完成支付

// 省略。。。。

return new Object();

}

}

隨著業務擴充套件,後期開始逐步接入一些其他的支付渠道,比如支付寶、雲閃付、紅包支付、零錢包支付、積分支付等,要如何迭代?

class PayGateway {

public Object pay(Object requestParam) {

if(微信支付){

// 請求微信完成支付

// 省略。。。。

}esle if(支付寶){

// 請求支付寶完成支付

// 省略。。。。

}esle if(雲閃付){

// 請求雲閃付完成支付

// 省略。。。。

}

// 其他,不同渠道的個性化引數的抽取,轉換,適配

// 可能有些渠道一次支付需要多次介面請求,獲取一些前置準備引數

// 省略。。。。

return new Object();

}

}

所有的業務邏輯都集中到一個方法中,每一個支付渠道本身的業務邏輯又相當複雜,隨著更多支付渠道的接入,pay方法中的程式碼邏輯會越來越重,維護性只會越來越差。每一次改動都要回歸測試所有的支付渠道,勞民傷財。那麼有沒有什麼好的設計原則,來解決這個問題。我們可以嘗試按

開閉原則

重新編排程式碼

首先定義一個支付渠道的抽象介面類,把所有的支付渠道的骨架抽象出來。

設計一系列的插入點,並對若干插入點流程關聯。

關於插入點,用過OpenResty的同學都知道,透過set_by_lua、rewrite_by_lua、body_filter_by_lua 等不同階段來處理請求在對應階段的邏輯,有效的避免各種衍生問題。

abstract class AbstractPayChannel {

public Object pay(Object requestParam) {

// 抽象方法

}

}

逐個實現不同支付渠道的子類,如:AliayPayChannel、WeixinPayChannel,每個渠道都是獨立的,後期如果做渠道升級維護,只需修改對應的子類即可,降低修改程式碼的影響面。

class AliayPayChannel extends AbstractPayChannel{

public Object pay(Object requestParam) {

// 根據請求引數,如果選擇支付寶支付,處理後續流程

// 支付寶處理

}

}

class WeixinPayChannel extends AbstractPayChannel{

public Object pay(Object requestParam) {

// 根據請求引數,如果選擇微信支付,處理後續流程

// 微信處理

}

}

總排程入口,遍歷所有的支付渠道,根據requestParam裡的引數,判斷當前渠道是否處理本次請求。

當然,也有可能採用組合支付的方式,比如,紅包支付+微信支付,可以透過上下文引數,傳遞一些中間態的資料。

class PayGateway {

List payChannelList;

public Object pay(Object requestParam) {

for(AbstractPayChannel channel:payChannelList){

channel。pay(requestParam);

}

}

}

里氏替換

里氏替換原則(LSP:Liskov Substitution Principle):所有引用基類的地方必須能透明地使用其子類的物件

簡單來講,子類可以擴充套件父類的功能,但不能改變父類原有的功能(如:不能改變父類的入參,返回),跟面向物件程式設計的多型性類似。

多型是面向物件程式語言的一種語法,是一種程式碼實現的思路。而里氏替換是一種設計原則,是用來指導繼承關係中子類如何設計,子類的設計要保證在替換父類的時候,不改變原有程式的邏輯以及不破壞原有程式的正確性。

實現思路:

子類可以實現父類的抽象方法

子類中可以增加自己特有的方法。

當子類的方法過載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入引數更寬鬆。

當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

介面隔離

介面隔離原則(ISP:Interface Segregation Principle)要求程式設計師儘量將臃腫龐大的介面拆分成更小的和更具體的介面,讓介面中只包含呼叫方感興趣的方法,而不應該強迫呼叫方依賴它不需要的介面。

實現思路:

介面儘量小,但是要有限度。一個介面只服務於一個子模組或業務邏輯。

為依賴介面的類定製服務。只提供呼叫者需要的方法,遮蔽不需要的方法。

結合業務,因地制宜。每個專案或產品都有特定的環境因素,環境不同,介面拆分的標準就不同,需要我們有較強的業務 sense

提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。

示例:

使用者中心封裝了一套UserService介面,給上層呼叫(業務端以及管理後臺)提供使用者基礎服務。

public interface UserService{

// 註冊介面

Object register(Object param);

// 登入介面

Object login(Object param);

// 查詢使用者資訊

Object queryUserInfoById(Long uid);

}

但隨著業務衍化,我們需要提供一個刪除使用者功能,常規的做法是直接在UserService介面中增加一個deleteById方法,比較簡單。

但這樣會帶來一個安全隱患,如果該方法被普通許可權的業務方誤呼叫,容易導致誤刪使用者,引發災難。

如何避免這個問題,我們可以採用

介面隔離的原則

定義一個全新的介面服務,並提供deleteById方法,BopsUserService介面只提供給Bops管理後臺系統使用。

public interface BopsUserService{

// 刪除使用者

Object deleteById(Long uid);

}

總結一下,在設計微服務介面時,如果其中一些方法只限於部分呼叫者使用,我們可以將其拆分出來,獨立封裝,而不是強迫所有的呼叫方都能看到它。

依賴倒置

軟體設計中的細節具有多變性,

但是抽象相對穩定

,為了利用好這個特性,我們引入了依賴倒置原則。

依賴倒置原則(DIP:Dependence Inversion Principle):高層模組不應直接依賴低層模組,二者應依賴於抽象;

抽象不應該依賴實現細節;而實現細節應該依賴於抽象。

依賴倒置原則的主要思想是要面向介面程式設計,不要面向具體實現程式設計。

示例:

定義一個訊息傳送介面MessageSender,具體的例項Bean注入到Handler,觸發完成訊息的傳送。

interface MessageSender {

void send(Message message);

}

class Handler {

@Resource

private MessageSender sender;

void execute() {

sender。send(message);

}

}

假如訊息的傳送採用Kafka訊息中介軟體,我們需要定義一個KafkaMessageSender實現類來實現具體的傳送邏輯。

class KafkaMessageSender implements MessageSender {

private KafkaProducer producer;

public void send(final Message message) {

producer。send(new KafkaRecord<>(“topic”, message));

}

}

這樣實現的好處,將高層模組與低層實現解耦開來。假如,後期公司升級訊息中介軟體框架,採用Pulsar,我們只需要定義一個PulsarMessageSender類即可,藉助Spring容器的@Resource會自動將其Bean例項依賴注入。

優點:

降低類間的耦合性

提高系統的穩定性

降低並行開發引起的風險

提高程式碼的可讀性和可維護性

最後,要玩溜

依賴倒置原則

,必須要熟悉控制反轉和依賴注入,如果你是java後端,這兩個詞語你一定不陌生,Spring框架核心設計就是依賴這兩個原則。

簡單原則

複雜系統的終極架構思路就是化繁為簡,此簡單非彼簡單,簡單意味著靈活性的無限擴充套件,接下來我們來了解下這個簡單原則。

簡單原則(KISS:Keep It Simple and Stupid)。翻譯過來,

保持簡單,保持愚蠢。

我們深入剖析下這個 “簡單”:

1、簡單不等於簡單設計或簡單程式設計。軟體開發中,為了趕時間進度,很多技術方案簡化甚至沒有技術方案,認為後面再找時間重構,編碼時,風格隨意,追求本次專案快速落地,導致欠下一大堆技術債。長此以往,專案維護成本越來越高。

保持簡單並不是只能做簡單設計或簡單程式設計,而是做設計或程式設計時要努力以最終產出簡單為目標,過程可能非常複雜也沒關係。

2、簡單不等於數量少。這兩者沒有必然聯絡,程式碼行少或者引入不熟悉的開源框架,看似簡單,但可能引入更復雜的問題。

如何寫出“簡單”的程式碼?

不要長期進行打補丁式的編碼

不要炫耀程式設計技巧

不要簡單程式設計

不要過早最佳化

要定期做 Code Review

要選擇合適的編碼規範

要適時重構

要有目標地逐漸最佳化

最少原則

最少原則也稱迪米特法則(LoD:Law of Demeter)。迪米特法則定義只與你的直接朋友交談,不跟“陌生人”說話。

如果兩個軟體實體無須直接通訊,那麼就不應當發生直接的相互呼叫,可以透過第三方轉發該呼叫。其目的是降低類之間的耦合度,提高模組的相對獨立性。

核心思路:

一個類只應該與它直接相關的類通訊

每一個類應該知道自己需要的最少知識

示例:

現在的軟體採用分層架構,比如常見的Web ——> Service ——> Dao 三層結構。如果中間的Service層沒有什麼業務邏輯,但是按照迪米特法則保持層之間的密切聯絡,也要定義一個類,純粹用於Web層和Dao層之間的呼叫轉發。

這樣傳遞效率勢必低下,而且存在大量程式碼冗餘。面對此問題,我們需靈活應對,早期可以允許Web層直接呼叫Dao。後面隨著業務複雜度的提高,我們可以慢慢將Controller中的重業務邏輯收攏沉澱到Service層中。隨著架構的衍化,清晰的分層開始慢慢沉澱下來。

寫在最後,迪米特法則關心區域性簡化,這樣很容易忽視整體的簡化。

表達原則

程式碼的可維護性也是考驗工程師能力的一個重要標準。試問一個人寫的程式碼,每次code review時都是一堆問題,你會覺得他靠譜嗎?

這時候我們就需要引入一個表達原則。

表達原則(Program Intently and Expressively,簡稱 PIE),起源於敏捷程式設計,是指程式設計時應該有清晰的程式設計意圖,並透過程式碼明確地表達出來。

表達原則的核心思想:程式碼即文件,透過程式碼清晰地表達我們的真實意圖。

那麼如何提高程式碼的可讀性?

1、最佳化程式碼表現形式

無論是變數名、類名還是方法名,要命名合理,要能清晰準確的表達含義。再配合一定的中文註釋,基本不用看設計文件就能快速的熟悉專案程式碼,理解原作者的意圖。

2、改進控制流和邏輯

控制巢狀程式碼的深度,比如if else的深度最好不要超多三層。外層最好提前做否定式判斷,提前終止操作或返回。這樣的程式碼邏輯清晰。下面示例便是正確的處理:

public List getStudents(int uid) {

List result = new ArrayList<>();

User user = getUserByUid(uid);

if (null == user) {

System。out。println(“獲取員工資訊失敗”);

return result;

}

Manager manager = user。getManager();

if (null == manager) {

System。out。println(“獲取領導資訊失敗”);

return result;

}

List users = manager。getUsers();

if (null == users || users。size() == 0) {

System。out。println(“獲取員工列表失敗”);

return result;

}

for (User user1 : users) {

if (user1。getAge() > 35 && “MALE”。equals(user1。getSex())) {

result。add(user1);

}

}

return result;

}

分離原則

天下大事,分久必合合久必分。面對複雜的問題,考慮人腦的處理能力有限,有效的解決方案,就是大事化小,小事化了,將複雜問題拆分為若干個小問題,透過解決小問題進而解決大問題。

分離的核心思路:

1、架構視角

結合業務場景對整個系統內若干元件進行邊界劃分,如,層與層(MVC)、模組與模組、服務與服務等。像現在流行的DDD領域驅動設計指導的微服務就是一種很好的拆解方式,透過水平分離的策略達到服務與服務之間的分離。

學會這10個設計原則,離架構師又進了一步!

架構設計視角下的關注點分離更重視元件之間的分離,並透過一定的通訊策略來保證架構內各個元件間的相互引用。

2、編碼視角

編碼視角主要側重於某個具體類或方法間的邊界劃分。比如Stream流的filter、map、limit,資料集在不同階段按照不同的邏輯處理,並將輸出內容作為下一個方法的輸入,當所有的流程處理完後,最後彙總結果。

一些不錯分層案例:

1、MVC模型

2、網路 OSI 七層模型

一個好的架構一定具有不錯的分層,各層之間透過定義好的規範通訊 ,一旦系統中的某一部分發生了改變,並不會影響其他部分(前提,系統容錯做的足夠好)。

契約原則

天下事無規矩不成方圓,軟體架構也是一樣道理。動輒千日的大專案,如何分工協作,保證大家的工作能有條不紊的向前推進,靠的就是契約原則。

契約式原則(DbC:Design by Contract)。軟體設計時應該為軟體元件定義一種精確和可驗證的介面規範,這種規範要包括使用的預置條件、後置條件和不變條件,用來擴充套件普通抽象資料型別的定義。

契約原則關注重點:

API 必須要保證輸入是接收者期望的輸入引數

API 必須要保證輸出結果的正確性

API 必須要保持處理過程中的一致性。如果一個API被二次修改後,整個叢集的伺服器都要重新部署,保證服務能力狀態的一致。

如何做好 API 介面設計?

1、介面職責分離。設計 API 的時候,應該儘量讓每一個 API 只做一個職責的事情,保證API的簡單和穩定性。避免相互干擾。

2、 API 命名。透過命名基本能猜出介面的功能,另外儘量使用小寫英文

3、介面具有冪等性。當一個操作執行多次所產生的影響與一次執行的影響相同

4、安全策略。如果API是外部使用,要考慮駭客攻擊、介面濫用,比如採用限流策略。

5、版本管理。API釋出後不可能一成不變,很可能因為升級導致新、舊版本的相容性問題,解決辦法就是對API 進行版本控制和管理。

寫在最後

軟體架構原則的核心精髓,儘可能把變的部分和不變的部分分開,讓不變的部分穩定下來。我們知道,模型是相對穩定的,實現細節則是容易變動的部分。所以,構建出一個穩定的模型層,對任何一個系統而言,都是至關重要的。