iOS VIPER架構(gòu)實(shí)踐(二):VIPER詳解與實(shí)現(xiàn)

第一篇文章對VIPER進(jìn)行了簡單的介紹,這篇文章將從VIPER的源頭開始,比較現(xiàn)有的幾種VIPER實(shí)現(xiàn),對VIPER進(jìn)行進(jìn)一步的職責(zé)剖析,并對各種細(xì)節(jié)實(shí)現(xiàn)問題進(jìn)行挖掘和探討。最后給出兩個(gè)完整的VIPER實(shí)現(xiàn),并且提供快速生成VIPER代碼的模板。

Demo和輪子的github地址是:ZIKViper,路由工具:ZIKRouter。有用請點(diǎn)個(gè)star~
注意,Demo需要先用pod install安裝一下依賴庫。

兩個(gè)實(shí)現(xiàn)展示了以下問題的解決方案:

  • 如何徹底地解決不同模塊之間的耦合
  • 如何在一個(gè)模塊里引入子模塊
  • 子模塊和父模塊之間如何通信
  • 如何對模塊進(jìn)行依賴注入
  • 面向接口的路由工具

目錄

  • 起源
  • Clean Architecture
    • Enterprise Business Rules
    • Application Business Rules
    • Interface Adapters
    • Frameworks & Drivers
    • 總結(jié)
  • 現(xiàn)有的各種VIPER實(shí)現(xiàn)
    • Brigade團(tuán)隊(duì)的實(shí)現(xiàn)
      • 爭議
    • Rambler&Co團(tuán)隊(duì)的實(shí)現(xiàn)
      • 爭議
    • Uber團(tuán)隊(duì)的實(shí)現(xiàn)
      • 各部分職責(zé)
      • 數(shù)據(jù)驅(qū)動
      • 爭議
      • 其他設(shè)計(jì)
  • 方案一:最完整的VIPER
    • View
    • Presenter
    • Interactor
    • Service
    • Wireframe
    • Router
    • Adapter
    • Builder
  • 模塊間解耦
  • 子模塊
    • 子模塊的來源
    • 通信方式
  • 依賴注入
  • 映射到MVC
  • 方案二:允許適當(dāng)耦合
    • View
    • Presenter
    • Interactor
    • 路由和依賴注入
    • 總結(jié)
  • Demo和代碼模板
  • 參考

起源

VIPER架構(gòu),最初是2013年在MutualMobile的技術(shù)博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他們的博客網(wǎng)站有過一次遷移,原文地址已經(jīng)失效,這是遷移后的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPS。

這是文章中提出的架構(gòu)示意圖:

viper_mutualmobile

Wireframe可以看作是Router的另一種表達(dá)??梢钥吹剑琕IPER之間的關(guān)系已經(jīng)很明確了。之后,作者在2014年在objc.io上發(fā)表了另一篇更詳細(xì)的介紹文章:Architecting iOS Apps with VIPER。

在作者的第一篇文章里,闡述了VIPER是在接觸到了Uncle Bob的Clean Architecture后,對Clean Architecture的一次實(shí)踐。因此,VIPER真正的源頭應(yīng)該是Clean Architecture。

Clean Architecture

由Uncle Bob在2011年提出的Clean Architecture,是一個(gè)平臺無關(guān)的抽象架構(gòu)。想要詳細(xì)學(xué)習(xí)的,可以閱讀作者的原文:Clean Architecture,翻譯:干凈的架構(gòu)The Clean Architecture。

它通過梳理軟件中不同層之間的依賴關(guān)系,提出了一個(gè)自外向內(nèi),單向依賴的架構(gòu),如下圖所示:

Clean Architecture

越靠近內(nèi)層,越變得抽象,越接近設(shè)計(jì)的核心。越靠近外層,越和具體的平臺和實(shí)現(xiàn)技術(shù)相關(guān)。內(nèi)層的部分完全不知道外層的存在和實(shí)現(xiàn)方式,代碼只能從外層向內(nèi)層引用,目的是為了實(shí)現(xiàn)層與層之間的隔離。將不同抽象程度的層進(jìn)行隔離,做到了把業(yè)務(wù)規(guī)則和具體實(shí)現(xiàn)分離開。你可以把外層看作是內(nèi)層的delegate,外層只能通過內(nèi)層提供的delegate接口來使用內(nèi)層。

Enterprise Business Rules

代表了這個(gè)軟件項(xiàng)目的業(yè)務(wù)規(guī)則。由數(shù)據(jù)實(shí)體體現(xiàn),是一些可以在不同的程序應(yīng)用之間共享的數(shù)據(jù)結(jié)構(gòu)。

Application Business Rules

代表了本應(yīng)用所使用的一些業(yè)務(wù)規(guī)則。封裝和實(shí)現(xiàn)了用到的業(yè)務(wù)功能,會將各種實(shí)體的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)為在用例中傳遞的實(shí)體類,但是和具體的數(shù)據(jù)庫技術(shù)或者UI無關(guān)。

Interface Adapters

接口適配層。將用例的規(guī)則和具體的實(shí)現(xiàn)技術(shù)進(jìn)行抽象地對接,將用例中用到的實(shí)體類轉(zhuǎn)為供數(shù)據(jù)庫存儲的格式或者供View展示的格式。類似于MVVM中把Model的數(shù)據(jù)傳遞給ViewModel供View顯示。

右下角表示了接口適配層中不同模塊間的通信方式。不同的模塊在業(yè)務(wù)用例中產(chǎn)生關(guān)聯(lián)和數(shù)據(jù)傳遞。Input、Output就是Use Case提供給外層的數(shù)據(jù)流動接口。

Frameworks & Drivers

庫和驅(qū)動層,代表了選用的各種具體的實(shí)現(xiàn)技術(shù),例如持久層使用SQLite還是Core Data,網(wǎng)絡(luò)層使用NSURLSession、NSURLConnection還是AFNetworking等。

總結(jié)

可以看到,Clean Architecture里已經(jīng)出現(xiàn)了Use Case、Interactor、Presenter等概念,它為VIPER的工程實(shí)現(xiàn)提供了設(shè)計(jì)思想,VIPER將它的設(shè)計(jì)轉(zhuǎn)化成了具體的實(shí)現(xiàn)。VIPER里的各部分正是存在著由外向內(nèi)的依賴,從外向內(nèi)表現(xiàn)為:View -> Presenter -> Interactor -> EntityWireframe嚴(yán)格來說也是一類特殊的Use Case,用于不同模塊之間通信,連接了不同的Presenter。

必須要記住的是,VIPER架構(gòu)是根據(jù)由外向內(nèi)的依賴關(guān)系來設(shè)計(jì)的。這句話是指導(dǎo)我們進(jìn)行進(jìn)一步設(shè)計(jì)和優(yōu)化的關(guān)鍵。

現(xiàn)有的各種VIPER實(shí)現(xiàn)

MutualMobile的那兩篇文章雖然已經(jīng)明確了VIPER各部分之間的職責(zé),并且給出了簡單的Demo,但是對Wireframe部分的實(shí)現(xiàn)有些爭議,解耦做得不夠徹底,并且對各層之間如何交互還處在最簡單的實(shí)現(xiàn)上。之后出現(xiàn)了挺多文章來將VIPER進(jìn)一步細(xì)化,不過某些細(xì)節(jié)的實(shí)現(xiàn)上有些差別,在給出我自己的VIPER之前,我將先對這些實(shí)現(xiàn)進(jìn)行一次綜合的比較分析,看看他們都使用了哪些技術(shù),遇到了哪些爭議點(diǎn)。不同實(shí)現(xiàn)之間已經(jīng)公認(rèn)的地方我就不再單獨(dú)列出了。

Brigade團(tuán)隊(duì)的實(shí)現(xiàn)

原文地址:Brigade’s Experience Using an MVC Alternative: VIPER architecture for iOS applications。

文章把VIPER的優(yōu)點(diǎn)總結(jié)了一下,提出了這樣的架構(gòu)圖:

Brigade’s VIPER

他們對VIPER的各部分都沒有異議,只是對Interactor的實(shí)現(xiàn)進(jìn)行了進(jìn)一步細(xì)化。用一個(gè)Data Manager提供給各個(gè)Use Case管理Entity,比如獲取、存儲功能。在Service中調(diào)用網(wǎng)絡(luò)層去獲取服務(wù)端的數(shù)據(jù)。

文章中還認(rèn)為應(yīng)該由Wireframe負(fù)責(zé)初始化整個(gè)VIPER,生成各部分的類,并設(shè)置依賴關(guān)系,并且引用另一個(gè)模塊的Wireframe,負(fù)責(zé)跳轉(zhuǎn)到另一個(gè)界面。

和這個(gè)實(shí)現(xiàn)類似的還有:

針對VIPER需要編寫太多初始化代碼的麻煩,可以使用Xcode自帶的Template解決。而很多作者都提到了一個(gè)代碼生成工具:Generamba。

爭議

文章并沒有對VIPER進(jìn)行修改,只是進(jìn)一步細(xì)化了。這應(yīng)該是一個(gè)最簡單的實(shí)現(xiàn)。如果你要實(shí)施VIPER,參照這篇文章來沒有什么大問題。但是它沒有探討的問題是:

  • 如何解決不同Wrieframe之間的耦合?
  • Wrieframe如何知道其他模塊需要的初始化參數(shù)?
  • 在模塊間通信時(shí),Interactor的數(shù)據(jù)如何傳遞給另一個(gè)模塊?
  • 父模塊和子模塊之間是怎樣的關(guān)系?

Rambler&Co團(tuán)隊(duì)的實(shí)現(xiàn)

一個(gè)對VIPER十分感興趣的俄國團(tuán)隊(duì),編寫了一本關(guān)于VIPER的書:The-Book-of-VIPER。并且給出了一個(gè)目前網(wǎng)絡(luò)上實(shí)現(xiàn)完成度最高的開源Demo:rambler-it-ios,以及他們用于實(shí)施VIPER的庫:ViperMcFlurry。

他們整理的VIPER架構(gòu)圖如下:

Rambler&Co's VIPER

和其他實(shí)現(xiàn)不同的是,他們把VIPER的初始化和裝配工作單獨(dú)放到了一個(gè)Assembly里,Router只做界面跳轉(zhuǎn)的工作。并且把VIPER內(nèi)不同部分之間的通信統(tǒng)一用Input和Output來表示。Input表示外部主動調(diào)用模塊提供的接口,Output表示模塊通過外部實(shí)現(xiàn)所要求的接口,將事件傳遞到外部。

之所以將模塊初始化單獨(dú)放到Assembly里,是因?yàn)镽outer如果負(fù)責(zé)初始化本模塊,會違背單一職責(zé)原則。

爭議

這個(gè)實(shí)現(xiàn)的愿景很好,只是在轉(zhuǎn)變?yōu)榫唧w實(shí)現(xiàn)的時(shí)候不夠完美,有很多問題尚待解決。具體可以參見Demo。

  • Assembly使用了Typhoon這個(gè)依賴注入工具,通過Method Swizzling自動初始化VIPER的各個(gè)部分

我對Typhoon這個(gè)依賴注入工具不是特別感冒,它使用了十分復(fù)雜的run time技術(shù),想要追蹤一個(gè)對象的注入過程時(shí),會看得暈頭轉(zhuǎn)向。而且它無法實(shí)現(xiàn)運(yùn)行時(shí)由調(diào)用方動態(tài)注入,只能實(shí)現(xiàn)預(yù)定義好的靜態(tài)注入。也就是不能動態(tài)傳參。

  • 使用storyboard進(jìn)行路由

在Demo中實(shí)現(xiàn)了在執(zhí)行segue時(shí)用block來使用-prepareForSegue:sender:,實(shí)現(xiàn)向目的界面?zhèn)鲄?,?shí)現(xiàn)了動態(tài)注入。但是這樣就把路由限定在了storyboard的segue技術(shù)上,那么對于那些沒有使用storyboard的項(xiàng)目應(yīng)該怎么辦呢?Demo并沒有給出答案。而且-prepareForSegue:sender:只能向View傳參,但是有一些參數(shù)是View不應(yīng)該接觸到的,而是應(yīng)該直接傳給Presenter或者Interactor的。

  • 有時(shí)候模塊需要從Output中獲取數(shù)據(jù),例如Presenter主動獲取View中的文字,傳遞給Interactor,此時(shí)Output并不能完整描述它的職責(zé),還可以再進(jìn)一步劃分

也就是說,他們的方案在設(shè)計(jì)上是不錯(cuò)的,但在技術(shù)上還有很多改進(jìn)空間。

Uber團(tuán)隊(duì)的實(shí)現(xiàn)

Uber由于業(yè)務(wù)越來越復(fù)雜,舊項(xiàng)目的架構(gòu)已經(jīng)無法滿足當(dāng)前的需求,因此在2016年完全重構(gòu)了他們的 rider app。他們借鑒VIPER,并且設(shè)計(jì)出了一個(gè)VIPER的變種架構(gòu):Riblets。文章地址:ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP

架構(gòu)圖如下:

riblets

數(shù)據(jù)流向圖:

riblets_數(shù)據(jù)流向

父模塊和子模塊之間通信:

riblet_父子模塊間通信

各部分職責(zé)

這里只列出一些和VIPER有差異的地方:

  • Builder負(fù)責(zé)初始化Riblets模塊內(nèi)的各個(gè)部分,定義了模塊的依賴參數(shù)
  • Component負(fù)責(zé)獲取和初始化那些不是Riblets模塊內(nèi)的部分,例如services,并注入到Interactor中
  • Router負(fù)責(zé)管理子模塊,持有子模塊的Router,并把子模塊的View添加到視圖樹上
  • Interactor通過調(diào)用Service管理Model,而不是在Interactor中直接管理
  • Interactor和子模塊的Interactor通過監(jiān)聽者模式和delegate互相通信

數(shù)據(jù)驅(qū)動

最大的改變是將Router從Presenter移到了Interactor,改變了模塊的主從關(guān)系,整個(gè)模塊的生命周期現(xiàn)在由Interactor來管理。而之前的VIPER模塊是依賴于View的生命周期的。這樣一來,整個(gè)架構(gòu)就從View驅(qū)動變成了業(yè)務(wù)驅(qū)動,或者數(shù)據(jù)驅(qū)動。

關(guān)于這個(gè)改變,Uber給出了兩個(gè)原因:

  • 想要統(tǒng)一iOS和Andorid的軟件架構(gòu),以及更好地互相借鑒開發(fā)經(jīng)驗(yàn)和教訓(xùn),因而需要改變iOS中視圖驅(qū)動的設(shè)計(jì)
  • 想要創(chuàng)建一個(gè)沒有View,只有業(yè)務(wù)邏輯的模塊,因此生命周期需要由Interactor管理

爭議

Uber團(tuán)隊(duì)的確很有想法。在對他們的這個(gè)方案進(jìn)行深入實(shí)踐之前,我無法評論這個(gè)方案是好是壞,我只在這里提出一些實(shí)踐中可能會遇到的問題。

關(guān)于Uber給出的第一個(gè)原因,這是Uber團(tuán)隊(duì)基于協(xié)調(diào)兩個(gè)開發(fā)團(tuán)隊(duì)的情況而做出的選擇,如果我們沒有他們這樣統(tǒng)一開發(fā)的需求,并沒有必要借鑒。iOS的UIKit是一個(gè)視圖驅(qū)動的框架,很難做到100%數(shù)據(jù)驅(qū)動,在實(shí)踐中將會遇到許多需要解決的問題,除非有足夠的開發(fā)時(shí)間,否則不要草率地投入其中。是否要使用數(shù)據(jù)驅(qū)動的設(shè)計(jì),還是應(yīng)該由項(xiàng)目的業(yè)務(wù)設(shè)計(jì)來決定。當(dāng)數(shù)據(jù)變化大部分是由后端的Service和網(wǎng)絡(luò)數(shù)據(jù)引起時(shí),再去考慮數(shù)據(jù)驅(qū)動吧。例如Uber的地圖路線由定位模塊不斷計(jì)算,自動更新,就比較適合使用數(shù)據(jù)驅(qū)動。

關(guān)于第二個(gè)原因,一個(gè)沒有View和Presenter的VIPER,就只剩下Router、Interactor、Model,這時(shí)這個(gè)模塊可以看做是一個(gè)可以通過Router調(diào)用的Service或者M(jìn)anager,這個(gè)Service有自己的狀態(tài)和生命周期,Service也可以在View銷毀后繼續(xù)完成剩余的業(yè)務(wù)工作,只要業(yè)務(wù)需要,可以進(jìn)行自持有,自釋放。而且這個(gè)Service最終還是會表現(xiàn)在某個(gè)View上。這么看來,Router的層級已經(jīng)升高了,成為了整個(gè)app內(nèi)的模塊間通信工具,可以連接任意模塊,不僅僅是VIPER,因此Router由誰持有,就完全由模塊內(nèi)部自由管理了。

只是,在iOS中的VIPER里,實(shí)際的路由API都是存在于UIViewController上的,Router會直接和View產(chǎn)生引用,把Router放到和View隔離的Interactor里會破壞隔離。而且從Clean架構(gòu)的分層來看,層級升高后的Router應(yīng)該是處在Interface Adapter層和Framework & Driver層之間,而Interactor則是在Application Business Rules層,由Interactor來管理其他角色,會破壞了Clean Architecture里的依賴關(guān)系。

比如一個(gè)沒有View的、用于管理語音通話數(shù)據(jù)的Interactor,收到了通話異常中斷的事件,在處理事件時(shí),它不應(yīng)該通過Router將自己移除,或者結(jié)束整個(gè)語音通話業(yè)務(wù),或者自動調(diào)用重新?lián)芴柕臉I(yè)務(wù),這樣很容易會讓不同的Use Case之間產(chǎn)生耦合,這些都應(yīng)該由更上層的Service去選擇執(zhí)行,如果有頁面跳轉(zhuǎn)的設(shè)計(jì),則應(yīng)該把事件轉(zhuǎn)發(fā)給一個(gè)存在Presenter層的Parent VIPER模塊,由parent來決定是退出通話界面還是彈窗提示。當(dāng)一個(gè)Interactor沒有Presenter和View時(shí),它一定是另一個(gè)VIPER的子模塊。這么看來,在沒有View時(shí),或許讓Service來持有Router才是正確的。

因此,如果真的有把VIPER變成數(shù)據(jù)驅(qū)動的需求,主要還是源于Uber給出的第一個(gè)基于團(tuán)隊(duì)統(tǒng)一的理由。

其他設(shè)計(jì)

文章里還給出了一些很有參考價(jià)值的內(nèi)容,比如:

  • 對Interactor進(jìn)行注入的Component
  • 視圖樹變成了Router樹
  • Interactor不直接維護(hù)Model,而是通過對應(yīng)的Service來維護(hù)Model
  • 父模塊和子模塊之間通過Interactor來通信

Uber的這個(gè)方案講了很多其他方案沒有提到的方面,比如依賴注入、如何引入子模塊等問題。不過這個(gè)方案并沒有開源。

方案一:最完整的VIPER

各種實(shí)現(xiàn)方案都分析了一遍,接下來就開始進(jìn)行一個(gè)總結(jié)。首先總結(jié)出一個(gè)絕對標(biāo)準(zhǔn)的VIPER,各部分遵循隔離關(guān)系,同時(shí)考慮到依賴注入、子模塊通信、模塊間解耦等問題,將VIPER的各部分的職責(zé)變得更加明確,也新增了幾個(gè)角色。示例圖如下,各角色的顏色和Clean Architecture圖中各層的顏色對應(yīng):

thorough_viper

示例代碼將用一個(gè)筆記應(yīng)用作為演示。

View

View可以是一個(gè)UIView + UIViewController,也可以只是一個(gè)custom UIView,也可以是一個(gè)自定義的用于管理UIView的Manager,只要它實(shí)現(xiàn)了View的接口就可以。

View層的職責(zé):

  • 展示界面,組合各種UIView,并在UIViewController內(nèi)管理各種控件的布局、更新
  • View對外暴露各種用于更新UI的接口,而自己不主動更新UI
  • View持有一個(gè)由外部注入的eventHandler對象,將View層的事件發(fā)送給eventHandler
  • View持有一個(gè)由外部注入的viewDataSource對象,在View的渲染過程中,會從viewDataSource獲取一些用于展示的數(shù)據(jù),viewDataSource的接口命名應(yīng)該盡量和具體業(yè)務(wù)無關(guān)
  • View向Presenter提供routeSource,也就是用于界面跳轉(zhuǎn)的源界面

View層會引入各種自定義控件,這些控件有許多delegate,都在View層實(shí)現(xiàn),統(tǒng)一包裝后,再交給Presenter層實(shí)現(xiàn)。因?yàn)镻resenter層并不知道View的實(shí)現(xiàn)細(xì)節(jié),因此也就不知道這些控件的接口,Presenter層只知道View層統(tǒng)一暴露出來的接口。而且這些控件的接口在定義時(shí)可能會將數(shù)據(jù)獲取、事件回調(diào)、控件渲染接口混雜起來,最具代表性的就是UITableViewDataSource里的-tableView:cellForRowAtIndexPath:。這個(gè)接口同時(shí)涉及到了UITableViewCell和渲染cell所需要的Model,是非常容易產(chǎn)生耦合的地方,因此需要做一次分解。應(yīng)該在View的dataSource里定義一個(gè)從外部獲取所需要的簡單類型數(shù)據(jù)的方法,在-tableView:cellForRowAtIndexPath:里用獲取到的數(shù)據(jù)渲染cell。示例代碼:

@protocol ZIKNoteListViewEventHandler <NSObject>
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath;
@end
@protocol ZIKNoteListViewDataSource <NSObject>
- (NSInteger)numberOfRowsInSection:(NSInteger)section;
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface ZIKNoteListViewController () <UITableViewDelegate,UITableViewDataSource>
@property (nonatomic, strong) id<ZIKNoteListViewEventHandler> eventHandler;
@property (nonatomic, strong) id<ZIKNoteListViewDataSource> viewDataSource;
@property (weak, nonatomic) IBOutlet UITableView *noteListTableView;
@end

@implementation ZIKNoteListViewController

- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
                                      text:(NSString *)text
                                detailText:(NSString *)detailText {
    UITableViewCell *cell = [self.noteListTableView dequeueReusableCellWithIdentifier:@"noteListCell" forIndexPath:indexPath];
    cell.textLabel.text = text;
    cell.detailTextLabel.text = detailText;
    return cell;
}


#pragma mark UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.viewDataSource numberOfRowsInSection:section];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *text = [self.viewDataSource textOfCellForRowAtIndexPath:indexPath];
    NSString *detailText = [self.viewDataSource detailTextOfCellForRowAtIndexPath:indexPath];
    UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath
                                                   text:text
                                             detailText:detailText];
    return cell;
}

#pragma mark UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    [self.eventHandler handleDidSelectRowAtIndexPath:indexPath];
}

@end

一般來說,viewDataSource和eventHandler都是由Presenter來擔(dān)任的,Presenter接收到dataSource請求時(shí),從Interactor里獲取并返回對應(yīng)的數(shù)據(jù)。你也可以選擇在View和Presenter之間用ViewModel來進(jìn)行交互。

Presenter

Presenter由View持有,它的職責(zé)有:

  • 接收并處理來自View的事件
  • 維護(hù)和View相關(guān)的各種狀態(tài)和配置,比如界面是否使用夜間模式等
  • 調(diào)用Interactor提供的Use Case執(zhí)行業(yè)務(wù)邏輯
  • 向Interactor提供View中的數(shù)據(jù),讓Interactor生成需要的Model
  • 接收并處理來自Interactor的業(yè)務(wù)事件回調(diào)事件
  • 通知View進(jìn)行更新操作
  • 通過Wireframe跳轉(zhuǎn)到其他View

Presenter是View和業(yè)務(wù)之間的中轉(zhuǎn)站,它不包含業(yè)務(wù)實(shí)現(xiàn)代碼,而是負(fù)責(zé)調(diào)用現(xiàn)成的各種Use Case,將具體事件轉(zhuǎn)化為具體業(yè)務(wù)。Presenter里不應(yīng)該導(dǎo)入U(xiǎn)IKit,否則就有可能入侵View層的渲染工作。Presenter里也不應(yīng)該出現(xiàn)Model類,當(dāng)數(shù)據(jù)從Interactor傳遞到Presenter里時(shí),應(yīng)該轉(zhuǎn)變?yōu)楹唵蔚臄?shù)據(jù)結(jié)構(gòu)。

示例代碼:

@interface ZIKNoteListViewPresenter () < ZIKNoteListViewDataSource, ZIKNoteListViewEventHandler >
@property (nonatomic, strong) id<ZIKNoteListWireframeProtocol> wireframe;
@property (nonatomic, weak) id<ZIKViperView,ZIKNoteListViewProtocol> view;
@property (nonatomic, strong) id<ZIKNoteListInteractorInput> interactor;
@end

@implementation ZIKNoteListViewPresenter

#pragma mark ZIKNoteListViewDataSource

- (NSInteger)numberOfRowsInSection:(NSInteger)section {
    return self.interactor.noteCount;
}

- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *title = [self.interactor titleForNoteAtIndex:indexPath.row];
    return title;
}

- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *content = [self.interactor contentForNoteAtIndex:indexPath.row];
    return content;
}

#pragma mark ZIKNoteListViewEventHandler

- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *uuid = [self.interactor noteUUIDAtIndex:indexPath.row];
    NSString *title = [self.interactor noteTitleAtIndex:indexPath.row];
    NSString *content = [self.interactor noteContentAtIndex:indexPath.row];
    
    [self.wireframe pushEditorViewForEditingNoteWithUUID:uuid title:title content:content delegate:self];
}

@end

Interactor

Ineractor的職責(zé):

  • 實(shí)現(xiàn)和封裝各種業(yè)務(wù)的Use Case,供外部調(diào)用
  • 維護(hù)和業(yè)務(wù)相關(guān)的各種狀態(tài),比如是否正在編輯筆記
  • Interactor可以獲取各種Manager和Service,用于組合實(shí)現(xiàn)業(yè)務(wù)邏輯,這些Manager和Service應(yīng)該是由外部注入的依賴,而不是直接引用具體的類
  • 通過DataManager維護(hù)Model
  • 監(jiān)聽各種外部的業(yè)務(wù)事件并處理,必要時(shí)將事件發(fā)送給eventHandler
  • Interactor持有一個(gè)由外部注入的eventHandler對象,將需要外部處理的業(yè)務(wù)事件發(fā)送給eventHandler,或者通過eventHandler接口對某些數(shù)據(jù)操作的過程進(jìn)行回調(diào)
  • Interactor持有一個(gè)由外部注入的dataSource對象,用于獲取View上的數(shù)據(jù),以更新Model

Interactor是業(yè)務(wù)的實(shí)現(xiàn)者和維護(hù)者,它會調(diào)用各種Service來實(shí)現(xiàn)業(yè)務(wù)邏輯,封裝成明確的用例。而這些Service在使用時(shí),也都是基于接口的,因?yàn)镮nteractor的實(shí)現(xiàn)不和具體的類綁定,而是由Application注入Interactor需要的Service。

示例代碼:

@protocol ZIKNoteListInteractorInput <NSObject>
- (void)loadAllNotes;
- (NSInteger)noteCount;
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx;
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx;
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx;
- (NSString *)noteTitleAtIndex:(NSUInteger)idx;
- (NSString *)noteContentAtIndex:(NSUInteger)idx;
@end
@interface ZIKNoteListInteractor : NSObject <ZIKNoteListInteractorInput>
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id eventHandler;
@end

@implementation ZIKNoteListInteractor

- (void)loadAllNotes {
    [[ZIKNoteDataManager sharedInsatnce] fetchAllNotesWithCompletion:^(NSArray *notes) {
        [self.eventHandler didFinishLoadAllNotes];
    }];
}

- (NSArray<ZIKNoteModel *> *)noteList {
    return [ZIKNoteDataManager sharedInsatnce].noteList;
}

- (NSInteger)noteCount {
    return self.noteList.count;
}

- (NSString *)titleForNoteAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] title];
}

- (NSString *)contentForNoteAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] content];
}

- (NSString *)noteUUIDAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] uuid];
}

- (NSString *)noteTitleAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] title];
}

- (NSString *)noteContentAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] content];
}

@end

Service

向Interactor提供各種封裝好的服務(wù),例如數(shù)據(jù)庫的訪問、存儲,調(diào)用定位功能等。Service由Application在執(zhí)行路由時(shí)注入到Builder里,再由Buidler注入到Interactor里。也可以只注入一個(gè)Service Router,在運(yùn)行時(shí)再通過這個(gè)Service Router懶加載需要的Service,相當(dāng)于注入了一個(gè)提供Router功能的Service。

Service可以看作是沒有View的VIPER,也有自己的路由和Builder。

Wireframe

翻譯成中文叫線框,用于表達(dá)從一個(gè)Module到另一個(gè)Module的過程。雖然也是扮演者執(zhí)行路由的角色,但是其實(shí)它和Router是有區(qū)別的。

Wireframe和storyboard中連接好的一個(gè)個(gè)segue類似,負(fù)責(zé)提供一系列具體的路由用例,這個(gè)用例里已經(jīng)配置好了源界面和目的界面的一些依賴,包括轉(zhuǎn)場動畫、模塊間傳參等。Wireframe的接口是提供給模塊內(nèi)部使用的,它通過調(diào)用Router來執(zhí)行真正的路由操作。

示例代碼:

@interface ZIKTNoteListWireframe : NSObject <ZIKTViperWireframe>
- (void)presentLoginViewWithMessage:(NSString *)message delegate:(id<ZIKTLoginViewDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)dismissLoginView:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
- (void)presentEditorForCreatingNewNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)pushEditorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (UIViewController *)editorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (void)pushEditorViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
- (void)quitEditorViewWithAnimated:(BOOL)animated;
@end

Router

Router則是由Application提供的具體路由技術(shù),可以簡單封裝UIKit里的那些跳轉(zhuǎn)方法,也可以用URL Router來執(zhí)行路由。但是一個(gè)模塊是不需要知道app使用的是什么具體技術(shù)的。Router才是真正連接各個(gè)模塊的地方。它也負(fù)責(zé)尋找對應(yīng)的目的模塊,并且通過Buidler進(jìn)行依賴注入。

示例代碼:

@interface ZIKTRouter : NSObject <ZIKTViperRouter>
///封裝UIKit的跳轉(zhuǎn)方法
+ (void)pushViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
+ (void)popViewController:(UIViewController *)viewController animated:(BOOL)animated;
+ (void)presentViewController:(UIViewController *)viewControllerToPresent fromViewController:(UIViewController *)source animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
+ (void)dismissViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
@end

@implementation ZIKTRouter (ZIKTEditor)

+ (UIViewController *)viewForCreatingNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate {
    return [ZIKTEditorBuilder viewForCreatingNoteWithDelegate:delegate];
}

+ (UIViewController *)viewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate {
    return [ZIKTEditorBuilder viewForEditingNoteWithUUID:uuid title:title content:content delegate:delegate];
}

@end

Adapter

由Application實(shí)現(xiàn),負(fù)責(zé)在模塊通信時(shí)進(jìn)行一些接口的轉(zhuǎn)換,例如兩個(gè)模塊使用了相同業(yè)務(wù)功能的某個(gè)Service,使用的protocol實(shí)現(xiàn)一樣,但是protocol名字不一樣,就可以在路由時(shí),在Adapter里進(jìn)行一次轉(zhuǎn)換。甚至只要定義的邏輯一樣,依賴參數(shù)的名字和數(shù)據(jù)類型也可以允許不同。這樣就能讓模塊不依賴于某個(gè)具體的protocol,而是依賴于protocol實(shí)際定義的依賴和接口。

注意這里的Adapter和Clean Architecture里的Interface Adapter是不一樣的。這里的Adapter就是字面意義上的接口轉(zhuǎn)換,而Clean Architecture里的Interface Adapter層更加抽象,是Use Case層與具體實(shí)現(xiàn)技術(shù)之間的轉(zhuǎn)換,囊括了更多的角色。

Builder

負(fù)責(zé)初始化整個(gè)模塊,配置VIPER之間的關(guān)系,并對外聲明模塊需要的依賴,讓外部執(zhí)行注入。

模塊間解耦

一個(gè)VIPER模塊可以看做是一個(gè)獨(dú)立的組件,可以被單獨(dú)封裝成一個(gè)庫,被app引用。這時(shí)候,app就負(fù)責(zé)將各個(gè)模塊連接起來,也就是圖中灰色的Application Context部分。一個(gè)模塊,肯定是存在于一個(gè)上下文環(huán)境中才能運(yùn)行起來的。

Wireframe -> Router -> Adapter -> Builder 實(shí)現(xiàn)了一個(gè)完整的模塊間路由,并且實(shí)現(xiàn)了模塊間的解耦。

其中Wireframe和Builder是分別由引用者模塊和被引用模塊提供的,是兩個(gè)模塊的出口和入口,而Router和Adapter則是由模塊的使用者——Application實(shí)現(xiàn)的。

當(dāng)兩個(gè)模塊之間存在引用關(guān)系時(shí),說明存在業(yè)務(wù)邏輯上的耦合,這種耦合是業(yè)務(wù)的一部分,是不可能消除的。我們能做的就是把耦合盡量交給模塊調(diào)用者,由Application來提供具體的類,注入到各個(gè)模塊之中,而模塊內(nèi)部只面向protocol即可。這樣的話,被引用模塊只要實(shí)現(xiàn)了相同的接口,就可以隨時(shí)替換,甚至接口有一些差異時(shí),只要被引用模塊提供了相同功能的接口,也可以通過Adapter來做接口兼容轉(zhuǎn)換,讓引用者模塊無需做任何修改。

Wireframe相當(dāng)于插頭,Builder相當(dāng)于插座,而Router和Adapter相當(dāng)于電路和轉(zhuǎn)接頭,將不同規(guī)格的插座和插頭連接起來。把這些連接和適配的工作交給Application層,就能讓兩個(gè)模塊實(shí)現(xiàn)各自獨(dú)立。

子模塊

大部分方案都沒有討論子模塊存在的情況。在VIPER里如何引入另一個(gè)VIPER模塊?多個(gè)模塊之間如何交互?子模塊由誰初始化、由誰管理?

其他幾個(gè)實(shí)現(xiàn)中,只有Uber較為詳細(xì)地討論了子模塊的問題。在Uber的Riblets架構(gòu)里,子模塊的Router被添加到父模塊的Router,模塊之間通過delegate和監(jiān)聽的方式進(jìn)行通信。這樣做會讓模塊間產(chǎn)生一定的耦合。如果子模塊是由于父View使用了一個(gè)子View控件而被引入的,那么父Interactor就會在代碼里多出一個(gè)子Interactor,這樣就導(dǎo)致了View的實(shí)現(xiàn)方式影響了Interactor的實(shí)現(xiàn)。

子模塊的來源

子模塊的來源有:

  • View引用了一個(gè)封裝好的子View控件,連帶著引入了子View的整個(gè)VIPER
  • Interactor使用了一個(gè)Service

通信方式

子View可能是一個(gè)UIView,也可能是一個(gè)Child UIViewController。因此子View有可能需要向外部請求數(shù)據(jù),也可能獨(dú)立完成所有任務(wù),不需要依賴父模塊。

如果子View可以獨(dú)立,那在子模塊里不會出現(xiàn)和父模塊交互的邏輯,只有把一些事件通過Output傳遞出去的接口。這時(shí)只需要把子View的接口封裝在父View的接口里即可,父Presenter和父Interactor是不知道父View提供的這幾個(gè)接口是通過子View實(shí)現(xiàn)的。這樣父模塊就能接收到子模塊的事件了,而且能夠保持Interactor和Presenter、View之間從低到高的依賴關(guān)系。

如果父模塊需要調(diào)用子模塊的某些功能,或者從子模塊獲取數(shù)據(jù),可以選擇封裝到父View的接口里,不過如果涉及到數(shù)據(jù)模型,并且不想讓數(shù)據(jù)模型出現(xiàn)在View的接口中,可以把子Interactor作為父Interactor的一個(gè)Service,在引入子模塊時(shí),通過父Builder注入到父Interactor里,或者根據(jù)依賴關(guān)系解耦地再徹底一點(diǎn),注入到父Presenter里,讓父Presenter再把接口轉(zhuǎn)發(fā)給父Interactor。這樣子模塊和父模塊就能通過Service的形式進(jìn)行通信了,而這時(shí),父Interactor也不知道這個(gè)Service是來自子模塊里的。

在這樣的設(shè)計(jì)下,子模塊和父模塊是不知道彼此的存在的,只是通過接口進(jìn)行交互。好處是父View如果想要更換為另一個(gè)相同功能的子View控件,就只需要在父View里修改,不會影響Presenter和Interactor。

依賴注入

這個(gè)VIPER的設(shè)計(jì)是通過接口將各個(gè)部分組合在一起的,一個(gè)類需要設(shè)置很多依賴,例如Interactor需要依賴許多Service。這就涉及到了兩個(gè)問題:

  • 在哪里配置依賴
  • 一個(gè)類怎么聲明自己的依賴

在這個(gè)方案中,由Builder聲明整個(gè)模塊的依賴,然后在Builder內(nèi)部為不同的類設(shè)置依賴,外部在注入依賴時(shí),就不必知道內(nèi)部是怎么使用這些依賴參數(shù)的。一個(gè)類如果有必需的依賴參數(shù),可以直接在init方法里體現(xiàn),對于那些非必需的依賴,可以通過暴露接口來聲明。

如果需要動態(tài)注入,而不是在模塊初始化時(shí)就配置所有的依賴,Builder也可以提供動態(tài)注入的接口。

映射到MVC

如果你需要把一個(gè)模塊從MVC重構(gòu)到VIPER,可以先按照這個(gè)步驟:

  • 整理Controller中的代碼,把不同職責(zé)的代碼用pragma mark分隔好
  • 整理好后,按照各部分的職責(zé),將代碼分散到VIPER的各個(gè)角色中,此時(shí)View、Presenter、Interactor之間可以直接互相引用
  • 把View、Presenter、Interactor進(jìn)行解耦,抽出接口,互相之間依賴接口進(jìn)行交互

下面就是第一步里在Controller中可以分隔出的職責(zé):

@implementation ViewController
//------View-------

//View的生命周期
#pragma mark View life

//View的配置,包括布局設(shè)置
#pragma mark View config

//更新View的接口
#pragma mark Update view

//View需要從model中獲取的數(shù)據(jù)
#pragma mark Request view data source

//監(jiān)控、接收View的事件
#pragma mark Send view event

//------Presenter-------

//處理View的事件
#pragma mark Handle view event

//界面跳轉(zhuǎn)
#pragma mark Wireframe

//向View提供配置用的數(shù)據(jù)
#pragma mark Provide view data source

//提供生成model需要的數(shù)據(jù)
#pragma mark Provide model data source

//處理業(yè)務(wù)事件,調(diào)用業(yè)務(wù)用例
#pragma mark Handle business event

//------Interactor-------

//監(jiān)控、接收業(yè)務(wù)事件
#pragma mark Send business event

//業(yè)務(wù)用例
#pragma mark Business use case

//獲取生成model需要的數(shù)據(jù)
#pragma mark Request data for model

//維護(hù)model
#pragma mark Manage model

@end

這里缺少了View狀態(tài)管理、業(yè)務(wù)狀態(tài)管理等職責(zé),因?yàn)檫@些狀態(tài)一般都是@property,用pragma mark不能分隔它們,只能在@interface里聲明的時(shí)候進(jìn)行隔離。

方案二:允許適當(dāng)耦合

上面的方案是以最徹底的解耦為目標(biāo)設(shè)計(jì)的,在實(shí)踐中,如果真的完全按照這個(gè)設(shè)計(jì),代碼量的確不小。其實(shí)一些地方的耦合并不會引起多大問題,除非你的模塊需要封裝成通用組件供多個(gè)app使用,否則并不需要按照100%的解耦要求來編寫。因此接下來我再總結(jié)一個(gè)稍微簡化的方案,總結(jié)一下各部分可以在哪些地方出現(xiàn)耦合,哪些耦合不能出現(xiàn)。

在這個(gè)方案里,我使用了一個(gè)中介者來減少一部分代碼,Router就是一個(gè)很適合成為中介者的角色。

架構(gòu)圖如下:

final_viper

View

  • View可以直接通過Router引入另一個(gè)子View,不需要通過Presenter的路由來引入
  • View中的一些delegate如果變化的可能性不大,可以直接讓Presenter實(shí)現(xiàn)(例如UITableViewDataSource),不用再封裝一遍后交給Presenter
  • View不能出現(xiàn)Model類

Presenter

  • Presenter可以直接調(diào)用Router執(zhí)行路由,不用再通過Wireframe封裝一遍
  • Presenter的接口參數(shù)中可以出現(xiàn)Model類,但是不能導(dǎo)入Model類的頭文件并且使用Model類,只能用于參數(shù)傳遞
  • Presenter中不建議導(dǎo)入U(xiǎn)IKit,除非能保證不會使用那些會影響控件渲染的方法

Interactor

  • 一些app中常用的Service可以直接引入,不需要通過外部注入的方式來使用
  • Interactor可以用一個(gè)Service Router來動態(tài)獲取Service

路由和依賴注入

改變得最多的就是路由部分。View、Presenter和Interactor都可以使用路由來獲取一些模塊。View可以通過路由獲取子View,Presenter可以通過路由獲取其他View模塊,Interactor可以通過路由獲取Service。

在實(shí)現(xiàn)時(shí),可以把Wireframe、Router、Builder整合到一起,全都放到Router里,Router由模塊實(shí)現(xiàn)并提供給外部使用。類似于Brigade團(tuán)隊(duì)和Rambler&Co團(tuán)隊(duì)的實(shí)現(xiàn)。但是他們的實(shí)現(xiàn)都是直接在Router里引入其他模塊的Router,這樣會導(dǎo)致依賴混亂,更好的方式是通過一個(gè)中間人統(tǒng)一提供其他模塊的接口。

我在這里造了個(gè)輪子,通過protocol來尋找需要的模塊并執(zhí)行路由,不用直接導(dǎo)入目的模塊中的類,并且提供了Adapter的支持,可以讓多個(gè)protocol指向同一個(gè)模塊。這樣就能避免模塊間的直接依賴。

示例代碼:

///editor模塊的依賴聲明
@protocol NoteEditorProtocol <NSObject>
@property (nonatomic, weak) id<ZIKEditorDelegate> delegate;
- (void)constructForCreatingNewNote;
- (void)constructForEditingNote:(ZIKNoteModel *)note;
@end

@implementation ZIKNoteListViewPresenter

- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSAssert([[self.view routeSource] isKindOfClass:[UIViewController class]], nil);
    
    //跳轉(zhuǎn)到編輯器界面;通過protocol獲取對應(yīng)的router類,再通過protocol注入依賴
    //App可以用Adapter把NoteEditorProtocol和真正的protocol進(jìn)行匹配和轉(zhuǎn)接
    [ZIKViewRouterToModule(NoteEditorProtocol)
         performFromSource:[self.view routeSource] //跳轉(zhuǎn)的源界面
         configuring:^(ZIKViewRouteConfiguration<NoteEditorProtocol> *config) {
             //路由配置
             //設(shè)置跳轉(zhuǎn)方式,支持所有界面跳轉(zhuǎn)類型
             config.routeType = ZIKViewRouteTypePush;
             //Router內(nèi)部負(fù)責(zé)用獲取到的參數(shù)初始化editor模塊
             config.delegate = self;
             [config constructForEditingNote:[self.interactor noteAtIndex:indexPath.row]];
             config.prepareForRoute = ^(id destination) {
                 //跳轉(zhuǎn)前配置目的界面
             };
             config.routeCompletion = ^(id destination) {
                 //跳轉(zhuǎn)結(jié)束處理
             };
             config.performerErrorHandler = ^(SEL routeAction, NSError * error) {
                 //跳轉(zhuǎn)失敗處理
             };
         }];
}

@end

總結(jié)

這個(gè)方案依賴于一個(gè)統(tǒng)一的中間人,也就是路由工具,在我的實(shí)現(xiàn)里就是ZIKRouter。View、Presenter、Interactor都可以使用對應(yīng)功能的Router獲取子模塊。而由于ZIKRouter仍然是通過protocol的方式來和子模塊進(jìn)行交互,因此仍然可保持模塊間解耦。唯一的耦合就是各部分都引用了ZIKRouter這個(gè)工具。如果你想把模塊和ZIKRouter的耦合也去除,可以讓Router也變成面向接口,由外部注入。

Demo和代碼模板

針對兩個(gè)方案,同時(shí)寫了兩個(gè)相同功能的Demo,可以比較一下代碼上的區(qū)別。地址在:ZIKViper。注意,Demo需要先用pod install安裝一下依賴庫。

項(xiàng)目里也提供了Xcode File Template用于快速生成VIPER代碼模板。把.xctemplate后綴的文件夾拷貝到~/Library/Developer/Xcode/Templates/目錄下,就可以在Xcode的New->File->Template里選擇代碼模板快速生成代碼。

總結(jié)

VIPER是按照Clean Architecture中由外向內(nèi)的依賴進(jìn)行設(shè)計(jì)的,各部分職責(zé)十分明確。并且由于引入了路由部分,更容易支持組件化開發(fā)。

下一篇文章將討論基于接口的路由設(shè)計(jì),總結(jié)UIKit中的各種視圖轉(zhuǎn)場,并講解ZIKRouter的實(shí)現(xiàn)。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容