React語境下前端DDD的長年探索經驗

圖片‍‍‍‍‍

導語 | 騰訊前端工程師唐霜在React項目中,嘗試使用DDD方法論爲業務對象建模,其所在團隊形成良好的業務溝通規範和業務邏輯沉澱流程,構建了更加穩固的業務系統。作者將多年的積累探索經驗總結分享出來,從對業務的思考、react項目的特徵出發,闡述在項目中進行的前端DDD探索。歡迎閱讀和交流。

圖片

前言

我們所處的業務團隊服務於騰訊某投資部門,系統涉及的投資相關業務在國內具有典型意義,是覆蓋一級市場、二級市場、基金的多品類全流程的投資系統。其中包含了對投資項目本身的業務處理,也包含了投資流程的工作處理(類比OA系統),還包括了其他大部分系統需要考慮的技術建設(例如基於安全性考慮的數據、合同文件、電子籤等)。方方面面使得我們在建設這套系統時除了要有技術本身之外,還要有對業務的掌握能力。雖然技術團隊跟隨產品團隊完成技術開發,但是在一些具體問題上如果對業務不熟悉,很難真正處理好開發過程中的某些具體的邏輯代碼。而對於投資系統而言,準確實現業務是最基本的要求,否則帶來的風險可想而知。這也給我們技術團隊帶來了巨大挑戰:如何在如此複雜的系統中,比較合理地掌握每一個技術細節背後的業務邏輯,確保業務實現的準確呢?

我們把目光瞄準了DDD,它是由Eric Evans在_Domain-Driven Design: Tackling Complexity in the Heart of Software(2004)_總結提出的一整套概念和方法論。我們嘗試探索在前端踐行DDD的可能性。DDD的理念,可以幫助我們團隊合理的形成良好的業務溝通規範和業務邏輯沉澱流程;DDD在技術方面的指導,幫助我們構建更加穩固的業務系統。基於這樣的想法,我們開啓了相關的探索。

圖片

什麼是DDD?

Domain-Driven Design是從實際業務出發,站在解決領域問題的角度去思考和設計系統的方法論。它包含了兩個方面的內容:溝通方法論和研發方法論。

Eric從另一個角度讓我們技術人員重新思考自己的工作方式,對於面臨複雜業務的系統開發的技術人員,不能一上來就開始進行系統設計和代碼實現。他們要做的第一步,應該是和領域專家一道,基於某種大家都能理解的專業語言,構建出一套領域模型,這也就是我們所講的“溝通方法論”。而這個步驟,是我們現在的很多開發團隊完全沒有考慮過的事情。首先,這裏的“領域”(Domain)是指一類事物的集合,比如我們常講的金融領域、通信領域、數學領域等等。你可以很明顯的感知到,“領域”意味着“邊界”,意味着某些“共性”和某些“特性”。領域專家就是這些特定業務領域的資深工作者,對該領域的業務非常瞭解,是該領域的行家。要讓技術人員和領域專家坐在一起構建出對應業務的領域模型,必須摒棄他們各自在自己工作範圍內的狹義概念,大家找到一種相互都可以理解的概念表達方式,來最終確認各自提出的問題和回答對方都能準確理解。這在我們看來,就是世界上最高效的溝通方式,沒有之一。

我們可以使用DSL領域專用語言)來完成這一溝通,領域專家和技術專家基於統一語言構建領域模型,可想而知,這一模型是無法直接作爲代碼運行的。開發人員要做的,是基於該領域模型,用代碼將其準確實現。而Eric充分考慮到一個系統在技術實現時和現實世界存在差異,提出了非常多的技術建模方案(Scheme)和配套的架構理念,讓技術專家在和領域專家構建領域模型時,有意無意地引導領域專家一起構建出更加符合後期技術實現的模型架構體系。

圖片

圖1 統一語言、領域模型與代碼實現的關係

因此,DDD不是某種架構而是一種設計。不同的業務團隊,可以基於這種設計實現一種符合DDD的架構,來幫助自己更好的構建自己的系統。閱讀上書過程中我們反覆意識到,這本書是寫給技術人員的看上去都是技術字眼但實際上是在傳播工作方法的一本方法論書籍。如果有這樣的想法,那麼在閱讀此書,特別是後面的章節時,就不會越來越混亂。因爲你不再是在這本書中去找到某些具體的實現或架構,而是在思考如何去發現規律。它不能幫助你完成某個架構,但是它可以幫助你思考如何設計這個架構。

現在,回到我們的業務中,爲什麼DDD正好可以幫助解決複雜業務的系統設計呢?

圖片

業務的技術語義

1)技術上講,什麼是業務?

業務(Business) 專指商業活動,是實現企業生產到利益回收的一個環節。它的總和,構成了該企業盈利活動的整個流程。一般而言,我們所指的業務是企業商業活動中的一個部分,有的甚至小到一個環節,例如“結算”這個環節。業務系統則是輔助這些商業活動的計算機在線系統,以信息化的形式管理和決策企業的商業活動(理論上沒有業務系統企業也能運轉,但信息化社會沒有業務系統會讓企業寸步難行)。

業務模塊:是以業務系統的建設者(領域專家、系統工程師等)的角度看待業務系統時,將龐大的業務系統,按照某個業務活動的邊界,進行劃分的某個單元。但是技術上,一般一個模塊還是粒度比較大的單元。一般而言,業務模塊囊括了系統關於該業務的所有內容,且和其他業務有明顯的界限,理論上,可以在脫離了其他業務模塊的情況下獨立運行。

業務邏輯:是隻用代碼實現的真實業務的規則映射。簡單說,一個業務中,存在什麼邏輯,可以通過在紙上畫出不同業務對象之間的聯繫和約束,並將這些聯繫和約束一條條列出來,形成一個列表,而這列表中的每一條,就是一條規則,這些規則的總和,就是這個業務的全部業務邏輯。既然是一條條的規則,那麼我們就可以在代碼層面對規則進行管理。對於前端開發者來說,最熟悉的規則管理,莫過於路由管理。

業務系統:本質上是基於人機交互的管理工具。不同角色在系統中管理的內容不同,但總體上,是進行基於業務上下游數據進行職權範圍內的業務管理活動。

從個人的感官上,業務系統的開發是最複雜的,爲什麼呢?我們前端開發對不同的前端應用的開發有非常明顯的感知差別。把前端應用歸納爲3類,分別是業務系統、通用應用、工具類應用。它們雖然都是前端應用,但是在開發工作中呈現出來的關注度卻非常不同。

表1 業務系統、通用應用和工具類應用的橫向對比

圖片

由於開發過程中關注的重點的不同,在開發時解決問題的思路也迥異。

圖片

圖2 業務系統、通用應用和工具類應用開發時解決問題的思路對比

這種解決問題的思路差異決定了我們在開發工作中方法論的差異。而業務系統涉及對象多,聯繫密,維度廣等特徵,導致在實際開發時,其複雜度比其他類應用高很多。(但在難度上往往並不高,業務系統常常不需要實現非常多需要基於特殊技術才能實現的功能。)

總結起來,業務系統之所以複雜的因素,我們認爲主要有以下一些方面:UI交互的不復雜與反覆實現的矛盾;特定業務邏輯對原有架構的挑戰;可持續維護(長期穩定性)與破壞性共存;模塊與模塊間數據、事件通知耦合;業務準確性要求高與需求多變之間的時間爭奪;前後端耦合

基於這些因素,我們需要謹慎對待開發的每一個功能。因爲對於業務而言,這一功能可能會在5年甚至10年後還要繼續使用,而如果功能不夠健壯,或者設計之初沒有充分考慮業務發展後所需要的擴展能力,很有可能在之後的迭代中給自己挖下巨大的坑。這是很多開發普適性強的大衆類產品的開發者們無法體會到的。

圖片

前端語境下DDD的價值主張

1)前端需要DDD嗎?

這個問題可以細化爲,前端需要與業務方領域專家進行溝通嗎?在設計系統或功能時,需要基於溝通結構的領域模型展開完成模塊的搭建嗎?我們需要在前端建模嗎?我們需要在前端分層嗎?等等。我無法給出標準答案,但我們思考:如今的前端開發,特別是類似和我們一樣的業務系統項目中,如果我們仍然是按照設計稿完成實現,是否能準確滿足需求?或者說,對於前端開發者而言,我們是否有必要掌握業務細節,通過一整套的方法論,在代碼中將這些細節管理起來?我們想從另一個角度爲前端開發者描繪他的工作場景:

圖片

圖3 前端開發人員的溝通域

上圖中,我們描述了前端開發者在整個業務開發過程中所處的境地。黑色圓圈表示在這過程中出現的角色,黑色實線表示與前端溝通時兩個角色的聯繫,虛線圈表示當問題發生時參與該問題討論的相關角色和內容。可以看到,前端開發者在與不同的角色進行溝通時,常常需要切換思維和角度,他所要面臨的問題,所處的境地,如果沒有一套方法論支撐,很難在繁雜的工作中不迷失。

2)前端可以DDD嗎?

包括Eric著作在內的DD作品,討論DDD多半是在後端環境下。作爲思想武器,DDD可以幫我們思考如何去設計業務系統架構,前端是否只需要基於後端接口輸出渲染界面就可以了呢?或許,前端根本無法按照DDD進行設計?並不是。

但隨着業務的深入,我們發現如今的業務系統(基於B/S架構),漸漸的靠近重客戶端的方向(類似C/S架構中的C端)。前端代碼中的業務邏輯逐漸越來越豐富,甚至很多邏輯只能由前端完成,後端無能爲力,或者前後端一起推進時成本更高。運行在C端的前端代碼,承載的業務邏輯與UI混雜在一起,導致組件或controller的代碼被撐的越來越大,越來越無法維護。我們曾指出過:基於vue寫的應用,當業務足夠複雜時,你根本無法區分vue組件中哪些是業務的,哪些是交互的。而這種情況持續下去,便是組件的不可維護,代碼的腐化。

作爲前端開發者我們需要思考,如何讓我們的代碼組織更健壯,更能體現業務的核心邏輯?基於溝通域的思考,我們認爲,前端所關注的主要內容包含:業務模型、數據服務、UI交互組件體系。

圖片

圖4 前端關注的主要內容

前端開發註定不可能只關注業務模型。在前端特別是web領域,除業務之外關注由後端吐出的數據和界面交互是一件必須的事。甚至有這樣一種情況,產品需求文檔中指出,“用戶點擊該按鈕時,需要彈出二次確認窗口,點擊確認後該簽署流轉爲已完成”。在這個描述中,用戶點擊按鈕彈出確認對話框,是否屬於業務邏輯呢?從傳統後端的角度,當然不屬於。但是,如果去掉這個交互邏輯,能否完整表達這一業務過程呢?這是一個值得思考的問題,前端如果要實施DDD,不可能照搬後端的實踐。前後端要解決的問題大不相同:

表2 前後端要解決的問題對比

圖片

這也就意味着,在前端語境下,我們關注的內容範疇比後端還要大。

圖片

圖5 前後端DDD範疇對比

這些思考反過來促使我們問自己:對於前端而言,DDD的價值是什麼?前端工程師需要重新審視自己的這一職位,從視覺交互的實現者陷阱中跳出來,正視自己作爲工程師的一面。解決業務項目中的關鍵問題,持續的與後端一道提供穩定可靠的業務服務,這纔是前端工程師的本職。而這些都是我們從DDD中發現的,對於前端而言,實施DDD有肉眼可見的好處:穩定的業務知識體系;可傳承的代碼體系;脫離UI的單元測試;跨端開發、多端共用的便捷性;明確的團隊分工;需求變更的快速響應;持續敏捷

這些好處對於需要持續迭代的項目團隊而言,非常有價值,特別是需要持續支撐業務團隊在下線完成更多以前無法完成的任務的價值,是業務系統最不可替代的。

圖片

前端領域建模

我們在騰訊投資系統中踐行前端建模已經超過兩年,這期間的收益與之前一股腦通過數據綁定的方式相比,可謂天差地別。在此前的開發中,我們雖然覺得麻煩,但是到底能把功能堆砌出來。但是隨着時間的推移,一些稍微老一點的邏輯就再也不敢動。所謂牽一髮而動全身,一點改動,重則可能帶來整個業務流程的癱瘓,輕則影響相關模塊的正確呈現。歸根結底,在於我們的系統是線性的(可以回頭看上文線性思維屬於哪種類型的應用),我們開發團隊的知識是不可複製的,一位開發者負責一個業務功能,只有他知道這業務裏面的邏輯到底有哪些;其他開發者即使重新研讀代碼,也很難梳理出準確全面的業務邏輯。如果需要重構,只能按照代碼邏輯慢慢複製。

兩年前,我們開始思考我們所遇到的大部分業務場景的共性,結合開發中遇到的痛點,我們急需有一種方式,可以對我們在應用界面的切換、數據的流轉中,有一種把握業務核心邏輯的能力。我們開始了建模探索。經過兩年的積累,我們現在才能將這些經驗總結出來。

1)思考單一業務的核心與邊界

我們在開始用代碼建模之前,我們需要和領域專家(也就是業務方,或者與我們對接的產品人員)開會討論某一業務的核心概念有哪些、有哪些可能的事件會發生、它怎麼和其他概念發生聯繫?我們在記事本上梳理出與要開發的業務發生聯繫的各個概念以及它們之間的連線,這樣我們基本掌握了有關這一業務的大部分知識。

當開始用代碼表達這些知識的時候,我們遇到的首要問題是:哪些是必須的,哪些是與該業務關聯但非必須的,我們該以何種形式表達這些概念?

圖片

圖6 前端領域建模的首要問題是劃清核心與邊界

使用OOP的範式進行建模是比較常見且直接的方式,通過建立形形色色的class來創建一個又一個的對象。關鍵的問題在於,這些對象的核心是什麼,邊界又在哪裏?這些都是我們必須在一開始考慮清楚的。

2)建模方法

DDD給我們提供了一些具體的建模方案,例如ENTITY、VALUE OBJECT、SERVICE、AGGREGATE、REPOSITORY、FACTORY等等。

圖片

圖7 DDD的建模方案

在建立對象模型時,我們根據對象在業務中所表達的意義,選擇其中對應的方案來進行建模。例如,我們爲一個投資對象進行建模,首先需要區分,投資對象的邊界在哪裏?比如一個投資中我們可能要走一些審批流程,會產生一些特殊數據,它們是否屬於該投資對象的核心?其次,我們需要了解一個投資它都由哪些資源構成,比如其中有一個字段叫做“投資主體”,投資主體即在投資中作爲合同上付錢的那一家公司出現,它自己本身也具有自己的一些屬性,那麼投資主體是否是投資這個對象的核心資源呢?我們應該處理投資主體?是用一個普通的JS對象來表達,還是創建一個子模型來管理?類似這樣的思考,在我們的建模過程中常常發生,而且有的時候,我們並不能一擊即中。有的時候需要在迭代更新中進行調整。

在長期摸索實踐中,我們形成了一套標準的建模步驟(如下圖)。在這一標準步驟中,我們抽象出了建模的基類,基於這些基類進行extends,根據每個對象的特徵,選擇性使用某些建模手段。具體如下:

圖片

圖8 前端領域建模步驟

在上圖中,完整呈現了我們在代碼層面實現領域模型的所有素材。建模的最小單位是Attribute即原子屬性。原子屬性描述一個字段(Field)或屬性(Property)的具體屬性,即元數據中的某一項。Meta是最小的模型,由Attribute組成,即關於單個字段的模型。Attribute和Meta不單獨存在,它們是顆粒度最小的素材,Attribute一般不可複用(或者說沒必要),Meta可限制性複用。

在上圖中,Model和本文講的領域模型概念稍有不同,Model是代碼層面可以表達完整業務對象的最小單位,它由Meta和其他資源組成。其他資源包括代碼層面的方法(概念上的Factory)、靜態屬性(概念上的Value Object)等。另外,和後端模型最大的不同,前端模型必須爲UI視圖層留足空間,我們爲Model提供了響應式能力,以便在與UI結合時,可以觀察到模型內變化,以觸發界面的更新。具體從代碼層面,我們用Mobx進行舉例。

圖片

圖9 兩個簡易模型

上圖中,我們創建了兩個模型Todo和TodoList,其中TodoList是對Todo的聚合,我們會在下文講。這是兩個再普通不過的class進行建模,現在的問題是:如果我們在前端使用這兩個模型,我們無法與我們已有的UI框架配合使用,比如在react中或vue中使用。有沒有一種方法,可以讓我們可以以最小的代價擴展模型的能力呢?我們使用mobx這個庫對模型進行改造

圖片

圖10 基於Mobx的簡易模型

使用mobx提供的裝飾器,我們以最少的改動增強模型。這一改動幾乎不會對原始模型的原有閱讀產生任何干擾,但卻使得該模型是可被觀察的。再配合mobx的工具,就可以與UI框架配合,就可以在應用中無縫對接。

圖片

圖11 基於Mobx的模型和UI對接

與後端框架不同,後端以Controller作爲入口,對模型和視圖進行消費。而前端主要的消費者是UI框架,視圖是入口。模型需要被實例化爲UI狀態後消費。因此,像Mobx這樣提供與UI框架對接的工具,是比較合理的設計方式。不過,這已經超出建模本身的話題,此處只是延伸。

我們回到建模步驟,在前端建模過程中,我們認爲最難的一點在於如何劃定一個模型的邊界。很多場景下,一個業務對象的某個邏輯,又依賴於另外一個業務對象的某個信息,跨模型的關係如何去描述?實踐中,我們通過嵌套模型來表達,也就是聚合Aggregate。通過Aggregate,我們把高於業務對象本身的邏輯進行梳理。一般而言,一個聚合根囊括了該業務的所有實體,也就是說,它界定了該業務在覈心實體上的邊界,該業務只在這些對象之間運轉。

但業務的運轉不僅只包含實體,也包含業務的流轉邏輯。概念上的領域事件,以及背後的領域服務——業務對象在該業務運轉過程中發生變化。對於前端而言,與後端對接數據也是很關鍵的一個點,我們基於Respository方案建立可響應式的接口數據管理方案,創建Service單例來在整個應用中響應事件,流轉業務對象的狀態,從而讓界面跟着業務的流轉而發生變化。

以上的建模成果被組織在一個Module(模塊)中,一個模塊不是一個單一文件,它是有特定目錄結構的一系列文件組成的功能單元。不過需要注意,這裏的Module和我們應用開發中通常講的Module概念上稍有不同,這裏的Module主要是對領域模型完成代碼實現後組成的組織單元。本質上,它還是建模的一部分,和我們通常意義上的業務模塊(包含UI等)有所區別。

圖片

在React項目中設計業務模塊

作爲優秀的視圖驅動庫,react實現了完美的從數據到視圖的映射。對比vue的優勢在於,它對數據本身的形式沒有要求。在vue中,你給定的數據包含getter(Object.defineProperty)或由Proxy創建抑或由特定的class實例化而來,會導致vue丟失部分響應式的能力。而react中沒有這層限制。因此,在我們實施DDD的過程中,更有親和力。

react雖然解決了數據到視圖的映射,卻沒有在反過來的視圖到數據的映射上提供方案。我們在react中,會在onClick等基於它內置的合成事件系統中執行回調函數來完成視圖到數據的處理,然而這種處理顯然是不利於建模的。因此,在react本身之外,我們創建了一套基於RxJS的單例服務來處理來自交互的事件與模型層的綁定。在具體的react組件中,我們只暴露給組件它渲染和交互需要用到的數據(狀態)和事件接口。

我們稱這種獨立於react組件本身之外的體系爲“無視圖交互模型”。該模型在撰寫時,站在視圖層的角度處理業務模型的實例化、修改等,處理來自視圖層的交互事件等等,但是在該模型中,沒有任何具體的UI實現。

如果你還有印象,你可能還記得我們在前文問到“用戶點擊按鈕彈出確認對話框,是否屬於業務邏輯呢?”。在“無視圖交互模型”的設計下,我們可以將“用戶點擊按鈕彈出對話框”這一交互轉化爲模型的一個部分,在該模型中,它提供了用戶點擊動作的接口,而該接口處理時會流轉模型內持有的其他具體業務模型,進而達到需求文檔中所描述的這一要求。但在具體UI中,這個按鈕長什麼樣子、在哪個位置、彈出框樣式,都需要在UI層(組件中)具體去實現。而在實現時,開發者不需要考慮該按鈕點擊事件的具體效果,只需要調用模型接口即可

截止到這一步,你會發現,在我們的業務模塊中,還沒有任何有關UI的具體實現,但是,我們幾乎已經把需求文檔中有關業務的實體對象、某些與業務流轉相關的交互,都用代碼表達出來了。

圖片

圖12 項目中實現業務模塊的具體流程

從需求分析,基於統一語言建立領域模型,到具體代碼去實現領域模型,再到建立無視圖交互模型。到這裏,我們有關業務的建模基本完成,但我們還沒有任何的界面效果。對於以前我們拿起react就開擼的開發方式而言,簡直不可思議,怎麼界面都還沒有就已經一大堆代碼了?

我們的系統包含PC端、APP端和微信內嵌H5,APP端和H5端交互基本相同,稍有些細節差異,但PC和移動端的差異大的就不止一點點。然而,對於某一業務模塊而言,兩端的交互雖然不同,卻在業務邏輯上(包含基於業務邏輯的交互邏輯)是必須一致的,否則業務本身就會出現問題

上圖中虛線的右側即我們多端共用的部分。業務模型在各端複用並非完全爲了代碼重用,更多的是爲了保證業務的一致性。而虛線的左側,則是我們常見的react組件體系的編寫。對於不同端,我們的組件很少有能複用的,基本上各自一套,但卻能夠保證在業務邏輯上沒有出入。一旦業務(數據和交互)被業務模型規範下來,對於不同端的視圖層,就是換皮操作(有點誇張)。

這一體系給了我們很多想象的空間,雖然我們團隊目前並沒有再深入,但是從個人的角度而言,我們可以在這一模式基礎上,做到更容易的業務單元測試、基於後端輸出的統一UI DSL、業務模塊服務化、小程序等等。

圖片

基於DDD的前端應用架構分層

分層也是DDD的重要思想。在Eric原書中,他對系統分層做了梳理,同時,在行業中,後續繼承者們對該分層進行了擴展,主要包括:依賴倒置四層架構、六邊形架構、Clean Architecture等。四層架構典型示意描述如圖:

圖片

圖13 遵循DDD的系統分層架構示意圖

前端與後端不同的是,前端是胖UI瘦數據,前端不需要去考慮數據的存儲讀寫所帶來的一系列問題。但是,在整體的分層劃分上,前後端具有一致性。

圖片

圖14 遵循DDD的前端應用分層示意圖

我們以前的前端應用開發也有分層的思想。但是分層實踐很微弱,大部分情況下還是把所有邏輯雜糅到react組件和狀態管理中。這也是我們以前的代碼在開發完一段時間後可維護性就極速變弱的原因。而按照DDD的設計,我們將代碼分層組織和管理。其中的核心層是領域層,這一層決定了整個應用的大部分代碼邏輯,雖然在控制層和UI層也有一些邏輯,但是它們多是處理交互的,與具體的業務無關。它完整的描述了這一系統所反應的業務面貌。也正因如此,在多端複用(例如PC和APP兩端寫同一個業務)時,只需要重寫UI層,而不需要再寫其他層。

圖片

結語

複雜的業務系統無論前端還是後端,都面臨着巨大的挑戰。除了系統本身的功能之外,最大的風險在於業務邏輯需要被準確實現的同時,給研發團隊的實現時間卻很緊張。緊隨而來的系統的代碼在長時間迭代中,越來越難以維護。DDD從溝通和研發兩個角度爲我們提供方法論,是面對複雜業務時的思想工具。它啓發了我們,讓我們在工作方式上不再莽撞的馬上開始寫代碼,而是先與業務方深入溝通、掌握業務的知識網絡,基於統一語言進行領域建模,之後再來考慮如何在react中結合,開發整個應用。

在前端語境下,由於前端關注的內容的異質性,我們不可能直接照搬後端的DDD實踐,不得不探索前端DDD的特殊途徑。基於DDD的設計,我們的架構剖離出不同的分層,在領域層和控制層完完全全描述了業務需求。因此,可以不考慮UI層就可以進行業務編程,並實施有效的業務單元測試。而對於跨端複用而言,就是換UI殼的處理,內在的業務邏輯代碼可直接複用。

以上是我們在前端DDD話題下的探索。當然,DDD不是唯一的選擇,它給了我們啓發,也允許我們在它的基礎上持續顛覆。歡迎在評論區分享交流。

| 由淺入深讀透vue源碼:diff算法

| 優雅應對故障:QQ音樂怎麼做高可用架構體系?

| QQ瀏覽器是如何提升搜索相關性的?

| 從Linux零拷貝深入瞭解Linux-I/O

技術盲盒:前端後端AI與算法運維工程師文化

公衆號後臺回覆“DDD”,獲得更多DDD閱讀材料。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章