軟件開發(fā)架構(gòu)模式淺談:一些思考和實(shí)踐記錄
一、背景和問(wèn)題
我個(gè)人平時(shí)會(huì)比較慎用“架構(gòu)”這個(gè)詞
-
一方面是覺(jué)得業(yè)界有很多架構(gòu)大師和架構(gòu)模式,而我的認(rèn)知和實(shí)踐有限;
-
另一方面是因?yàn)檫@個(gè)詞看著挺高大上、有點(diǎn)務(wù)虛,如果不結(jié)合實(shí)際場(chǎng)景的具體問(wèn)題來(lái)討論,容易陷入“PHP是最好的語(yǔ)言”這樣的辯論賽中。而不同場(chǎng)景中又有各自的問(wèn)題,程序員們通過(guò)自己的理解和思考、針對(duì)實(shí)際場(chǎng)景對(duì)一些架構(gòu)模式進(jìn)行了擴(kuò)展實(shí)踐,以此來(lái)解決遇到的問(wèn)題,也會(huì)基于同一個(gè)模式延伸出一些派生概念。
兵無(wú)常勢(shì),水無(wú)常形。 所以,我個(gè)人的觀點(diǎn)是:以要解決的問(wèn)題為出發(fā)點(diǎn),去討論我們要采用的架構(gòu)模式(技術(shù)方案)。
另外,由于我們是站在很多巨人肩膀上的,討論時(shí)可以站在一些如SOLID等軟件設(shè)計(jì)/開發(fā)原則的基礎(chǔ)上。
寫這篇文章,我也是從解決一些問(wèn)題的目的出發(fā)的:
-
最近和團(tuán)隊(duì)同學(xué)討論了相關(guān)話題,雖然大多數(shù)同學(xué)在實(shí)踐上基本一致,但具體到話術(shù)、名詞概念和具體使用的理解和實(shí)踐上有些差異(這是很正常的,因?yàn)闃I(yè)界對(duì)同一個(gè)模式的理解和實(shí)踐也不同)。我結(jié)合一些實(shí)際編碼場(chǎng)景做了一番陳述,為了避免后續(xù)重復(fù)大費(fèi)口舌,所以打算寫下來(lái),以后有需要直接發(fā)文章鏈接。
-
由于我個(gè)人的認(rèn)知和實(shí)踐有限,所以也希望能拋(huan)磚(ying)引(lai)玉(pen),讓我學(xué)到更多。
-
雖然同一個(gè)架構(gòu)模式在不同業(yè)務(wù)/技術(shù)領(lǐng)域的實(shí)施會(huì)有區(qū)別,但同一個(gè)團(tuán)隊(duì)內(nèi)應(yīng)該保持一致性,因?yàn)檫@樣有助于日常的code review、功能模塊的交接backup等活動(dòng),尤其是有利于使用統(tǒng)一的單測(cè)建設(shè)方案來(lái)保障我們的產(chǎn)品質(zhì)量。
實(shí)際問(wèn)題:我最近在開發(fā)商家合并發(fā)貨的功能,但由于之前基礎(chǔ)發(fā)貨功能的界面和邏輯并不是我開發(fā)的,所以我在修改原有代碼、支持有非常多細(xì)節(jié)邏輯的合并發(fā)貨能力時(shí),就在擔(dān)心對(duì)原有發(fā)(zhong)貨(yao)能力的影響。而這時(shí)候,如果有單測(cè)的保障,我就可以更放心地進(jìn)行功能升級(jí)改造了 —— 別說(shuō)更復(fù)雜的合并發(fā)貨能力了,而這類訴求在復(fù)雜的交易場(chǎng)景里很普遍。
提煉一下我遇到的具體問(wèn)題:
在由不同開發(fā)人員持續(xù)迭代、進(jìn)行功能升級(jí)的軟件開發(fā)活動(dòng)中,如何保障具有復(fù)雜邏輯的商家經(jīng)營(yíng)工具的產(chǎn)品質(zhì)量。
軟件開發(fā)活動(dòng)是整個(gè)流程的核心環(huán)節(jié):接收產(chǎn)品和視覺(jué)設(shè)計(jì)需求/變更作為輸入,然后輸出客戶可用的終端產(chǎn)品。
而統(tǒng)一的軟件開發(fā)架構(gòu)模式,則是我們保障軟件開發(fā)質(zhì)量的基礎(chǔ)。(這里就不具體展開WHY了)
由于討論的是具體面向客戶使用的業(yè)務(wù)場(chǎng)景,少不了客戶操作交互的視圖層(View),所以我從MVC開始談起。
二、從表現(xiàn)層的MVC談起
雖然我平時(shí)比較慎用“架構(gòu)”這個(gè)詞,但我平時(shí)喜歡隨手拍一些建筑物。 因?yàn)榻ㄖ?,?huì)讓我聯(lián)想到軟件的架構(gòu)也應(yīng)該有美感,畢竟Software Architecture這個(gè)概念也是起源于Architecture。
這時(shí)候,架構(gòu)這個(gè)詞就會(huì)給我一種接地氣的感覺(jué):有多少塊磚,每塊磚做什么用、放到哪里去,這塊磚 和 那塊磚怎么黏在一起或互相支撐。 當(dāng)然,由于軟件的可移植性、可復(fù)用性,從某些角度來(lái)講,軟件架構(gòu)相比建筑架構(gòu)有其更復(fù)雜的地方。
MVC誕生至今已經(jīng)超過(guò)40年了(Since 1979),10多年前就得到過(guò)很廣泛的討論和實(shí)踐,穿越時(shí)空到今天肯定有其反脆弱性和內(nèi)在核心價(jià)值。 雖然如今乍看起來(lái)好像已經(jīng)過(guò)氣、被討論過(guò)千百遍了,但仍然有很多程序員會(huì)有不同理解和看法,或多或少。這是很正常的,上面也提到了部分原因,這里具體再展開下。
1 MVC在經(jīng)典三層架構(gòu)里的位置
MVC是一種通用架構(gòu)模式
-
早期PC時(shí)代應(yīng)用在桌面客戶端,
-
后來(lái)在Web時(shí)代變得流行(我以前寫PHP也用過(guò)相關(guān)MVC框架),
-
如今在移動(dòng)互聯(lián)網(wǎng)時(shí)代也得到廣泛應(yīng)用。
上面這三個(gè)場(chǎng)景的應(yīng)用,都是面向客戶的,需要交互表現(xiàn)的。
從MVC命名中的View(視圖)也可以看出,MVC模式應(yīng)用在軟件系統(tǒng)架構(gòu)里的表現(xiàn)層。
在業(yè)界某知名公司的官方文檔里,也明確把MVC放在Web Presentation Patterns下。
我之所以沒(méi)有在上圖中對(duì)M-V-C添加箭頭線條,是因?yàn)樵谶@一點(diǎn)上,不同程序員也有不同理解和實(shí)踐。
這是第一個(gè)需要明確的點(diǎn):MVC架構(gòu)模式在多層系統(tǒng)架構(gòu)里的應(yīng)用范圍。
左側(cè) 業(yè)務(wù)表現(xiàn)層-業(yè)務(wù)服務(wù)層-基礎(chǔ)服務(wù)層 是移動(dòng)端三層架構(gòu)模式,未涉及到 C/S 交互;右側(cè)是Web B/S場(chǎng)景的三層架構(gòu)模式。
因?yàn)橛行?yīng)用會(huì)比較簡(jiǎn)單,根本不需要業(yè)務(wù)服務(wù)或基礎(chǔ)服務(wù)層,純粹靠一個(gè)MVC(或者VC)就能交付出一個(gè)Mobile/Web App;
而且在一些業(yè)務(wù)系統(tǒng)里,Web前端/桌面客戶端/移動(dòng)App 也可能會(huì)被簡(jiǎn)化為 大前端/大終端表現(xiàn)層;
所以可能基于不同信息,不同程序員對(duì)此會(huì)有不同認(rèn)知。
但隨著用戶終端應(yīng)用的重要性和復(fù)雜度的提升,已經(jīng)從簡(jiǎn)單應(yīng)用發(fā)展到復(fù)雜多團(tuán)隊(duì)協(xié)同的平臺(tái)型或航母級(jí)應(yīng)用,僅靠一個(gè)MVC來(lái)完成交付是不合適的。
我們也可以反過(guò)來(lái)想,程序員會(huì)把以下代碼放在客戶端代碼的哪一層:
-
對(duì)Web引擎的擴(kuò)展邏輯。
-
通信協(xié)議的結(jié)構(gòu)定義,以及相應(yīng)的socket連接和通信代碼。
-
一個(gè)業(yè)務(wù)相關(guān)且UI無(wú)關(guān)的平臺(tái)開放能力。
-
Crash捕獲、卡頓監(jiān)控、日志埋點(diǎn)等功能實(shí)現(xiàn),比如Android在做APM相關(guān)事情時(shí)會(huì)采用AOP方式,利用ASM、AspectJ等方案來(lái)做字節(jié)碼插樁。
-
……
2 業(yè)界基于MVC模式的不同實(shí)踐
前面提到不同程序員對(duì)MVC模式的理解和實(shí)踐存在差異
業(yè)界大廠亦然,以下會(huì)結(jié)合業(yè)界一些知名且有影響力的公司在MVC模式上的實(shí)踐,做進(jìn)一步的展開討論。
知名公司A
知名公司A在指導(dǎo)開發(fā)者使用MVC時(shí),推薦下圖方式:
可以看出在他們的實(shí)踐上:
-
Controller可以引用View和Model。
-
View可以引用Model。
-
這里的Model傾向于是Passive。
同時(shí),他們建議:
-
在強(qiáng)類型視圖場(chǎng)景,控制器從模型創(chuàng)建并填充ViewModel實(shí)例,該ViewModel 實(shí)例包含要在該視圖上顯示的數(shù)據(jù)。
-
當(dāng)控制器由于責(zé)任過(guò)多而變得過(guò)于復(fù)雜時(shí),也就是業(yè)界戲稱的“MVC means Massive View Controller”,需要將業(yè)務(wù)邏輯從控制器移出并推入域模型中。
知名公司B
說(shuō)到Massive View Controller,知名公司B在移動(dòng)互聯(lián)網(wǎng)方興未艾的時(shí)候,推薦下圖所示的MVC實(shí)踐方案:
上圖呈現(xiàn)出:
-
Controller引用View和Model。
-
Model通過(guò)一些松耦合方式來(lái)觸達(dá)Controller,如廣播通知、callback等,驅(qū)動(dòng)Controller做出響應(yīng)。
-
View通過(guò)代理模式等方案弱依賴Controller,由Controller對(duì)各種用戶操作、UI渲染訴求做出響應(yīng)。
-
而View和Model之間是隔離的,Model變化后對(duì)View的更新操作全部由Controller負(fù)責(zé)。
不過(guò)相應(yīng)的官方文檔已經(jīng)被聲明是過(guò)期文檔了,并備注不一定是目前的最佳實(shí)踐。
是的,隨著移動(dòng)互聯(lián)網(wǎng)蓬勃發(fā)展,十年前的“最佳實(shí)踐”被一路多種挑戰(zhàn) —— 在采用這種方案的開發(fā)領(lǐng)域中,如何重構(gòu)Massive View Controller為L(zhǎng)ighter View Controller已經(jīng)成為了一個(gè)專題。
對(duì)比和思考
A和B的異同點(diǎn)
-
相同點(diǎn):Model包含 所需的數(shù)據(jù)結(jié)構(gòu)封裝,以及相應(yīng)數(shù)據(jù)操作的方法定義。即Data + 本地或遠(yuǎn)端的CURD。
-
差異點(diǎn):在知名公司A給的圖中,View可以引用Model,而在知名公司B給的圖中則不行。
一些問(wèn)題和思考
-
View有箭頭指向Model,這里的引用關(guān)系是指什么?是View持有Model.Data數(shù)據(jù)對(duì)象,還是View調(diào)用Model.CURD方法。
-
Controller的本意是Controing Logic,那除了ViewController外,是否還可以有其它的XxController,比如DataSourceController、NotificationController?
-
從命名上看,既然ViewController 既有View 又有Controller,那為什么把它放在 C里面,而不放在V里面呢?比如當(dāng)我們?cè)趇OS/Android開發(fā)中引入MVVM模式后,ViewController或Activity屬于M-VM-V的哪部分呢,代碼放在哪個(gè)目錄下呢?
-
我有類名使用ViewModel后綴就代表我使用MVVM模式了嗎?
Martin Fowler
作為
-
《重構(gòu) : 改善既有代碼的設(shè)計(jì)》、《企業(yè)應(yīng)用架構(gòu)模式》等著作的作者;
-
敏捷軟件開發(fā)宣言創(chuàng)作者之一;
-
MVVM模式誕生時(shí)參考引用的技術(shù)專家。
Martin Fowler給的MVC模式圖如下:
和上面知名公司A和B的圖,又不一樣了, 不過(guò)他這里也是認(rèn)為View可以引用Model的。
MVC和DDD
Martin Fowler和《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》作者Eric Evans也討論過(guò)MVC中Model的設(shè)計(jì)理念:
-
貧血模型:將Model分為簡(jiǎn)(pin)單(xue)的Model數(shù)據(jù)對(duì)象,和處理操作數(shù)據(jù)對(duì)象的Service/Manager/BizLogic等。
-
-
示例:為aPerson修改name,則由 CitizenService.changeNameOfPerson( aPerson ) 這種方式來(lái)實(shí)現(xiàn)。
-
-
充血模型:將對(duì)應(yīng)領(lǐng)域的處理邏輯放到領(lǐng)域模型中,使得這個(gè)領(lǐng)域模型更飽(chong)滿(xue)。
-
-
示例:aPerson要刷牙,則由 aPerson.brushTeeth() 來(lái)實(shí)現(xiàn)。
-
-
補(bǔ)充:充血模型更有面向?qū)ο缶幊痰奈兜?,尤其是搭配交易領(lǐng)域等業(yè)務(wù)場(chǎng)景,更有體感。不過(guò)稍微細(xì)想一下,可能就會(huì)發(fā)現(xiàn)DDD對(duì)設(shè)計(jì)的要求會(huì)更高,從而對(duì)研發(fā)周期和質(zhì)量保障提出了新的要求,并且可能引起對(duì)現(xiàn)有系統(tǒng)的大規(guī)模重構(gòu)。(盒馬的DDD實(shí)踐)
-
也就是說(shuō),大到MVC各個(gè)模塊的依賴引用關(guān)系,細(xì)到Model中的代碼設(shè)計(jì)方式,業(yè)界都有不同的理念和實(shí)踐。
Java Web開發(fā)領(lǐng)域也對(duì)Model的設(shè)計(jì)產(chǎn)生過(guò)非常激烈的討論。
小結(jié)
先拋開具體模塊的代碼設(shè)計(jì)方案,基于以上幾種業(yè)界大廠或?qū)<业拿枋觯倚〗Y(jié)了以下這張圖并標(biāo)注了待解問(wèn)題:
問(wèn)題一:如何解決MVC中Controller的膨脹臃腫問(wèn)題?
要回答如何解決,需要先思考為什么膨脹。
問(wèn)題二:View能否引用Model?
-
要回答能否引用,需要先定義引用關(guān)系是什么。
-
-
是持有對(duì)象,還是調(diào)用CURD接口操作對(duì)象。
-
又或者這兩者沒(méi)有必要區(qū)分,因?yàn)槌钟械膶?duì)象本身就可能帶CURD接口。
-
-
參考上面相關(guān)資料,目前業(yè)界有的支持、有的反對(duì)。
問(wèn)題三:存在View -> Model,那么是否可以反過(guò)來(lái)存在 Model -> View?
和問(wèn)題二在描述上相反但又有關(guān)聯(lián),如果對(duì)問(wèn)題再進(jìn)一步提問(wèn)的話:
-
使用 -> 引用關(guān)系,是為了解決什么問(wèn)題?
-
使用 -> 引用關(guān)系,會(huì)產(chǎn)生什么問(wèn)題?
如同文章開頭所說(shuō),以上問(wèn)題需要結(jié)合具體場(chǎng)景來(lái)展開(見 實(shí)際案例結(jié)合),盡量從務(wù)虛到務(wù)實(shí)。
3 Redux-like Architecture and Framework
隨著前后端分離得更徹底,終端設(shè)備性能和用戶體驗(yàn)重要性的提升,前端領(lǐng)域也得到了蓬勃發(fā)展,開發(fā)方式也有了比較大的變化,MVC-like方式不再是主流:
-
UI開發(fā)方面從早期的命令式到現(xiàn)在的聲明式。
-
整體應(yīng)用和業(yè)務(wù)邏輯實(shí)現(xiàn)方面,從早期的OOP寫法,轉(zhuǎn)向基于FP的響應(yīng)式編程。比如Redux的數(shù)據(jù)流、React的Hook特性等。
-
各種框架蓬勃發(fā)展,一些概念和模式的提出、實(shí)踐應(yīng)用方面,我個(gè)人認(rèn)為是領(lǐng)先并影響客戶端的。
-
-
其中Redux是一個(gè)經(jīng)典案例,并且我覺(jué)得Redux官方也挺開放包容的,比如Dan Abramov寫的《You Might Not Need Redux》。
-
和MVC延伸派生出的MVC Family一樣,Redux提出或重新帶火了數(shù)據(jù)流、狀態(tài)管理等概念,開始影響其它平臺(tái)領(lǐng)域,并誕生了一些框架。
比如ReSwift、swift-composable-architecture,以及SwiftUI里的State and Data Flow。
雖然我也寫過(guò)點(diǎn)React,但并沒(méi)有怎么實(shí)踐過(guò)Redux。
不過(guò)這些變化和影響,是我們?cè)诮鉀Q問(wèn)題的過(guò)程中需要結(jié)合考慮的。
三、實(shí)際案例結(jié)合
1 常見的數(shù)據(jù)結(jié)構(gòu)定義和使用
程序 = 數(shù)據(jù)結(jié)構(gòu) + 算法。
—— Nicklaus Wirth,Pascal之父,圖靈獎(jiǎng)獲得者
這句話乍看起來(lái)可能會(huì)有點(diǎn)面向過(guò)程設(shè)計(jì)的感覺(jué),但OOP中的對(duì)象其實(shí)也是由數(shù)據(jù)+方法組成,而FP則更不用說(shuō)了。
在編碼開發(fā)活動(dòng)中,會(huì)存在以上3種數(shù)據(jù)結(jié)構(gòu)定義和使用方式:
-
原生數(shù)據(jù)結(jié)構(gòu),比如list/array、map/dictionary、tuple等。
-
類似MyContact的數(shù)據(jù)結(jié)構(gòu)定義,由服務(wù)端返回的數(shù)據(jù)進(jìn)行轉(zhuǎn)化,并可能根據(jù)業(yè)務(wù)邏輯按需加上一些標(biāo)志位給Controller消費(fèi)。
-
類似ContactViewModel這樣的純粹為視圖View服務(wù)的數(shù)據(jù)結(jié)構(gòu)定義。
補(bǔ)充:
(1)MyContact 和 ContactViewModel 只是特意區(qū)分的命名,實(shí)際上 MyContact 也可以是純粹為視圖View服務(wù)的數(shù)據(jù)結(jié)構(gòu)定義。
(2)但是,合適的命名有助于幫助我們思考和編碼,從表達(dá)上呈現(xiàn)出我們的傾向和重點(diǎn)。
"There are only two hard things in Computer Science: cache invalidation and naming things."
—— Phil Karlton
2 常見的多復(fù)雜卡片的列表場(chǎng)景
這個(gè)場(chǎng)景可以部分回答問(wèn)題一:為什么Controller會(huì)膨脹,以及如何解決。
其它部分答案則落在復(fù)雜頁(yè)面場(chǎng)景的多delegate、target-action、notification-observer等視圖交互響應(yīng)的處理邏輯上。
我認(rèn)為,之前反對(duì)View引用Model,就是導(dǎo)致MVC變成Massive View-Controller的一個(gè)原因。
另一個(gè)原因我認(rèn)為是工具鏈只提供了ViewController這樣的Controller模板,隱式教導(dǎo)開發(fā)者都在這里寫代碼。
這也可能是因?yàn)槭畮啄昵耙苿?dòng)互聯(lián)網(wǎng)還沒(méi)發(fā)展起來(lái),移動(dòng)App的復(fù)雜度低,所以提供了在當(dāng)時(shí)簡(jiǎn)單夠用的方案。
當(dāng)只能由Controller 持有-> Model的時(shí)候,那么在多復(fù)雜卡片的列表場(chǎng)景中,必須由Controller來(lái)更新每個(gè)View的屬性/狀態(tài)。
-
MyViewController需要為ContactCell更新它的各種相關(guān)屬性,類似的還有AddressCell、PackageCell等。
-
MyViewController在更新AddressCell展示前,可能還需要先為它計(jì)算出合適的富文本展示內(nèi)容。
-
MyViewController需要響應(yīng)不同Cell的點(diǎn)擊交互行為,包含但不限于按鈕點(diǎn)擊、輸入框變化、富文本跳轉(zhuǎn)、鍵盤起落等。
-
MyViewController需要響應(yīng)CollectionView/TableView的DataSource/Delegate各種方法實(shí)現(xiàn)。
-
MyViewController需要響應(yīng)Model層的變更通知,或者是另外一個(gè)ViewController拋過(guò)來(lái)的廣播通知。
-
……
然后MyViewController就爆炸了。
針對(duì)這種場(chǎng)景,我的解法是:
-
通過(guò)讓View->Model,基于工廠模式,把組件化Cell基于數(shù)據(jù)更新的布局邏輯交給View負(fù)責(zé),如contactCell.configUIWithModel( contactModel )。這樣有點(diǎn)類似上面DDD提到的充血Model,具備高內(nèi)聚的特點(diǎn),帶來(lái)好處:
-
基于ViewController,拆分出不同職責(zé)的擴(kuò)展,比如MyViewController+Delegate專門復(fù)雜響應(yīng)代理事件處理。
-
定義出其它類型的Controller,比如MyDataSourceController,專門為TableView提供數(shù)據(jù)源,可以類比參考Android中ListView的Adapter+ViewHolder。
工廠模式下,產(chǎn)品的刷漆、烘干、印花等操作會(huì)在內(nèi)部完成,不會(huì)丟一個(gè)模型讓客戶去自己貼logo。MVC的每一部分都可以用不同的設(shè)計(jì)模式來(lái)組合實(shí)施,實(shí)現(xiàn)解耦或動(dòng)態(tài)靈活的目標(biāo)。
對(duì)應(yīng)下圖:
到這里已經(jīng)回答了前面的問(wèn)題一和問(wèn)題二。
更多解法可以參考上面提到的相關(guān)建議,比如lighter view controllers
這里采用了 VIew -> Model 的方案,用來(lái)參與解決Massive View-Controller的問(wèn)題,并且讓View更容易復(fù)用和做UI測(cè)試,帶來(lái)了好處。
可以結(jié)合前面提到的
“當(dāng)控制器由于責(zé)任過(guò)多而變得過(guò)于復(fù)雜時(shí),需要將業(yè)務(wù)邏輯從控制器移出并推入域模型中。”
再進(jìn)一步討論下。
我的理解和舉例:
-
存在一個(gè)輸入框,讓用戶提交物流單號(hào)。
-
用戶在輸入過(guò)程或者完成輸入后,由View通過(guò)delegate模式路由給Controller做校驗(yàn),而Controller可能還要進(jìn)一步依賴Model去做更完整的校驗(yàn)(如網(wǎng)絡(luò)請(qǐng)求到服務(wù)端,因?yàn)槲锪鲉翁?hào)的規(guī)則很多而且可能動(dòng)態(tài)更新)。
-
當(dāng)Controller責(zé)任過(guò)多、代碼膨脹、過(guò)于復(fù)雜時(shí),就將物流單號(hào)這塊業(yè)務(wù)邏輯推入 物流(單號(hào))域模型中,即由View直接通過(guò)delegate模式交給 LogisticsModel來(lái)做校驗(yàn)。
-
也是 View -> Model 。
不一定對(duì),拋(huan)磚(ying)引(lai)玉(pen)。
那么,存在VIew -> Model,有什么壞處嗎?
3 一個(gè)Kotlin跨平臺(tái)場(chǎng)景案例
這里不具體展開講Kotlin及其跨平臺(tái)相關(guān)內(nèi)容,只是描述從MVC模式做跨平臺(tái)遷移時(shí)遇到的問(wèn)題。
這里的ViewController/Activity放在哪里,也和上面的一個(gè)問(wèn)題相呼應(yīng)。
雖然D-KMP主張通過(guò)全新構(gòu)建工程寫代碼的方式來(lái)實(shí)踐,但從實(shí)際情況出發(fā),絕大多數(shù)現(xiàn)有系統(tǒng)都會(huì)是以單點(diǎn)嘗試、漸進(jìn)式的方式來(lái)落地,或發(fā)展、或回撤。
那么,如上圖所示:
-
如果在既有MVC代碼結(jié)構(gòu)中,View -> Model,在這種漸進(jìn)式遷移場(chǎng)景下,就需要修改View來(lái)適配新的ViewModel。
-
如果 View 不引用 Model,則是由 Controller來(lái)做膠水層設(shè)置更新視圖(命令式UI)。
-
后者的好處是,保持View的獨(dú)立性,只需要修改這個(gè)業(yè)務(wù)場(chǎng)景對(duì)應(yīng)的單個(gè)Controller即可。因?yàn)閂iew具有可復(fù)用性,可能在另外還沒(méi)準(zhǔn)備遷移/改變的模塊里也有使用。
所以,此處不建議 View -> Model。 那么,誰(shuí)對(duì)呢?
4 誰(shuí)對(duì)誰(shuí)錯(cuò)的務(wù)虛討論
當(dāng)有程序員要推薦使用其它架構(gòu)模式的時(shí)候,通常開頭的一句話就是先說(shuō)MVC模式的問(wèn)題。
比如ReSwift在寫 Why ReSwift? 時(shí),開頭第一句話就是:Model-View-Controller (MVC) is not a holistic application architecture.
我個(gè)人認(rèn)為
-
架構(gòu)是一個(gè)名詞(n.)+動(dòng)詞(v.)。
-
-
架構(gòu)(n. & v.)是為了幫助 開發(fā)者在交流時(shí)有一致的理解、在業(yè)務(wù)需要時(shí)能夠便于擴(kuò)展、在出問(wèn)題時(shí)能夠快速定位等等(對(duì)抗熵增)。具體還是看采用這個(gè)架構(gòu)的得失,要解決什么問(wèn)題或帶來(lái)什么好處,然后帶來(lái)什么成本或付出什么代價(jià)。
-
-
架構(gòu)(v.)通過(guò)分配每部分代碼的職責(zé)并為他們?nèi)∶ê帽萯OS/Android開發(fā)工程師這樣的崗位名稱,讓別人一看就知道是做什么的),然后幾個(gè)名字加在一起 形成了架構(gòu)模式這個(gè)抽象概念。
-
-
從具體到抽象,然后再由這個(gè)抽象概念去指導(dǎo)程序員實(shí)踐寫代碼,促進(jìn)了架構(gòu)的傳播,比如MVC、MVP、MVVM、MVVM-C、MVI、MV-Whatever,VIPER,Redux and More……
-
-
有時(shí)候并不一定是架構(gòu)模式的錯(cuò),還有可能是平臺(tái)/框架在讓架構(gòu)模式自傳播時(shí)采用的具體方案出了點(diǎn)問(wèn)題,又或者是開發(fā)者自己寫代碼的問(wèn)題。
-
有時(shí)候架構(gòu)模式之間也不是互斥的,也可以在不同場(chǎng)景下互補(bǔ)。比如在MVC里,每個(gè)業(yè)務(wù)表現(xiàn)模塊不一定要有三個(gè)元素齊聚,甚至也可以 VC-M-VC 共用一個(gè)M(可參考斯坦福iOS開發(fā)課程內(nèi)容)。
有一個(gè)實(shí)際的業(yè)務(wù)場(chǎng)景是這么實(shí)踐的:
MVC和MVVM在一個(gè)業(yè)務(wù)場(chǎng)景里相結(jié)合。
這里提到了MVVM, 前面的Kotlin跨平臺(tái)圖,除了涉及到 View -> Model 的引用關(guān)系是否應(yīng)該存在的討論,也涉及到了Declarative-UI和MVVM架構(gòu)模式等概念, 再順便展開下。
1)MVVM 和 MVW
MVC、MVP 和 Declarative-UI 這三個(gè)名詞概念都有幾十年歷史了,其中聲明式UI在近十年又開始火了起來(lái); 而MVVM則有十幾年歷史。
官方對(duì)MVVM有這么一些描述:
-
V和VM是一對(duì)多的關(guān)系,即View 1:n ViewModel。相比于 MVC里 Controller 1:n View,顯然MVC的C更容易膨脹,所以上面也提到了使用多Controller方案。
-
V引用VM,VM操作M,而VM不需要引用V(和MVP不同)。VM作為V的上下文,包含V所需用來(lái)展示的數(shù)據(jù),響應(yīng)V被用戶觸發(fā)的事件。
-
V對(duì)M無(wú)感知,反過(guò)來(lái) M 對(duì) V和VM 也無(wú)感知。整體是松散解耦的模式。
-
當(dāng)VM的屬性值發(fā)生變化時(shí),通過(guò)數(shù)據(jù)綁定方式傳達(dá)給V。這就需要有機(jī)制支撐,有對(duì)應(yīng)的機(jī)制MVC也能做數(shù)據(jù)綁定。
基于上述描述我畫了下圖:
總體來(lái)說(shuō),MVVM是基于事件驅(qū)動(dòng)的、以數(shù)據(jù)綁定機(jī)制為支撐的松散解耦架構(gòu)模式。
-
它的前提是有系統(tǒng)/平臺(tái)/框架的機(jī)制支撐,不然實(shí)現(xiàn)成本和復(fù)雜度有點(diǎn)大。
-
相比MVC,它的優(yōu)勢(shì)是視圖控制邏輯不太會(huì)膨脹,代碼單元更容易被測(cè)試,在可讀性、可維護(hù)性上更好。
-
它可能帶來(lái)的成本/問(wèn)題是入門上手較難,簡(jiǎn)單場(chǎng)景下使用容易過(guò)度設(shè)計(jì),復(fù)雜場(chǎng)景下出問(wèn)題調(diào)試比較麻煩。
考慮到ViewController在該場(chǎng)景的薄膠水特質(zhì),以及也參與視圖展示和用戶響應(yīng),所以我把它放在V里。
由于業(yè)界有太多MV-開頭的模式名詞了,所以Angular官方直接聲明了一個(gè)MVW:
I hereby declare AngularJS to be MVW framework - Model-View-Whatever. Where Whatever stands for "whatever works for you".
這種聲明有個(gè)好處,就是免去了誰(shuí)對(duì)誰(shuí)錯(cuò)/誰(shuí)好誰(shuí)壞的爭(zhēng)執(zhí),而重點(diǎn)關(guān)注于誰(shuí)適合。
5 誰(shuí)對(duì)誰(shuí)錯(cuò)的務(wù)實(shí)案例:VIPER和分層演變
關(guān)于誰(shuí)對(duì)誰(shuí)錯(cuò)/誰(shuí)好誰(shuí)壞,可以再來(lái)看一個(gè)案例。
VIPER 架構(gòu)模式的應(yīng)用
VIPER概念由View、Interactor、Presenter、Entity、Router幾個(gè)元素組成,大致如下:
我之前寫過(guò)MVC、MVP、MVVM、VIPER等架構(gòu)模式下的代碼,完全實(shí)現(xiàn)或不完全實(shí)現(xiàn)。
有一次,我在某個(gè)業(yè)務(wù)表現(xiàn)模塊里應(yīng)用VIPER,然后定義了一個(gè)XxxNetworkInteractor類,用來(lái)負(fù)責(zé)做網(wǎng)絡(luò)請(qǐng)求。
VIPER架構(gòu)模式的演變
后來(lái)隨著業(yè)務(wù)場(chǎng)景的變化,一個(gè)業(yè)務(wù)模塊可能需要對(duì)接多套網(wǎng)關(guān)服務(wù), XxxNetworkInteractor就要做抽象隔離,消除業(yè)務(wù)邏輯對(duì)多套網(wǎng)關(guān)請(qǐng)求的感知,并且應(yīng)用到其它業(yè)務(wù)模塊。這就有點(diǎn)像 VPER - I - VPER 模式 —— 多個(gè)業(yè)務(wù)模塊對(duì) Interactor 進(jìn)行了復(fù)用。
進(jìn)化
-
隨著對(duì)接網(wǎng)關(guān)數(shù)的增加、網(wǎng)關(guān)能力的增強(qiáng),XxxNetworkInteractor也配套建設(shè)了更多能力。
-
隨著使用XxxNetworkInteractor的模塊越來(lái)越多,不同業(yè)務(wù)的一些通用訴求和處理邏輯也隨之增加。
-
隨著XxxNetworkInteractor的功能越來(lái)越強(qiáng)大,代碼越來(lái)越多,逐漸需要抽離出一個(gè)單獨(dú)模塊,不管是從提高編譯速度,還是從封裝暴露等角度。
于是,XxxNetworkSDK.framework誕生了。
退化
再后來(lái),隨著技術(shù)和業(yè)務(wù)的變化,底層網(wǎng)關(guān)服務(wù)ATop成為了領(lǐng)域范圍內(nèi)的事實(shí)標(biāo)準(zhǔn),之前遇到的問(wèn)題也消失了,而XxxNetworkSDK又帶來(lái)一些成本,比如要配套ATop的能力升級(jí)進(jìn)行迭代維護(hù)。
此時(shí),XxxNetworkSDK可以退出歷史舞臺(tái)了,讓業(yè)務(wù)模塊直接面向ATop編程,這樣既降低了維護(hù)成本,又帶來(lái)了一定的包大小收益。
以后,假如有另外一個(gè)領(lǐng)域范圍的業(yè)務(wù)場(chǎng)景(采用了XTop),要復(fù)用最上層的業(yè)務(wù)模塊, 由于在另外一個(gè)領(lǐng)域范圍,業(yè)務(wù)模塊要從 調(diào)用ATop 改為調(diào)用XTop。
那么,接手或負(fù)責(zé)遷移復(fù)用的開發(fā)者,會(huì)不會(huì)在想:為什么這些業(yè)務(wù)模塊直接調(diào)用ATop,而沒(méi)有使用一個(gè)XxxNetworkInteractor來(lái)做分層隔離呢?讓業(yè)務(wù)不要感知具體的數(shù)據(jù)庫(kù)或網(wǎng)絡(luò)服務(wù)實(shí)現(xiàn)。
-
如果有,他只要做一件事情:讓XxxNetworkInteractor對(duì)接X(jué)Top即可。
-
如果沒(méi)有,他需要去替換修改每個(gè)業(yè)務(wù)模塊的調(diào)用代碼、返回值處理等等。
四 寫在最后,回到問(wèn)題
不管是務(wù)虛的討論,還是務(wù)實(shí)的案例,或者是Angular的“whatever works for you”的觀點(diǎn), 結(jié)論是沒(méi)有誰(shuí)對(duì)誰(shuí)錯(cuò)、誰(shuí)好誰(shuí)壞,只有實(shí)際場(chǎng)景下要解決的核心問(wèn)題。
兵無(wú)常勢(shì),水無(wú)常形。 那么,回到想要解決的問(wèn)題:
在由不同開發(fā)人員持續(xù)迭代、進(jìn)行功能升級(jí)的軟件開發(fā)活動(dòng)中,如何保障具有復(fù)雜邏輯的商家經(jīng)營(yíng)工具的產(chǎn)品質(zhì)量。
我的想法是以可測(cè)性作為手段,來(lái)保障功能升級(jí)改造或代碼重構(gòu)后,可在合理時(shí)間范圍內(nèi)得到充分回歸驗(yàn)收,保障相關(guān)組件、模塊或整體產(chǎn)品的質(zhì)量。
具體的方案實(shí)施上,因?yàn)槭且钥蓽y(cè)性為重要關(guān)注點(diǎn),再結(jié)合目前的技術(shù)方向,我會(huì)傾向于采用MVVM。
-
MVVM的架構(gòu)模式在理解和認(rèn)知上比較成熟(應(yīng)該是移動(dòng)端開發(fā)領(lǐng)域TOP2流行的),便于實(shí)施和傳播,并通過(guò)模式的名稱來(lái)體現(xiàn)要強(qiáng)調(diào)的關(guān)注點(diǎn):可測(cè)性。
-
MVVM可以更好地結(jié)合SwiftUI+Combine、Kotlin跨平臺(tái)等技術(shù)方向。
-
MVVM完全版的上手門檻和簡(jiǎn)單場(chǎng)景的過(guò)度設(shè)計(jì)問(wèn)題,可以通過(guò)采用不完全版MVVM來(lái)解決。
-
-
不完全版的MVVM可以cover適用于簡(jiǎn)單場(chǎng)景的MVC,近似于 超集-子集 關(guān)系。
-
-
雖然也可以通過(guò)多Controller的方式來(lái)解決MVC膨脹問(wèn)題,但MVC的命名在實(shí)踐中就容易讓程序員弱化掉可測(cè)性這個(gè)關(guān)注點(diǎn)(也可能是我個(gè)人理解和實(shí)踐不夠正確)。
-
-
復(fù)雜場(chǎng)景的調(diào)試問(wèn)題、更多可測(cè)性的實(shí)踐,需要再摸索下,也希望得到相關(guān)分享和指點(diǎn)。