iOS 架構模式--解密 MVC,MVP,MVVM以及VIPER架構


在 iOS 中使用 MVC 架構感受很奇怪? 遷移到MVVM架構又懷有疑慮?據說過 VIPER 又不肯定是否真的值得切換?git

相信你會找到以上問題的答案,若是沒找到請在評論中指出。github

你將要整理出你在 iOS 環境下全部關於架構模式的知識。咱們將帶領你們簡要的回顧一些流行的架構,而且在理論和實踐上對它們進行比較,經過一些小的例子深化你的認知。若是對文中提到的一些關鍵詞有興趣,能夠點擊鏈接去查看更詳細的內容。objective-c

掌控設計模式可能會令人上癮,因此要小心,你可能會對一些問題清晰明瞭,再也不像閱讀以前那樣迷惑,好比下面這些問題:編程

誰應該來負責網絡請求?Model 仍是 Controller ?設計模式

應該怎樣向一個新的頁面的 ViewModel 傳入一個 Model ?服務器

誰來建立一個 VIPER 模塊,是 Router 仍是 Presenter ?網絡

10.png

爲何要關注架構設計?

由於假如你不關心架構,那麼總有一天,須要在同一個龐大的類中調試若干複雜的事情,你會發如今這樣的條件下,根本不可能在這個類中快速的找到以及有效的修改任何bug.固然,把這樣的一個類想象爲一個總體是困難的,所以,有可能一些重要的細節總會在這個過程當中會被忽略。若是如今的你正是處於這樣一個開發環境中,頗有可能具體的狀況就像下面這樣:

  • 這個類是一個UIViewController的子類

  • 數據直接在UIViewController中存儲

  • UIView類幾乎不作任何事情

  • Model 僅僅是一個數據結構

  • 單元測試覆蓋不了任何用例

以上這些狀況仍舊會出現,即便是你遵循了Apple的指導原則而且實現了其 MVC(模式,因此,大可沒必要驚慌。Apple所提出的 MVC 模式存在一些問題,咱們以後會詳述。

在此,咱們能夠定義一個好的架構應該具有的特色:

  1. 任務均衡分攤給具備清晰角色的實體

  2. 可測試性一般都來自與上一條(對於一個合適的架構是很是容易)

  3. 易用性和低成本維護

爲何採用分佈式?

採用分佈式能夠在咱們要弄清楚一些事情的原理時保持一個均衡的負載。若是你認爲你的開發工做越多,你的大腦越能習慣複雜的思惟,其實這是對的。可是,不能忽略的一個事實是,這種思惟能力並非線性增加的,並且也並不能很快的到達峯值。因此,可以打敗這種複雜性的最簡單的方法就是在遵循 單一功能原則 的前提下,將功能劃分給不一樣的實體。

爲何須要易測性?

其實這條要求對於哪些習慣了單元測試的人並非一個問題,由於在添加了新的特性或者要增長一些類的複雜性以後一般會失效。這就意味着,測試能夠避免開發者在運行時才發現問題----當應用到達用戶的設備,每一次維護都須要浪費長達至少[一週](http://appreviewtimes.com)的時間才能再次分發給用戶。

爲何須要易用性?

這個問題沒有固定的答案,但值得一提的是,最好的代碼是那些從未寫過的代碼。所以,代碼寫的越少,Bug就越少。這意味着但願寫更少的代碼不該該被單純的解釋爲開發者的懶惰,並且也不該該由於偏心更聰明的解決方案而忽視了它的維護開銷。

MV(X)系列概要

當今咱們已經有很架構設計模式方面的選擇:

前三種設計模式都把一個應用中的實體分爲如下三類:

  • Models--負責主要的數據或者操做數據的數據訪問層,能夠想象 Perspn 和 PersonDataProvider 類。

  • Views--負責展現層(GUI),對於iOS環境能夠聯想一下以 UI 開頭的全部類。

  • Controller/Presenter/ViewModel--負責協調 Model 和 View,一般根據用戶在View上的動做在Model上做出對應的更改,同時將更改的信息返回到View上。

將實體進行劃分給咱們帶來了如下好處:

  • 更好的理解它們之間的關係

  • 複用(尤爲是對於View和Model)

  • 獨立的測試

讓咱們開始瞭解MV(X)系列,以後再返回到VIPER模式。

MVC的過去

在咱們探討Apple的MVC模式以前,咱們來看下傳統的MVC模式

1-E9A5fOrSr0yVmc7Kly5C6A.png

傳統的MVC

在這裏,View並無任何界限,僅僅是簡單的在Controller中呈現出Model的變化。想象一下,就像網頁同樣,在點擊了跳轉到某個其餘頁面的鏈接以後就會徹底的從新加載頁面。儘管在iOS平臺上實現這這種MVC模式是沒有任何難度的,可是它並不會爲咱們解決架構問題帶來任何裨益。由於它自己也是,三個實體間相互都有通訊,並且是緊密耦合的。這很顯然會大大下降了三者的複用性,而這正是咱們不肯意看到的。鑑於此咱們再也不給出例子。

「傳統的MVC架構不適用於當下的iOS開發」

蘋果推薦的MVC--願景

02.png

Cocoa MVC

因爲Controller是一個介於View 和 Model之間的協調器,因此View和Model之間沒有任何直接的聯繫。Controller是一個最小可重用單元,這對咱們來講是一個好消息,由於咱們總要找一個地方來寫邏輯複雜度較高的代碼,而這些代碼又不適合放在Model中。

理論上來說,這種模式看起來很是直觀,但你有沒有感到哪裏有一絲詭異?你甚至據說過,有人將MVC的縮寫展開成(Massive View Controller),更有甚者,爲View controller減負也成爲iOS開發者面臨的一個重要話題。若是蘋果繼承而且對MVC模式有一些進展,全部這些爲何還會發生?

蘋果推薦的MVC--事實

1-PkWjDU0jqGJOB972cMsrnA.png

Realistic Cocoa MVC

Cocoa的MVC模式驅令人們寫出臃腫的視圖控制器,由於它們常常被混雜到View的生命週期中,所以很難說View和ViewController是分離的。儘管仍能夠將業務邏輯和數據轉換到Model,可是大多數狀況下當須要爲View減負的時候咱們卻無能爲力了,View的最大的任務就是向Controller傳遞用戶動做事件。ViewController最終會承擔一切代理和數據源的職責,還負責一些分發和取消網絡請求以及一些其餘的任務,所以它的名字的由來...你懂的。

你可能會看見過不少次這樣的代碼:

1
2
var  userCell = tableView.dequeueReusableCellWithIdentifier( "identifier" ) as UserCell
userCell.configureWithUser(user)

這個cell,正是由View直接來調用Model,因此事實上MVC的原則已經違背了,可是這種狀況是一直髮生的甚至於人們不以爲這裏有哪些不對。若是嚴格遵照MVC的話,你會把對cell的設置放在 Controller 中,不向View傳遞一個Model對象,這樣就會大大增長Controller的體積。

「Cocoa 的MVC被寫成Massive View Controller 是不無道理的。」

直到進行單元測試的時候纔會發現問題愈來愈明顯。由於你的ViewController和View是緊密耦合的,對它們進行測試就顯得很艱難--你得有足夠的創造性來模擬View和它們的生命週期,在以這樣的方式來寫View Controller的同時,業務邏輯的代碼也逐漸被分散到View的佈局代碼中去。

咱們看下一些簡單的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit
 
struct Person {  // Model
     let firstName: String
     let lastName: String
}
 
class GreetingViewController : UIViewController {  // View + Controller
     var  person: Person!
     let showGreetingButton = UIButton()
     let greetingLabel = UILabel()
     
     override func viewDidLoad() {
         super .viewDidLoad()
         self.showGreetingButton.addTarget(self, action:  "didTapButton:" , forControlEvents: .TouchUpInside)
     }
     
     func didTapButton(button: UIButton) {
         let greeting =  "Hello"  " "  + self.person.firstName +  " "  + self.person.lastName
         self.greetingLabel.text = greeting
         
     }
     // layout code goes here
}
// Assembling of MVC
let model = Person(firstName:  "David" , lastName:  "Blaine" )
let view = GreetingViewController()
view.person = model;

「MVC能夠在一個正在顯示的ViewController中實現」

這段代碼看起來可測試性並不強,咱們能夠把和greeting相關的都放到GreetingModel中而後分開測試,可是這樣咱們就沒法經過直接調用在GreetingViewController中的UIView的方法(viewDidLoad和didTapButton方法)來測試頁面的展現邏輯了,由於一旦調用則會使整個頁面都變化,這對單元測試來說並非什麼好消息。

事實上,在單獨一個模擬器中(好比iPhone 4S)加載並測試UIView並不能保證在其餘設備中也能正常工做,所以我建議在單元測試的Target的設置下移除"Host Application"項,而且不要在模擬器中測試你的應用。

「View和Controller的接口並不適合單元測試。」

以上所述,彷佛Cocoa MVC 看起來是一個至關差的架構方案。咱們來從新評估一下文章開頭咱們提出的MVC一系列的特徵:

  • 任務均攤--View和Model確實是分開的,可是View和Controller倒是緊密耦合的

  • 可測試性--因爲糟糕的分散性,只能對Model進行測試

  • 易用性--與其餘幾種模式相比最小的代碼量。熟悉的人不少,於是即便對於經驗不那麼豐富的開發者來說維護起來也較爲容易。

若是你不想在架構選擇上投入更多精力,那麼Cocoa MVC無疑是最好的方案,並且你會發現一些其餘維護成本較高的模式對於你所開發的小的應用是一個致命的打擊。

「就開發速度而言,Cocoa MVC是最好的架構選擇方案。」

MVP 實現了Cocoa的MVC的願景

021.png

Passive View variant of MVP

這看起來不正是蘋果所提出的MVC方案嗎?確實是的,這種模式的名字叫作MVC,可是,這就是說蘋果的MVC實際上就是MVP了?不,並非這樣的。若是你仔細回憶一下,View是和Controller緊密耦合的,可是MVP的協調器Presenter並無對ViewController的生命週期作任何改變,所以View能夠很容易的被模擬出來。在Presenter中根本沒有和佈局有關的代碼,可是它卻負責更新View的數據和狀態。

QQ截圖20160107154558.png

「假如告訴你UIViewController就是View呢?」

就MVP而言,UIViewController的子類實際上就是Views並非Presenters。這點區別使得這種模式的可測試性獲得了極大的提升,付出的代價是開發速度的一些下降,由於必需要作一些手動的數據和事件綁定,從下例中能夠看出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import UIKit
 
struct Person {  // Model
     let firstName: String
     let lastName: String
}
 
protocol GreetingView: class {
     func setGreeting(greeting: String)
}
 
protocol GreetingViewPresenter {
     init(view: GreetingView, person: Person)
     func showGreeting()
}
 
class GreetingPresenter : GreetingViewPresenter {
     unowned let view: GreetingView
     let person: Person
     required init(view: GreetingView, person: Person) {
         self.view = view
         self.person = person
     }
     func showGreeting() {
         let greeting =  "Hello"  " "  + self.person.firstName +  " "  + self.person.lastName
         self.view.setGreeting(greeting)
     }
}
 
class GreetingViewController : UIViewController, GreetingView {
     var  presenter: GreetingViewPresenter!
     let showGreetingButton = UIButton()
     let greetingLabel = UILabel()
     
     override func viewDidLoad() {
         super .viewDidLoad()
         self.showGreetingButton.addTarget(self, action:  "didTapButton:" , forControlEvents: .TouchUpInside)
     }
     
     func didTapButton(button: UIButton) {
         self.presenter.showGreeting()
     }
     
     func setGreeting(greeting: String) {
         self.greetingLabel.text = greeting
     }
     
     // layout code goes here
}
// Assembling of MVP
let model = Person(firstName:  "David" , lastName:  "Blaine" )
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

關於整合問題的重要說明

MVP是第一個如何協調整合三個實際上分離的層次的架構模式,既然咱們不但願View涉及到Model,那麼在顯示的View Controller(其實就是View)中處理這種協調的邏輯就是不正確的,所以咱們須要在其餘地方來作這些事情。例如,咱們能夠作基於整個App範圍內的路由服務,由它來負責執行協調任務,以及View到View的展現。這個出現而且必須處理的問題不只僅是在MVP模式中,同時也存在於如下集中方案中。

咱們來看下MVP模式下的三個特性的分析:

  • 任務均攤--咱們將最主要的任務劃分到Presenter和Model,而View的功能較少(雖然上述例子中Model的任務也並很少)。

  • 可測試性--很是好,因爲一個功能簡單的View層,因此測試大多數業務邏輯也變得簡單

  • 易用性--在咱們上邊不切實際的簡單的例子中,代碼量是MVC模式的2倍,但同時MVP的概念卻很是清晰

「iOS 中的MVP意味着可測試性強、代碼量大。」

MVP--綁定和信號

還有一些其餘形態的MVP--監控控制器的MVP。

這個變體包含了View和Model之間的直接綁定,可是Presenter仍然來管理來自View的動做事件,同時也能勝任對View的更新。

022.png

Supervising Presenter variant of the MVP

可是咱們以前就瞭解到,模糊的職責劃分是很是糟糕的,更況且將View和Model緊密的聯繫起來。這和Cocoa的桌面開發的原理有些類似。

和傳統的MVC同樣,寫這樣的例子沒有什麼價值,故再也不給出。

MVVM--最新且是最偉大的MV(X)系列的一員

MVVM架構是MV(X)系列最新的一員,所以讓咱們但願它已經考慮到MV(X)系列中以前已經出現的問題。

從理論層面來說MVVM看起來不錯,咱們已經很是熟悉View和Model,以及Meditor,在MVVM中它是View Model。

023.png

MVVM

它和MVP模式看起來很是像:

  • MVVM將ViewController視做View

  • 在View和Model之間沒有緊密的聯繫

此外,它還有像監管版本的MVP那樣的綁定功能,但這個綁定不是在View和Model之間而是在View和ViewModel之間。

那麼問題來了,在iOS中ViewModel實際上表明什麼?它基本上就是UIKit下的每一個控件以及控件的狀態。ViewModel調用會改變Model同時會將Model的改變動新到自身而且由於咱們綁定了View和ViewModel,第一步就是相應的更新狀態。

綁定

我在MVP部分已經提到這點了,可是該部分咱們仍會繼續討論。

若是咱們本身不想本身實現,那麼咱們有兩種選擇:

事實上,尤爲是最近,你聽到MVVM就會想到ReactiveCoca,反之亦然。儘管經過簡單的綁定來使用MVVM是可實現的,可是ReactiveCocoa卻能更好的發揮MVVM的特色。

可是關於這個框架有一個不得不說的事實:強大的能力來自於巨大的責任。當你開始使用Reactive的時候有很大的可能就會把事情搞砸。換句話來講就是,若是發現了一些錯誤,調試出這個bug可能會花費大量的時間,看下函數調用棧:

1-WGIs3XQL1MtKiyApr-m9bg.png

Reactive Debugging

在咱們簡單的例子中,FRF框架和KVO被過渡禁用,取而代之地咱們直接去調用showGreeting方法更新ViewModel,以及經過greetingDidChange 回調函數使用屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import UIKit
 
struct Person {  // Model
     let firstName: String
     let lastName: String
}
 
protocol GreetingViewModelProtocol: class {
     var  greeting: String? { get }
     var  greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set }  // function to call when greeting did change
     init(person: Person)
     func showGreeting()
}
 
class GreetingViewModel : GreetingViewModelProtocol {
     let person: Person
     var  greeting: String? {
         didSet {
             self.greetingDidChange?(self)
         }
     }
     var  greetingDidChange: ((GreetingViewModelProtocol) -> ())?
     required init(person: Person) {
         self.person = person
     }
     func showGreeting() {
         self.greeting =  "Hello"  " "  + self.person.firstName +  " "  + self.person.lastName
     }
}
 
class GreetingViewController : UIViewController {
     var  viewModel: GreetingViewModelProtocol! {
         didSet {
             self.viewModel.greetingDidChange = { [unowned self] viewModel  in
                 self.greetingLabel.text = viewModel.greeting
             }
         }
     }
     let showGreetingButton = UIButton()
     let greetingLabel = UILabel()
     
     override func viewDidLoad() {
         super .viewDidLoad()
         self.showGreetingButton.addTarget(self.viewModel, action:  "showGreeting" , forControlEvents: .TouchUpInside)
     }
     // layout code goes here
}
// Assembling of MVVM
let model = Person(firstName:  "David" , lastName:  "Blaine" )
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

讓咱們再來看看關於三個特性的評估:

  • 任務均攤 -- 在例子中並非很清晰,可是事實上,MVVM的View要比MVP中的View承擔的責任多。由於前者經過ViewModel的設置綁定來更新狀態,然後者只監聽Presenter的事件但並不會對本身有什麼更新。

  • 可測試性 -- ViewModel不知道關於View的任何事情,這容許咱們能夠輕易的測試ViewModel。同時View也能夠被測試,可是因爲屬於UIKit的範疇,對他們的測試一般會被忽略。

  • 易用性 -- 在咱們例子中的代碼量和MVP的差很少,可是在實際開發中,咱們必須把View中的事件指向Presenter而且手動的來更新View,若是使用綁定的話,MVVM代碼量將會小的多。

「MVVM很誘人,由於它集合了上述方法的優勢,而且因爲在View層的綁定,它並不須要其餘附加的代碼來更新View,儘管這樣,可測試性依然很強。」

VIPER--把LEGO建築經驗遷移到iOS app的設計

VIPER是咱們最後要介紹的,因爲不是來自於MV(X)系列,它具有必定的趣味性。

迄今爲止,劃分責任的粒度是很好的選擇。VIPER在責任劃分層面進行了迭代,VIPER分爲五個層次:

024.png

VIPER

  • 交互器 -- 包括關於數據和網絡請求的業務邏輯,例如建立一個實體(數據),或者從服務器中獲取一些數據。爲了實現這些功能,須要使用服務、管理器,可是他們並不被認爲是VIPER架構內的模塊,而是外部依賴。

  • 展現器 -- 包含UI層面的業務邏輯以及在交互器層面的方法調用。

  • 實體 -- 普通的數據對象,不屬於數據訪問層次,由於數據訪問屬於交互器的職責。

  • 路由器 -- 用來鏈接VIPER的各個模塊。

基本上,VIPER模塊能夠是一個屏幕或者用戶使用應用的整個過程--想一想認證過程,能夠由一屏完成或者須要幾步才能完成,你的模塊指望是多大的,這取決於你。

當咱們把VIPER和MV(X)系列做比較時,咱們會在任務均攤性方面發現一些不一樣:

  • Model 邏輯經過把實體做爲最小的數據結構轉換到交互器中。

  • Controller/Presenter/ViewModel的UI展現方面的職責移到了Presenter中,可是並無數據轉換相關的操做。

  • VIPER是第一個經過路由器實現明確的地址導航模式。

「找到一個適合的方法來實現路由對於iOS應用是一個挑戰,MV(X)系列避開了這個問題。」

例子中並不包含路由和模塊之間的交互,因此和MV(X)系列部分架構同樣再也不給出例子。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import UIKit
 
struct Person {  // Entity (usually more complex e.g. NSManagedObject)
     let firstName: String
     let lastName: String
}
 
struct GreetingData {  // Transport data structure (not Entity)
     let greeting: String
     let subject: String
}
 
protocol GreetingProvider {
     func provideGreetingData()
}
 
protocol GreetingOutput: class {
     func receiveGreetingData(greetingData: GreetingData)
}
 
class GreetingInteractor : GreetingProvider {
     weak  var  output: GreetingOutput!
     
     func provideGreetingData() {
         let person = Person(firstName:  "David" , lastName:  "Blaine" // usually comes from data access layer
         let subject = person.firstName +  " "  + person.lastName
         let greeting = GreetingData(greeting:  "Hello" , subject: subject)
         self.output.receiveGreetingData(greeting)
     }
}
 
protocol GreetingViewEventHandler {
     func didTapShowGreetingButton()
}
 
protocol GreetingView: class {
     func setGreeting(greeting: String)
}
 
class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
     weak  var  view: GreetingView!
     var  greetingProvider: GreetingProvider!
     
     func didTapShowGreetingButton() {
         self.greetingProvider.provideGreetingData()
     }
     
     func receiveGreetingData(greetingData: GreetingData) {
         let greeting = greetingData.greeting +  " "  + greetingData.subject
         self.view.setGreeting(greeting)
     }
}
 
class GreetingViewController : UIViewController, GreetingView {
     var  eventHandler: GreetingViewEventHandler!
     let showGreetingButton = UIButton()
     let greetingLabel = UILabel()
     override func viewDidLoad() {
         super .viewDidLoad()
         self.showGreetingButton.addTarget(self, action:  "didTapButton:" , forControlEvents: .TouchUpInside)
     }
     
     func didTapButton(button: UIButton) {
         self.eventHandler.didTapShowGreetingButton()
     }
     
     func setGreeting(greeting: String) {
         self.greetingLabel.text = greeting
     }
     
     // layout code goes here
}
// Assembling of VIPER module, without Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter

讓咱們再來評估一下特性:

  • 任務均攤 -- 毫無疑問,VIPER是任務劃分中的佼佼者。

  • 可測試性 -- 不出意外地,更好的分佈性就有更好的可測試性。

  • 易用性 -- 最後你可能已經猜到了維護成本方面的問題。你必須爲很小功能的類寫出大量的接口。

什麼是LEGO

當使用VIPER時,你的感受就像是用樂高積木來搭建一個城堡,這也是一個代表當前存在一些問題的信號。可能如今就應用VIPER架構還爲時過早,考慮一些更爲簡單的模式可能會更好。一些人會忽略這些問題,大材小用。假定他們篤信VIPER架構會在將來給他們的應用帶來一些好處,雖然如今維護起來確實是有些不合理。若是你也持這樣的觀點,我爲你推薦 Generamba 這個用來搭建VIPER架構的工具。雖然我我的感受,使用起來就像加農炮的自動瞄準系統,而不是簡單的像投石器那樣的簡單的拋擲。

總結

咱們瞭解了集中架構模式,但願你已經找到了究竟是什麼在困擾你。毫無疑問經過閱讀本篇文章,你已經瞭解到其實並無徹底的銀彈。因此選擇架構是一個根據實際狀況具體分析利弊的過程。

所以,在同一個應用中包含着多種架構。好比,你開始的時候使用MVC,而後忽然意識到一個頁面在MVC模式下的變得愈來愈難以維護,而後就切換到MVVM架構,可是僅僅針對這一個頁面。並無必要對哪些MVC模式下運轉良好的頁面進行重構,由於兩者是能夠並存的。


  • 本文僅用於學習和交流目的,轉載請註明文章譯者、做者、出處以及本文連接。

  • 感謝博文視點對本期翻譯活動的支持

轉載:http://www.cocoachina.com/ios/20160108/14916.html

 

MVCMVVMVIPERMVP,以及最新的ReactiveCocoa都作過實戰嘗試,還有其餘變種,諸如猿題庫iOS客戶端架構設計,也作過一些學習研究。這些技術概念若是不熟悉,建議每一個連接都點開好好研讀下,不要對你的大腦太溫柔。

最後推薦一些其餘很是值得一讀的文章:

唐巧-被誤解的 MVC 和被神化的 MVVM

Casa Taloyum iOS架構系列文章

objc.io架構系列文章

相關文章
相關標籤/搜索