动态

详情 返回 返回

且談軟件架構(二) 模塊化與MVC - 动态 详情

前言

我一貫不喜歡手冊式的文章,就告訴你一些定律、經驗,我更願意完整的告訴我的經驗,我的理論是如何得出的,讀我的文章,就好像在和我進行交談,本篇可以認為是經驗之談,所謂經驗不是定理,就是這些經驗部分具備普適性,部分不具備普適性,具體情況要具體分析。本身本篇的標題是從MVC走向DDD,主要還是在掘金看到了轉轉技術團隊的《轉轉價格系統DDD實踐》這篇文章,其中提到:

在使用傳統的mvc模式下,我們往往使用三層架構,即controller,service,dao或者其類似的方式。這種架構會把所有的業務邏輯堆積在service之下,領域實體只做數據傳輸,沒有行為。隨着項目的迭代,可能出現service臃腫的情況,大量業務邏輯,把service搞成一個胖子,業務邏輯就會變得混亂不堪,理解和維護成本極大。

但是從MVC走向DDD,這個標題給人一種感覺是DDD一定優於MVC,事實上對於那種不會進行過多迭代的小型系統而言,沒必要進行DDD,在《且談軟件架構》中我們將軟件發展類比到城市發展,像一座城市,經濟快速發展會吸引人,越來越多的人涌入城市,城市變得擁擠,城市開始着手擴建,那麼對於軟件也是,龐大的用户量涌入,軟件開始追求越來越多強的性能,越來越強的可靠性,所以對於告訴發展的城市,會有乒乓乓乓的聲音,那是城市的心臟跳動的聲音,建築的聲音越多,這座城市就越是在發展,同樣的如果沒有那麼多居住人口,就沒有必要對城區進行擴建,擴建了也用不上這也就是浪費,。不同的城市有不同的發展歷程,也就有着不同的產業結構,所謂的結構我們也在《且談軟件架構》中探討過了,結構即架構,當我在搜索引擎搜索Architecture,第一個出來的就是建築相關的詞條:

所以這個架構就是跟建築學借的詞,是軟件行業對於建築行業的學習和借鑑,用一個物理、實在的“建築”來比喻一個抽象、虛擬的軟件系統。在維基百科中Architecture的意思是:

Architecture is the art and technique of designing and building, as distinguished from the skills associated with construction

建築是設計與構建的技術與藝術,有別於建築的相關的技能。

Architecture 也被譯為架構,所以架構就是設計與構建的技術與藝術,這裏有兩個我們熟悉的單詞,design 和 build,設計與構建,產品進行需求設計,開發根據需求設計進行代碼設計,而自動化構建工具則將我們代碼發佈,變成用户可使用的服務,這裏面有一個又一個的構建,也有一個又一個的設計。在設計上就可以衍生出藝術,從最初的有一個庇護之所,到追求舒適賞心悦目,我在搜索引擎搜索藝術這個關鍵字,順着鏈接點進去看到了一幅畫:

我在看到這幅畫的時候,我的心感到很平靜,連帶遭遇的被強迫的無意義加班帶來的焦灼情緒也舒緩了很多,我的鼠標在滑動的時候,劃到的葉子也在跟着動,這幅畫的主題是In Rhythm With Nature, 也就是與自然和諧相處,這幅畫的靈感來自於Carl Linnaeus的花鐘,Linnaeus是著名的植物學家和分類學家,他建立了一套現代生物識別、命名和分類系統。他的獨特花園設計利用了不同植物的自然晝夜節律,通過觀察它們花朵的開放和閉合來標記一天中的時間。與我們的植物朋友一樣,人類也會通過晝夜節律對環境變化做出反應。這個位於大腦中的 24 小時內部時鐘幫助調節我們的睡眠-覺醒週期。從上午到下午,我們會進入一個越來越警覺的狀態,然後在傍晚降低警覺,為夜間睡眠做準備。與自然共舞》將視聽與您的當地時間同步,從而尊重我們的晝夜節律。

現代人的睡眠大多沒有那麼節律,一般都是在手機裏面刷無可刷,才會放下手機準備睡覺,想起了一句詩: 久在樊籠裏,復得返自然。所以藝術是什麼?

指憑藉技巧、意願、想象力、經驗等綜合人為因素的融合與平衡,以創作隱含美學的器物、環境、影像、動作或聲音的表達模式,也指和他人分享美的感覺或有深意的情感與意識的人類用以表達既有感知且將個人或羣體體驗沉澱與展現的過程。 來自維基百科

在技術之上派生了藝術,就像建築,有些材料放在哪裏,可以被人建造出舒適美觀的建築,可以被建造成醜陋而又讓人不適的建築,那對於一個軟件呢,如果有對應的美學指導,我想應該也是有藝術的,對於用户直接接觸的部分有着對應的設計美學指導,界面的顯示無疑是藝術的,這方面也有商業的權衡,比如迅雷的安卓版就塞滿了廣告,很難用,這已經不是藝術不藝術的問題了,是很難用的問題。 那麼我們將目光依然拉回到構建界面的代碼呢,代碼構建的頁面本身是一種服務,所以速度要快,在代碼層面上我更認為是實用美學,複用性、穩定性、可靠性、可維護性、可擴展性構成了代碼美學的六個點,如果我們要討論代碼的藝術的話,我認為都是基於這六個點來討論,那麼為了達到這六個點,我們採用的手段一般也就是分層,分層就是分類,根據不同的功能將代碼分散到各個組件功能中,分類降低了複雜度,降低了大腦的符合,這一方面分層或分類做的比較成功的是網絡模型的分層,每層負責不同的功能。那軟件代碼中是如何分層的呢?

MVC

首先我是一名Java後端程序員,Java是我的主語言,我所熟悉的也就是服務端開發,這裏談的也就是我的經驗,當我剛學Java EE的時候,前端代碼和後端代碼放在一起,我們用包對代碼進行管理,不同的功能放在不同的包裏,不同的功能放在不同的包裏面, 那什麼是包? 我想起剛學Java的時候,那是在大幾的時候,我那個時候還只會C和C++,我在一個文件夾下寫代碼,到後面學Java也是,我沒有建包,我在Eclipse默認的文件夾下面寫代碼,後面看別人建了包,在包下面建代碼,我也有樣學樣,我開始建包,在包下面建class,但是剛開始我寫的類都沒有一個主題或者相似的地方,所以我只有一個包,到後面我開始學Servlet、JSP、JDBC,代碼量越來越多了,我開始分包,包像一個房間一樣容納不同的功能:

上面我大致上分了三個包,包的最後一個單詞就是這個包的功能,controller代表控制器,dao和service代表model,jsp所在的文件夾屬於view,這也就是MVC:

圖片來自於mdn web docs,視圖層也就是用户直接接觸的界面,定義數據該如何顯示,控制器接收用户請求,將請求轉發給模型層。 上面舉了一個三層交互的例子,用户點擊加入購物車, 視圖層發送請求到控制器這一層,控制器這一層從視圖層接收到請求,通知對應的模型去添加,模型中定義了數據結構,添加數據,然後更新視圖層對應的顯示。 在那個剛學Java EE的時候,我就通過包來進行功能分層,這麼做的目的是避免一個包裏面放太多代碼,就像居住的房子,我們根據不同的需要將不同的房間承擔不同的職能,如果一個房間承擔了太多職能,比如雜物間承擔了卧室、廚房、雜物間的功能,那麼進入這個房間的人就會覺得這個房間亂糟糟的,更關鍵在於不可預測的反應,可能不小心就會碰到鍋碗瓢盆。

所有堆積如山的東西,都是不可預測的。

簡化系統的首選方法,就是將一個大系統,轉變為多個更小的子系統組成的系統。 《系統、數學和爆炸》

這也是我們分層的目的,降低複雜度,將一個大系統,轉變為多個更小的子系統組成的系統。 轉變為更小的系統還有一個妙用就是方便複用,就像是一個方法主體提供了A功能,但是在實現A功能的時候調用了B功能,在方法中也就體現為十幾行代碼,但是隻想要用到方法中的B功能就沒辦法剝離出來,他們捆在一起。隨着功能的豐富,一個項目下會有越來越多的功能,越來越多的包,這些包也可以進行劃分,對外提供功能,在後端領域我們一般使用maven來構建項目,一般我們會有一個項目總的文件夾,然後下面的文件夾層級是:

src/main/包
src/resources 

這些包對外提供一個主體功能,但是隨着功能的迭代,功能在逐漸變多,但是如果外部想要使用的話,還是以jar的形式,但是如果我只想使用部分功能呢,我就得不得不接受所有,我們的結構像下面這樣:

以fastJson這個json框架為例,我們對他的期待也就是對象轉json,json轉對象了,但是fast-json 的依賴有:

<dependency>
        <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
            <version>2.0.1</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-transports-http</artifactId>
            <version>3.1.2</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
            <version>3.1.2</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.3.7.RELEASE</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.3.7.RELEASE</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
            <version>4.3.7.RELEASE</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>1.8.6.RELEASE</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>retrofit</artifactId>
            <version>2.5.0</version>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.6.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-spring-web</artifactId>
            <version>2.6.1</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>io.javaslang</groupId>
            <artifactId>javaslang</artifactId>
            <version>2.0.6</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-common</artifactId>
            <version>2.23.2</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>net.sf.trove4j</groupId>
            <artifactId>core</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.javamoney.moneta</groupId>
            <artifactId>moneta-core</artifactId>
            <version>1.3</version>
            <scope>provided</scope>
        </dependency>
   <dependency>
            <groupId>io.airlift</groupId>
            <artifactId>slice</artifactId>
            <version>0.36</version>
            <scope>provided</scope>
 </dependency>

注意javax.ws.rs-api這個依賴,這個依賴定義了一組接口,來自於JSR-311,旨在定義一個統一的規範使得Java程序員可以使用一套固定的接口來開發REST應用,具體的實現由第三方框架完成,javax.ws.rs通過spi機制來加載對應的實現,那這部分功能否單獨做個依賴出來呢? 也就是將fastjson根據能力再進行拆分, 哪些是fastjson最基本的能力,哪些是其他擴展,我看了下fastjson2就是用這種思路將其進行拆分:

裏面有core、擴展。我想起高中生物生命系統的結構層次:

對於代碼的發展也是這樣,最初是類,然後將類聯合起來是包,然後包形成模塊有特定的職能。模塊化着眼於分離職責,增強複用性。JDK 在9也進行了模塊化,JDK也是類似的思路進行拆分,JDK 模塊化的目標是:

  • 可靠的配置 — 模塊提供了在編譯期和執行期加以識別模塊之間依賴項的機制,系統可以通過這些依賴項確保所有模塊的子集合能滿足程式的需求。
  • 強封裝 — 模塊中的包只有在模塊顯式導出時可供其他模塊訪問,即使如此,其他模塊也無法使用這些包,除非它明確指出它需要其他模塊的功能。由於潛在攻擊者只可以訪問較少的類別,因此提高了平台的安全性。您會發現,模塊化可幫助您做出更簡潔、更合乎邏輯的設計。
  • 可擴展的 Java 平台 — 在這之前,Java 平台是由大量的包組成的龐然大物,使其難以開發、維護和發展,還不能輕易地被子集化。現在,平台被模塊化為 95 個模塊(此數字可能隨着 Java 的發展而變化),您可以創建定製運行時,其中僅包含應用或您定位的設備所需的模塊。例如,如果設備不支持 GUI,您可以創建一個不包含 GUI 模塊的運行時,顯著降低運行時的大小。
  • 更佳的平台完整性 — 在 Java 9 之前,您可以使用平台中許多不預期供應用使用的類別,通過強封裝,這些內部 API 真正會被封裝並隱藏在使用該平台的應用中。如果您的代碼依賴這些內部 API,則可能會對轉移舊有代碼到以模塊化的 Java 9 造成問題。
  • 提高性能 — JVM 使用各種優化技術來提高應用性能。JSR 376 表明,當預先知道某技術僅在特定模塊中被使用,這些技術會更有效。

Java平台是由大量的包組成的龐然大物,難以開發、維護和發展,不能定製運行時,僅包含我所需的模塊。fastjson 1也是這個毛病,好在fastjson2就開始拆分,根據不同的職能,將不同的包分拆到不同的模塊中。Java平台也是選中了一個最小的運行時,也就是最基礎的base模塊,jdk的其他模塊都需要這個模塊提供的能力,jdk各個模塊之間的圖如下所示:

微服務與模塊化

回憶一下我們在《寫給小白看的Spring Cloud入門教程》講的微服務,我們選擇微服務架構的原因是代碼量在不斷的變大,我們期待更強的可伸縮性,可擴展性,所以我們選擇了微服務,根據業務將我們的系統拆成若干個大小適當的微服務,然後我們就開始藉助於Spring Cloud 和 Spring Boot 來搭建微服務,我們用PPT化的架構圖是很漂亮的:

但是這幅圖也有缺陷,缺陷就在於沒畫出各個服務之間的關係,比如A服務需要和B服務通信,那麼接口在B服務提供,如果入參和出參是一個類的話,雙方就需要共享這一部分,那麼由於A服務和B服務不在一個模塊,那麼A服務該如何引用到B服務的數據類型呢,在Java中主要是類,在B服務中複製一份,複製到A裏面,那如果將來B原來提供的接口不滿足於A的要求,需要添加字段,那B和A都需要跟着改動,那如果B提供的接口被七個微服務所使用,那麼這七個微服務都需要改? 這太恐怖了吧,這不還是耦合在一起了嘛,説好的解耦合呢,更聰明的方式是,每個服務內部專門建一個模塊,專門給外部調用。那這樣每個服務又是一個不大不小的單體服務:

api模塊被其他服務所引用,而api模塊又需要引用基礎模塊的能力,或者你也可以根據自己的需要,定義粒度更細的依賴,讓給外部調用的依賴做到輕一些。將微服務拆分到適當的大小,這是一句很美妙的話,人們常説,不要太大,也不要太小,但這是空話,我該根據什麼原則來將微服務拆到合適的大小呢。這個問題的一個答案叫領域模型。我們完全沒有可能在一篇文章裏完全將使用領域模型來拆分微服務講清楚,這裏我們先引出這個問題,後文我們會進行細緻的討論。

總結一下

隨着代碼量的不斷增大,一個項目逐步在具備一個又一個功能,也在不斷的變得複雜,一個項目具備越來越多的功能也不利於複用:

所有堆積如山的東西,都是不可預測的。簡化系統的首選方法,就是將一個大系統,轉變為多個更小的子系統組成的系統。 《系統、數學和爆炸》

所以我們有了許多方法來拆分,微觀上不屬於這個類的職責被剝離出去,宏觀上,根據一定的功能將包分拆出去,比如fastjson,剔除自己的擴展,有一個最核心的依賴,這樣方便複用。我曾經將這種拆分方法應用到拆分微服務中,但是與朋友交流之後發現微服務更傾向於獨立、自治,各個服務之間應當是平等的,不應當存在核心包,基礎包,每個微服務傾向於自治,儘可能的少依賴第三方,這也是領域建模要回答的問題,如何恰到好處的拆分微服務。

參考資料

[1] 架構、構架、結構、框架之間有什麼區別? https://www.zhihu.com/question/32105413

[2] 互聯網協議入門(一) https://www.ruanyifeng.com/blog/2012/05/internet_protocol_sui...

[3] Entity層、DAO層、Service層、Controller層 https://www.jianshu.com/p/133f80c5af9b

[4] Modules, not microservices https://news.ycombinator.com/item?id=34230641

[5] 拜託,別在 agent 中依賴 fastjson 了 https://mp.weixin.qq.com/s/ZYSiPGBQZLljZE0ESMM2tg

[6] 譯 軟件中的安全漏洞是什麼? 獻給外行人的軟件漏洞指南 https://mp.weixin.qq.com/s/gLRBVLGguW196pzBLXBDSw

[7] 據報道稱“瀏覽器內核有上千萬行代碼”,瀏覽器內核真的很複雜嗎? https://www.zhihu.com/question/290767285/answer/1200063036

[8] 《系統、數學和爆炸》https://pjonori.blog/posts/systems-math-explosions/

Add a new 评论

Some HTML is okay.