前言
MVC是軟件工程中的一種軟件架構(gòu)模式,它把軟件系統(tǒng)分為三個基本的部分:模型Model、視圖View以及控制器Controller。這種模式的目的是為了實現(xiàn)一種動態(tài)的程序設(shè)計,簡化后續(xù)對軟件系統(tǒng)的修改和擴展植袍,并使得程序的某一部分的復用成為可能。三個部分按照其各自的職責劃分:
- 數(shù)據(jù)Model: 負責封裝數(shù)據(jù)籽懦、存儲和處理數(shù)據(jù)運算等工作
- 視圖View: 負責數(shù)據(jù)展示于个、監(jiān)聽用戶觸摸等工作
- 控制器Controller: 負責業(yè)務邏輯、事件響應暮顺、數(shù)據(jù)加工等工作
在傳統(tǒng)的MVC結(jié)構(gòu)中厅篓,數(shù)據(jù)層在發(fā)生改變之后會通知視圖層進行對應的處理,視圖層能直接訪問數(shù)據(jù)層拖云。但在iOS中贷笛,M和V之間禁止通信,必須由C控制器層來協(xié)調(diào)M和V之間的變化宙项。如下圖所示乏苦,C對M和V的訪問是不受限的,但M和V不允許直接接觸控制器層,而是由多種Callbacks方式來通知控制器
如何分層
MVC是iOS開發(fā)者最常用的框架結(jié)構(gòu)汇荐,即便是越來越熱門的MVVM或是其他框架結(jié)構(gòu)洞就,幾乎都是基于MVC模式下對各個組塊的職責進一步的細化分層罷了。那么掀淘,在開發(fā)的時候如何制定三部分的層次劃分呢旬蟋?基本上所有的應用無非都是在做這些事情:
雖然上圖不能囊括所有的應用,但是基本而言大眾開發(fā)者干的活就是這些了革娄。簡單的根據(jù)這些事情來分工倾贰,我們可以很快的得出MVC和工作內(nèi)容的對應關(guān)系:
controller <--> 網(wǎng)絡(luò)請求、事件響應
view <--> 數(shù)據(jù)展示拦惋、動效展示
model <--> 數(shù)據(jù)處理
通過對我們開發(fā)工作的分工匆浙,MVC架構(gòu)的代碼分層幾乎已經(jīng)可以確定了,下面筆者會對這三部分進行更詳細的講述
模型Model應該放什么代碼
在以往開發(fā)中厕妖,對于模型層筆者存在這么幾個疑惑:
- 模型Model只是一個純粹的數(shù)據(jù)結(jié)構(gòu)
- 負責數(shù)據(jù)I/O操作的操作屬于C還是M
第一個問題筆者認為原因在于認知錯誤首尼,過往開發(fā)的過程中,筆者曾經(jīng)一度認為數(shù)據(jù)和模型之間的轉(zhuǎn)換屬于業(yè)務操作言秸,將這些處理放在控制器Controller層中執(zhí)行:
- (void)analyseRequestJSON: (NSDictionary *)JSON {
NSArray *modelsData = JSON[@"result"];
NSMutableArray *models = @[].mutableCopy;
for (NSDictionary *data in modelsData) {
LXDRecord *record = [[LXDRecord alloc] init];
record.content = data[@"content"];
record.recorder = data[@"recorder"];
record.createDate = data[@"createDate"];
record.updateDate = data[@"updateDate"];
[models addObject: record];
}
}
這是典型的認知錯誤引發(fā)的代碼錯誤放置的錯誤软能,對于這種情況,直接常見的做法是在Model中直接加入全能構(gòu)造器Designed Initializer來將這部分代碼轉(zhuǎn)移至Model中:
@interface LXDRecord: NSObject
//properties
- (instancetype)initWithCreateDate: (NSString *)createDate
updateDate: (NSString *)updateDate
content: (NSString *)content
recorder: (NSString *)recorder;
@end
//Controller
- (void)analyseRequestJSON: (NSDictionary *)JSON {
NSArray *modelsData = JSON[@"result"];
NSMutableArray *models = @[].mutableCopy;
for (NSDictionary *data in modelsData) {
LXDRecord *record = [[LXDRecord alloc] initWithCreateDate: data[@"createDate"]
updateDate: data[@"updateDate"]
content: data[@"content"]
recorder: data[@"recorder"]];
[models addObject: record];
}
}
在轉(zhuǎn)移數(shù)據(jù)->模型這一邏輯處理之后數(shù)據(jù)層相對而言就充實的多举畸,但這還不夠查排。數(shù)據(jù)在完成抽象轉(zhuǎn)換的工作之后,通常要展示到視圖層面上抄沮。但往往模型還需要進行額外的加工才能展示雹嗦,比如筆者曾經(jīng)項目中的一個需求:用戶在繳納寬帶費用后將寬帶辦理期間顯示出來,這需求建立在服務器只有辦理時間和辦理時長兩個字段合是。在MVC的結(jié)構(gòu)下,將這部分代碼放在C層會導致代碼過多過于雜亂的后果锭环,因此筆者將其放在Model中:
@interface YQBNetworkRecord: YQBModel
@property (nonatomic, copy, readonly) NSString *dealDate; //辦理時間
@property (nonatomic, copy, readonly) NSString *effectTime; //辦理時長
- (NSString *)timeOfNetworkDuration;
@end
@implementation YQBNetworkRecord
- (NSString *)timeOfNetworkDuration {
NSTimeInterval effectInterval = [_effectTime stringToInterval];
return [_dealDate stringByAppendString: [_dealDate dateAfterInterval: effectInterval]];
}
@end
這一做法將一部分C 層次的邏輯放到了M中聪全,由于這一部分的邏輯屬于弱業(yè)務,屬于幾乎不會改動的業(yè)務邏輯辅辩,因此并不會影響MVC的整體結(jié)構(gòu)难礼。但同樣也存在著風險:
- 代碼依賴于Model的差異化,復用性低
- 代碼量取決于Model的數(shù)量玫锋,容易導致胖Model的情況
雖然存在著這些不足蛾茉,但是如果是在MVC模式下對控制器進行減負的情況下,這種做法簡單有效撩鹿。另外,使用category將這些邏輯代碼分離出去可以使得復用性變得不那么的糟键思。當然上面的數(shù)據(jù)->模型過程中也存在著因為數(shù)據(jù)類型變化而導致構(gòu)造器失效的問題,這時候參考YYModel的做法可以減少或者解決這些問題的發(fā)生
I/O操作
首先是I/O操作的業(yè)務歸屬問題吼鳞。假設(shè)我們的M采用了序列歸檔的持久化方案,那么M層應該實現(xiàn)NSCoding協(xié)議:
@interface LXDRecord: NSObject<NSCoding>
@end
@implementation LXDRecord
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject: _content forKey: @"content"];
[aCoder encodeObject: _recorder forKey: @"recorder"];
[aCoder encodeObject: _createDate forKey: @"createDate"];
[aCoder encodeObject: _updateDate forKey: @"updateDate"];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
_content = [aDecoder decodeObjectForKey: @"content"];
_recorder = [aDecoder decodeObjectForKey: @"recorder"];
_createDate = [aDecoder decodeObjectForKey: @"createDate"];
_updateDate = [aDecoder decodeObjectForKey: @"updateDate"];
}
return self;
}
@end
從序列化歸檔的實現(xiàn)中我們可以看到這些核心代碼是放在模型層中實現(xiàn)的赔桌,雖然還要借助NSKeyedArchiver來完成存取操作供炎,但是在這些實現(xiàn)上將I/O操作歸屬為M層的業(yè)務也算的上符合情理。另一方面疾党,合理的將這一業(yè)務放到模型層中既減少了控制器層的代碼量音诫,也讓模型層不僅僅是花瓶角色。通常情況下仿贬,我們的I/O操作不會直接放在控制器的代碼中纽竣,而是會將這部分操作封裝成一個數(shù)據(jù)庫管理者來執(zhí)行:
@interface LXDDataManager: NSObject
+ (instancetype)sharedManager;
- (void)insertData: (LXDRecord *)record;
- (NSArray<LXDRecord *> *)storedRecord;
@end
這是一段非常常見的數(shù)據(jù)庫管理者的代碼,缺點是顯而易見的:I/O操作的業(yè)務實現(xiàn)對象過于依賴數(shù)據(jù)模型的結(jié)構(gòu)茧泪,這使得這部分業(yè)務幾乎不可復用蜓氨,僅能服務指定的數(shù)據(jù)模型。解決的方案之一采用數(shù)據(jù)庫關(guān)鍵字<->屬性變量名映射的方式傳入映射字典:
@interface LXDDataManager: NSObject
+ (instancetype)managerWithTableName: (NSString *)tableName;
- (void)insertData: (id)dataObject mapper: (NSDictionary *)mapper;
@end
@implementation LXDDataManager
- (void)insertData: (id)dataObject mapper: (NSDictionary *)mapper {
NSMutableString * insertSql = [NSMutableString stringWithFormat: @"insert into %@ (", _tableName];
NSMutableArray * keys = @[].mutableCopy;
NSMutableArray * values = @[].mutableCopy;
NSMutableArray * valueSql = @[].mutableCopy;
for (NSString * key in mapper) {
[keys addObject: key];
[values addObject: ([dataObject valueForKey: key] ?: [NSNull null])];
[valueSql addObject: @"?"];
}
[insertSql appendString: [keys componentsJoinedByString: @","];
[insertSql appendString @") values ("];
[insertSql appendString: [valueSql componentsJoinedByString: @","];
[insertSql appendString: @")"];
[_database executeUpdate: insertSql withArgumentsInArray: values];
}
@end
通過鍵值對映射的方式讓數(shù)據(jù)管理者可以動態(tài)的插入不同的數(shù)據(jù)模型队伟,這樣可以減少I/O操作業(yè)務中對數(shù)據(jù)模型結(jié)構(gòu)的依賴穴吹,使得其更易用。更進一步還可以將這段代碼中mapper的映射任務分離出來嗜侮,通過聲明一個映射協(xié)議來完成這一工作:
@protocol LXDModelMapper <NSObject>
- (NSArray<NSString *> *)insertKeys;
- (NSArray *)insertValues;
@end
@interface LXDDataManager: NSObject
+ (instancetype)managerWithTableName: (NSString *)tableName;
- (void)insertData: (id<LXDModelMapper>)dataObject;
@end
@implementation LXDDataManager
- (void)insertData: (id<LXDModelMapper>)dataObject mapper: (NSDictionary *)mapper {
NSMutableString * insertSql = [NSMutableString stringWithFormat: @"insert into %@ (", _tableName];
NSMutableArray * keys = [dataObject insertKeys];
NSMutableArray * valueSql = @[].mutableCopy;
for (NSInteger idx = 0; idx < keys.count; idx++) {
[valueSql addObject: @"?"];
}
[insertSql appendString: [keys componentsJoinedByString: @","];
[insertSql appendString @") values ("];
[insertSql appendString: [valueSql componentsJoinedByString: @","];
[insertSql appendString: @")"];
[_database executeUpdate: insertSql withArgumentsInArray: [dataObject insertValues]];
}
@end
將這些邏輯分離成協(xié)議來實現(xiàn)的好處包括:
- 移除了I/O業(yè)務中不必要的邏輯港令,侵入性更低
- 讓開發(fā)者實現(xiàn)協(xié)議返回的數(shù)據(jù)排序會更對齊
- 擴展支持I/O操作的數(shù)據(jù)模型
總結(jié)一下M層可以做的事情:
1.提供接口來提供數(shù)據(jù)->展示內(nèi)容的實現(xiàn),盡可能以category的方式完成
2.對于M層統(tǒng)一的業(yè)務比如存取可以以協(xié)議實現(xiàn)的方式提供所需信息
視圖層的Self-Manager
通常情況下锈颗,視圖層只是簡單負責數(shù)據(jù)展示和負責將事件響應轉(zhuǎn)交給控制器C層執(zhí)行顷霹,創(chuàng)建視圖的代碼都在控制器層中完成,因此V層的狀態(tài)也不見得比M好得多击吱。比如當我自定義一個扇形展開的菜單視圖淋淀,在點擊時的響應:
//LXDMenuView.m
- (void)clickMenuItem: (LXDMenuItem *)menuItem {
if ([_delegate respondsToSelector: @selector(menuView:didSelectedItem:)]) {
[_delegate menuView: self didSelectedItem: menuItem.tag];
}
}
//ViewController.m
- (void)menuView: (LXDMenuView *)menuView didSelectedItem: (NSInteger)index {
Class controllerCls = NSClassFromString(_controllerNames[index]);
UIViewController *nextController = [[controllerCls alloc] init];
[self.navigationController pushViewController: nextController animated: YES];
}
這段代碼是最常見的視圖->控制器事件處理流程,當一個控制器界面的自定義視圖覆醇、控件響應事件過多的時候朵纷,即便我們已經(jīng)使用#pragma mark -的方式將這些事件進行分段,但還是會占用過大的代碼量永脓。MVC公認的問題是C完成了太多的業(yè)務邏輯袍辞,導致過胖,跟M層的處理一樣的常摧,筆者同樣將一部分弱業(yè)務轉(zhuǎn)移到V層上搅吁,比如上面的這段頁面跳轉(zhuǎn):
@interface LXDMenuView: UIView
@property (nonatomic, strong) NSArray<NSString *> * itemControllerNames;
@end
@implementation LXDMenuView
- (void)clickMenuItem: (LXDMenuItem *)menuItem {
UIViewController *currentController = [self currentController];
if (currentController == nil) { return; }
Class controllerCls = NSClassFromString(_itemControllerNames[menuItem.tag]);
UIViewController *nextController = [[controllerCls alloc] init];
if ([currentController respondsToSelector: @selector(menuView:transitionToController:)]) {
[currentController menuView: self transitionToController: nextController];
}
[currentController.navigationController pushViewController: nextController animated: YES];
}
- (UIViewController *)currentController {
UIResponder *nextResponder = self.nextResponder;
while (![nextResponder isKindOfClass: [UIWindow class]]) {
if ([nextResponder isKindOfClass: [UIViewController class]]) {
return (UIViewController *)nextResponder;
}
nextResponder = nextResponder.nextResponder;
}
return nil;
}
@end
這種業(yè)務轉(zhuǎn)移的思路來自于開發(fā)中的Self-Manager模式一文那婉。在這種代碼結(jié)構(gòu)中党瓮,如果V層決定了控制器接下來的跳轉(zhuǎn)寞奸,那么可以考慮將跳轉(zhuǎn)的業(yè)務遷移到V中執(zhí)行枪萄。通過事件鏈查找的方式獲取所在的控制器,這一過程并不能說違背了MVC的訪問限制原則聚凹,在整個過程中V不在乎其所在的currentController和nextController的具體類型妒牙,通過自定義一個協(xié)議來在跳轉(zhuǎn)前將nextController發(fā)送給當前控制器完成跳轉(zhuǎn)前的配置湘今。
這里要注意的是摩瞎,Self-Manager有其特定的使用場景旗们。當視圖層的回調(diào)處理需要兩層或者更多的時候蚪拦,Self-Manager能有效的執(zhí)行
如果抽離的足夠高級,甚至可以定義一個同一個的Self-Manager協(xié)議來提供給自定義視圖完成這些工作洛巢。這樣同一套業(yè)務邏輯可以給任意的自定義視圖復用次兆,只要其符合視圖<->控制器的捆綁關(guān)系:
@protocol LXDViewSelfManager <NSObject>
@optional
- (void)customView: (UIView *)customView transitionToController: (UIViewController *)nextController;
@end
視圖層的動畫效果
動畫實現(xiàn)也是屬于V部分的邏輯漓库,這點的理由有這么兩個:
- 動畫實現(xiàn)和演示視圖存在依賴關(guān)系
- 將動畫實現(xiàn)放到視圖層可以實現(xiàn)動效視圖的復用
話是這么說渺蒿,但是在許多的項目中茂装,這樣的代碼比比皆是:
@implementation ViewController: UIViewController
//彈窗動畫效果
- (void)animatedAlertView {
AlertView *alert = [[AlertView alloc] initWithMessage: @"這是一條彈窗警告信息"];
alert.alpha = 0;
alert.center = self.view.center;
alert.transform = CGAffineTransformMakeScale(0.01, 0.01);
[UIView animateWithDuration: 0.25 animations: ^{
alert.alpha = 1;
alert.transform = CGAffineTransformIdentity;
}];
}
@end
具體的槽點筆者就不吐了城侧,對于動畫實現(xiàn)筆者只有一個建議:無論你要實現(xiàn)的動畫多么簡單彼妻,統(tǒng)一扔到View中去實現(xiàn)侨歉,提供接口給C層調(diào)用展示摊册。要知道茅特,飽受開發(fā)者吐槽的UIAlertView在彈窗效果上的接口簡潔的挑不出毛病棋枕,僅僅一個- (void)show就完成了眾多的動畫效果兵睛。如果你不喜歡因為動畫效果就要自定義視圖窥浪,那么將常用的動畫效果以category的方式擴展出來使用:
@interface UIView (Animation)
- (void)pop;
@end
@implementation UIView (Animation)
- (void)pop {
CGPoint center = CGPointMake(self.superView.frame.size.width / 2, self.superView.frame.size.height / 2);
self.center = center;
self.alpha = 0;
self.transform = CGAffineTransformMakeScale(0.01, 0.01);
[UIView animateWithDuration: 0.25 animations: ^{
self.alpha = 1;
self.transform = CGAffineTransformIdentity;
}];
}
@end
瘦身Controller
MVC中最大的問題在于C層負擔了太多的業(yè)務假颇,所以導致Controller過大骨稿。那么將一些不屬于的Controller業(yè)務的邏輯分離到其他層中是主要的解決思路。iOS的MVC模式也被稱作重控制器模式哥桥,這是在實際開發(fā)中激涤,我們可以看到V和C難以相互獨立已卸,這兩部分總是緊緊的粘合在一起的:
在iOS中累澡,Controller管理著自己的視圖的生命周期愧哟,因此會和這個視圖本身產(chǎn)生較大的耦合關(guān)系蕊梧。這種耦合最大的表現(xiàn)在于我們的V總是幾乎在C中創(chuàng)建的肥矢,生命周期由C層來負責,所以對于下面這種視圖創(chuàng)建代碼我們并不會覺得有什么問題:
//ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame: CGRectMake(20, 60, self.view.bounds.size.width - 40, 45)];
[btn setTitle: @"點擊" forState: UIControlStateNormal];
[btn addTarget: self action: @selector(clickButton:) forControlEvents: UIControlEventTouchUpInside];
[self.view addSubview: btn];
}
但是按照業(yè)務邏輯來說十艾,我們可以在Controller里面創(chuàng)建視圖忘嫉,但是配置的任務不應該輕易的放在C層庆冕。因此愧杯,這些創(chuàng)建工作完全可以使用視圖的category來實現(xiàn)配置業(yè)務,對于常用的控件你都可以嘗試封裝一套構(gòu)造器來減少Controller中的代碼:
@interface UIButton(LXDDesignedInitializer)
+ (instancetype)buttonWithFrame: (CGRect)frame text: (NSString *)text;
+ (instancetype)buttonWithFrame: (CGRect)frame text: (NSString *)text textColor: (UIColor *)textColor;
+ (instancetype)buttonWithFrame: (CGRect)frame text: (NSString *)text textColor: (UIColor *)textColor fontSize: (CGFloat)fontSize target: (id)target action: (SEL)action;
+ (instancetype)buttonWithFrame: (CGRect)frame text: (NSString *)text textColor: (UIColor *)textColor fontSize: (CGFloat)fontSize cornerRadius: (CGFloat)cornerRadius;
+ (instancetype)buttonWithFrame: (CGRect)frame text: (NSString *)text textColor: (UIColor *)textColor fontSize: (CGFloat)fontSize cornerRadius: (CGFloat)cornerRadius target: (id)target action: (SEL)action backgroundColor: (UIColor *)backgroundColor;
+ (instancetype)buttonWithFrame:(CGRect)frame text:(NSString *)text textColor:(UIColor *)textColor fontSize: (CGFloat)fontSize cornerRadius: (CGFloat)cornerRadius target: (id)target action: (SEL)action image: (NSString *)image selectedImage: (NSString *)selectedImage backgroundColor: (UIColor *)backgroundColor;
@end
此外跌前,如果我們需要使用代碼設(shè)置視圖的約束時抵乓,Masonry大概是減少這些代碼的最優(yōu)選擇灾炭。視圖配置代碼是我們瘦身Controller的一部分蜈出,其次在于大量的代理協(xié)議方法铡原。因此,使用category將代理方法實現(xiàn)移到另外的文件中是一個好方法:
@interface ViewController (LXDDelegateExtension)<UITableViewDelegate, UITableViewDataSource>
@end
@implementation ViewController(LXDDelegateExtension)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//configurate and return cell
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//return rows in section of cell number
}
@end
這種方式簡單的把代理方法挪移到category當中卵洗,但是也存在著一些缺點,因此適用場合會比較局限:
- 在category中不能訪問原類的私有屬性酪夷、方法。這點Swift要超出OC太多
- 在減少原類的代碼量的情況下實際上使得整個項目結(jié)構(gòu)讀起來更加復雜
筆者在通過上述的方式分離代碼之后晚岭,控制器層的代碼量基本可以得到控制鸥印。當然,除了上面提到的之外坦报,還有一個小的補充库说,我們基本都使用#pragma mark給控制器的代碼分段,一個比較有層次的分段注釋大概是這樣的:
#pragma mark - View Life
//視圖生命周期
#pragma mark - Setup
//創(chuàng)建視圖等
#pragma mark - Lazy Load片择、Getter潜的、Setter
//懶加載、Getter和Setter
#pragma mark - Event字管、Callbacks
//事件啰挪、回調(diào)等
#pragma mark - Delegate And DataSource
//代理和數(shù)據(jù)源方法
#pragma mark - Private
//私有方法
認真看是不是發(fā)現(xiàn)了其實很多的業(yè)務邏輯我們都能通過category的方式從Controller中分離出去信不。在這里我非常同意Casa大神的話:不應該出現(xiàn)私有方法下硕。對于控制器來說嫩码,私有方法基本都是數(shù)據(jù)相關(guān)的業(yè)務處理,將這些業(yè)務通category
或者策略模式分離出去會讓控制器更加簡潔
尾言
其實不管是熱門的MVVM架構(gòu)、或者其他稍冷的MVCS、VIPER之類的架構(gòu)模式,都是基于MVC改進的。本文不是要講MVC的代碼應該怎么分層辜膝,只是把自己對于這個模式的思考簡單的分享一下忱辅,希望能讓各位有所領(lǐng)悟。當然谈为,沒有一種結(jié)構(gòu)是絕對完美的签舞,業(yè)務職責的劃分必然帶來其相應的負面影響搂鲫,找到這些劃分的平衡點就是我們學習架構(gòu)設(shè)計的意義所在
原文鏈接
作者:sindri的小巢
鏈接:http://www.reibang.com/p/4847c9a1e19b
來源:簡書
著作權(quán)歸作者所有擦酌。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)介袜,非商業(yè)轉(zhuǎn)載請注明出處巍耗。