寫一個iOS復雜表單的正確姿勢
前言
這幾天項目的新需求中有個復雜的表單界面,在做的過程中發(fā)現(xiàn)要比想象中復雜很多,有好多問題需要處理。有很多東西值得寫下來好好梳理下。
需求分析:
6創(chuàng)建網(wǎng)店1.png
上圖便是UI根據(jù)需求給的高保真, 我們先根據(jù)這張圖片來描述一下具體需求,明確一下我們都需要干些什么。
創(chuàng)建網(wǎng)店這個界面是一個復雜的表單,有“網(wǎng)店名稱”、“網(wǎng)店主標簽”、“網(wǎng)店簡介”、“網(wǎng)店地址”、“網(wǎng)店座機”、“email”、“網(wǎng)店LOGO”、“網(wǎng)店封面圖”這些項。大部分都是輸入框,但也有幾項有所不同。“網(wǎng)店地址”項,當被點擊后會彈出一個pickView來選擇“市&區(qū)”;“網(wǎng)店LOGO”和“網(wǎng)店封面圖”是一樣的,是選取圖片的控件,要求既可以通過相冊選取圖片,也可以現(xiàn)場拍照選擇。當被點擊后,彈出一個ActionSheet來是以“拍照”或以“相冊”來選取圖片。當選取成功后拍照的背景圖片變?yōu)楸贿x取的圖片,并在右上角出現(xiàn)一個刪除按鈕,可以刪除還原再次選取。
表單中除了“email”外所有的項目都是必填的,且“網(wǎng)店名稱”、“網(wǎng)店主標簽”、“網(wǎng)店簡介”和“網(wǎng)店座機”分別有30、20、500、15字的長度限制。“email”雖然為選填,但若填寫了則會進行郵箱格式校驗。對字數(shù)長度的限制要在輸入過程中進行監(jiān)聽,若輸入時超過限制,則輸入框出現(xiàn)紅色邊框并出現(xiàn)提示文字。等***點擊了“提交”按鈕后要進行數(shù)據(jù)校驗,所有該填但未填,所有格式不正確的項都會出現(xiàn)紅框和提示文字,當所有數(shù)據(jù)都合法后才可以提交給服務器。
需求大體就是如此。
這個界面我們還是以tableView來實現(xiàn),由cell視圖來表示圖中所需填寫的項目。那我們得先分析下這個界面需要寫哪幾種樣式的cell。
該界面總共有4種樣式的cell。4種樣式的cell樣式也有共同點,每個cell左邊部分均為表示該行所要填寫的項目名稱,右邊部分則為填寫或者選取的內(nèi)容值,這些值的顯示形式有所不同。 CreateShopTFCell和CreateShopTVCell其實非常類似,右邊首先是一個灰色的背景視圖,只不過在灰色背景之上的前者是textField,而后者是textView;CreateShopPickCell右邊則是兩個灰色背景視圖,點擊之后便彈出一個pickView供你選取“市&區(qū)”;CreateShopUploadPicCell右邊則是一個UIImageView,無圖片被選取時默認是一個相機的圖片,當被點擊后彈出ActionSheet供你選擇拍照還是從相冊選取照片,選好照片后UIImageView的圖片被替換,并在右上角出現(xiàn)紅色的刪除按鈕。
如下圖所示:
6創(chuàng)建網(wǎng)店.png
正確地將視圖和數(shù)據(jù)綁定:
我們假設已經(jīng)寫好了上面4種樣式cell的代碼,現(xiàn)在我們在控制器里為其填充數(shù)據(jù)。
我們首先定義一個表示cell數(shù)據(jù)的CreateShopModel。該model是為了給cell填充數(shù)據(jù),可以看到它里面的屬性就是cell上對應應該顯示的數(shù)據(jù)項。
同時,我們在開頭也定義了一個枚舉CreateShopCellType來代表4種不同樣式的cell,用于在tableView返回cell的代理方法里根據(jù)枚舉值來返回相應樣式的cell。
- #import
- typedef enum : NSUInteger {
- CreateShopCellType_TF = 0, // textfield
- CreateShopCellType_TV, // textView
- CreateShopCellType_PICK, // picker
- CreateShopCellType_PIC, // upload picture
- } CreateShopCellType;
- @interface CreateShopModel : NSObject
- @property (nonatomic, copy)NSString *title; // 所要填寫的項目名稱
- @property (nonatomic, copy)NSString *placeholder;
- @property (nonatomic, copy)NSString *key; // 表單對應的字段
- @property (nonatomic, copy)NSString *errText; // 校驗出錯時的提示信息
- @property (nonatomic, strong)UIImage *image; // 所選取的圖片
- @property (nonatomic, assign)CreateShopCellType cellType; // cell的類型
- @property (nonatomic, assign)NSInteger maxInputLength; // ***輸入長度限制
- @end
我們在將tableView創(chuàng)建并添加在控制器的view上后便可以初始化數(shù)據(jù)源了。該界面tableView的數(shù)據(jù)源是_tableViewData數(shù)組,數(shù)據(jù)的每項元素是代表cell顯示數(shù)據(jù)的CreateShopModel類型的model。準確地來說,這些數(shù)據(jù)是表單未填寫之前的死數(shù)據(jù),所以需要我們手動地給裝入數(shù)據(jù)源數(shù)組中。而在輸入框輸入或者選取而得的數(shù)據(jù)則需要我們在輸入之后將其捕獲存儲下來,以等到提交時提交給服務器,這個也有需要注意的坑點,后面再說。
現(xiàn)在我們的數(shù)據(jù)源準備好了,但是tableView還沒做處理呢,要等tableView也配套完成后再刷新tableView就OK了。我們來看tableView代理方法。

首先比較簡單的,在設置行高的代理方法里,根據(jù)該行數(shù)據(jù)所表示的cellType類型來設置相應的行高。
然后在返回cell的代理方法里,同樣以cellType來判斷返回相應樣式的cell,并給該cell賦相應的數(shù)據(jù)model。但是我們注意到,給cell賦值的方法,除了傳入我們前面說定義的CreateShopModel類型的createModel外,還有個名叫_shopFormModel參數(shù)被傳入。_shopFormModel是什么,它代表什么意思?
_shopFormModel是CreateShopFormModel類型的一個實例對象,它用來表示這個表單需要提交的數(shù)據(jù),它里面的每個屬性基本上對應著表單提交給服務器的字段。我們***不是要將表單數(shù)據(jù)作為參數(shù)去請求提交的接口嗎?表單數(shù)據(jù)從哪里來,就從_shopFormModel中來。那_shopFormModel中的數(shù)據(jù)從哪里來?
- #import
- @interface CreateShopFormModel : NSObject
- @property (nonatomic, copy)NSString *groupId;
- @property (nonatomic, copy)NSString *groupName;
- @property (nonatomic, copy)NSString *tag;
- @property (nonatomic, copy)NSString *introduction;
- @property (nonatomic, copy)NSString *regionId;
- @property (nonatomic, copy)NSString *cityId;
- @property (nonatomic, copy)NSString *address;
- @property (nonatomic, copy)NSString *telephone;
- @property (nonatomic, copy)NSString *contactMail;
- @property (nonatomic, copy)NSString *coverUrl;
- @property (nonatomic, copy)NSString *logoUrl;
- @property (nonatomic, strong)UIImage *logo;
- @property (nonatomic, strong)UIImage *cover;
- @property (nonatomic, strong)NSIndexPath *indexPath;
- @property (nonatomic, strong)id indexPathObj;
- + (CreateShopFormModel *)formModelFromDict:(NSDictionary *)dict;
- -(BOOL)submitCheck:(NSArray*)dataArr;
- @end
以CreateShopTFCell為例,它所表示的字段的數(shù)據(jù)是我們在輸入框輸入的,也就是說數(shù)據(jù)來自textField,_shopFormModel對象在控制器被傳入cell的refreshContent:formModel:方法,在該方法內(nèi)部,將參數(shù)formModel賦給成員變量_formModel。需要格外注意的是,_shopFormModel、formModel和_ formModel是同一個對象,指向的是同一塊內(nèi)存地址。方法傳遞對象參數(shù)時只是“引用拷貝”,拷貝了一份對象的引用。既然這樣,我們可以預想到,我們在cell內(nèi)部,將textField輸入的值賦給_formModel所指向的對象后,也即意味著控制器里的_shopFormModel也有數(shù)據(jù)了,因為它們本來就是同一個對象嘛!
事實正是如此。
可以看到我們在給textField添加的通知的回調(diào)方法textFiledEditChanged:里,將textField輸入的值以KVC的方式賦值給了_formModel。此時_formModel的某屬性,即該cell對應的表單的字段已經(jīng)有了數(shù)據(jù)。同樣的,在控制器中與_formModel指向同一塊內(nèi)存地址的_shopFormModel也有了數(shù)據(jù)。

我們看到在refreshContent:formModel:方法中,cell上的死數(shù)據(jù)是被CreateShopModel的實例對象createModel賦值的,而在其后我們又以KVC的方式又將_shopFormModel的某屬性的值賦給了textField。這是因為我們?yōu)榱朔乐筩ell在復用的過程中出現(xiàn)數(shù)據(jù)錯亂的問題,而在給cell賦值前先將每個視圖上的數(shù)據(jù)都清空了(即clearCellData方法),需要我們重新賦過。(不過,如果你沒清空數(shù)據(jù)的情況下,不再次給textField賦值好像也是沒問題的。不會出現(xiàn)數(shù)據(jù)錯亂和滑出屏幕再滑回來時從復用池取出cell后賦值時數(shù)據(jù)消失的問題。)
輸入長度的限制:
需求中要求“網(wǎng)店名稱”、“網(wǎng)店主標簽”、“網(wǎng)店簡介”、“網(wǎng)店座機”都有輸入長度的限制,分別為30、20、500、15字數(shù)的限制。其實我們在上面初始化數(shù)據(jù)源的時候已經(jīng)為每行的數(shù)據(jù)源model設置過字數(shù)限制了,即maxInputLength屬性。
我們還是以CreateShopTFCell為例。
要在開始輸入的時候監(jiān)聽輸入的長度,若字數(shù)超過***限制,則要出現(xiàn)紅框,并且顯示提示信息。那我們就得給textField開始輸入時添加valueChange的觀察,在textField輸入結束時移除觀察。
另外,可以看到在textField開始輸入的回調(diào)方法里,調(diào)用了該cell的代理方法。該cell為什么要調(diào)用這個代理方法,它需要代理給別人來干什么?…其實這個和鍵盤遮擋的處理有關,下面我們慢慢解釋。
處理鍵盤遮擋問題:
這個界面有很多行輸入框,在自然情況下,下面的幾個輸入框肯定是在鍵盤彈出后高度之下的,也即會被鍵盤遮擋住,我們沒法輸入。這時就一定處理鍵盤遮擋問題了。
關于鍵盤遮擋問題,其實我在以前的一篇筆記中就寫過了:UITextField一籮筐——輸入長度限制、自定義placeholder、鍵盤遮擋問題
我們要處理鍵盤遮擋問題,也就是要實現(xiàn)當鍵盤彈出時,被遮擋住的輸入框能上移到鍵盤高度之上;當鍵盤收回時,輸入框又能移回原來的位置。那么首先***步,我們得能獲取到鍵盤彈出或者收回這個動作的時機,在這個時機我們再按需要移動輸入框的位置。系統(tǒng)提供了表示鍵盤彈出和收回的兩個觀察的key,分別為UIKeyboardWillShowNotification和UIKeyboardWillHideNotification。注冊這兩個觀察者,然后在兩者的回調(diào)方法里實現(xiàn)輸入框位移就大功告成了。
因為鍵盤遮擋的處理有可能是比較普遍的需求,所以在公司的項目架構設計里是把上面兩個關于鍵盤的觀察是注冊在APPDelegate.m中的,并定義了一個有關鍵盤遮擋處理的協(xié)議,協(xié)議里定義了一個方法。具體需要具體處理,由需要處理鍵盤遮擋問題的控制器來實現(xiàn)該協(xié)議方法,具體實現(xiàn)怎么移動界面元素來使鍵盤不遮擋輸入框。這么說現(xiàn)在CreateShopViewController控制器需要處理鍵盤遮擋問題,那么就需要設置它為APPDelegate的代理,并由它實現(xiàn)所定義的協(xié)議嗎?其實不用,公司項目所有的控制器都是繼承于基類CommonViewController,在基類中實現(xiàn)了比較基本和普遍的功能,其實在基類中便定義了下面的方法來設置控制器為APPDelegate的代理,不過需要屬性isListensKeyboard為YES。下面這個方法在CommonViewController中是在viewWillAppear:方法中調(diào)用的。那我們在子類CreateShopViewController中需要做的僅僅只要在viewWillAppear之前設置isListensKeyboard屬性為YES,便會自動設置將自己設為APPDelegate的代理。然后在CreateShopViewController控制器里實現(xiàn)協(xié)議所定義的方法,實現(xiàn)具體的輸入框移動問題。
CommonViewController.m
- -(void)initListensKeyboardNotificationDelegate
- {
- if (!self.isListensKeyboard) {
- return;
- }
- if (!self.appDelegate) {
- self.appDelegate=(AppDelegate*)[[UIApplication sharedApplication] delegate];
- }
- [self.appDelegate setKeyboardDelegate:self];
- }
CreateShopViewController.m
可以看到在該代理方法的實現(xiàn)里。當鍵盤彈出時,我們首先將tableView的contentSize在原來的基礎上增加了鍵盤的高度keyBoard_h。然后將tableView的contentOffset值變?yōu)閟et_y,這個set_y的值是通過計算而來,但是計算它的_inputY這個變量代表什么意思?
我們可以回過頭去看看tableView返回cell的代理方法中,當為CreateShopTFCell時,我們設置了當前控制器為其cell的代理。
- cell.cellDelegate = self;
并且我們的控制器CreateShopViewController也實現(xiàn)了該cell的協(xié)議CreateShopTFCellDelegate,并且也實現(xiàn)了協(xié)議定義的方法。
- #pragma mark - tfCell delegate
- - (void)cellBeginInputviewY:(CGFloat)orginY
- {
- _inputY = orginY;
- }
原來上面的_intputY變量就是該協(xié)議方法從cell里的調(diào)用處傳遞而來的orginY參數(shù)值。我們回過頭看上面的代碼,該協(xié)議方法是在textField的開始輸入的回調(diào)方法里調(diào)用的,給協(xié)議方法傳入的參數(shù)是self.frame.origin.y,即被點擊的textField在手機屏幕內(nèi)所在的Y坐標值。
可以看到,處理鍵盤遮擋問題,其實也不是改變輸入框的坐標位置,而是變動tableView的contentSize和contentOffset屬性。
選取地址的實現(xiàn):
CreateShopPickCell實現(xiàn)里地址的選取和顯示。有左右兩個框框,點擊任何一個將會從屏幕下方彈出一個選取器,選取器有“市”和“區(qū)”兩列數(shù)據(jù)對應兩個框框,選取器左上方是“取消”按鈕,右上方是“確定”按鈕。點擊“取消”,選取器彈回,并不進行選取;點擊“確定”,選取器彈回,選取選擇的數(shù)據(jù)。
WechatIMG1.png
CreateShopPickCell的界面元素布局沒什么可說的,值得一說的是彈出的pickView視圖,是在cell的填充數(shù)據(jù)的方法中創(chuàng)建的。
這里只是創(chuàng)建了pickView的對象,并設置了數(shù)據(jù)源items,已經(jīng)點擊之后的回調(diào)block,而并未將其添加在父視圖上。
要將選取的“市&區(qū)”的結果從CustomPickView中以block回調(diào)到cell來,將數(shù)據(jù)賦給_formModel。并且當有了數(shù)據(jù)后UILabel的文本顏色也有變化。
pickView的對象已經(jīng)創(chuàng)建好,但是還未到彈出顯示的時機。所謂時機,就是當左右兩個框框被點擊后。
可以看到pickView是被添加在window上的。并且調(diào)用了pickView的接口方法showPickerView方法,讓其從屏幕底部彈出來。
- - (void)cityGestureHandle:(UITapGestureRecognizer *)tapGesture
- {
- [_superView endEditing:YES];
- [self showPicker];
- }
- - (void)areaGestureHandle:(UITapGestureRecognizer *)tapGesture
- {
- [_superView endEditing:YES];
- [self showPicker];
- }
- -(void)showPicker
- {
- [[PubicClassMethod getCurrentWindow] addSubview:_pickView];
- [_pickView showPickerView];
- }
前面代碼中給pickView設置數(shù)據(jù)源時,它的數(shù)據(jù)源有點特別,調(diào)用了ShopAddressModel的類方法cityAddressArr來返回有關地址的數(shù)據(jù)源數(shù)組。這是因為這里的地址數(shù)據(jù)雖然是從服務器接口請求的,但是一般情況不會改變,***是從服務器拿到數(shù)據(jù)后緩存在本地,當請求失敗或者無網(wǎng)絡時仍不受影響。
ShopAddressModel類定義了如下幾個屬性和方法。
- @interface ShopAddressModel : NSObject
- @property (nonatomic, copy)NSString *addresssId;
- @property (nonatomic, copy)NSString *name;
- @property (nonatomic, strong)NSArray *subArr;
- #pragma mark - 地址緩存
- + (void)saveAddressArr:(NSArray *)addressArr;
- +(NSArray*)cityAddressArr;
- +(NSArray*)addressArr;
- #pragma mark - 解析
- + (ShopAddressModel *)addressModelFromDict:(NSDictionary *)dict;
- @end
當我們我們從服務器拿到返回而來的地址數(shù)據(jù)后,調(diào)用saveAddressArr:方法,將數(shù)據(jù)緩存在本地。
- + (void)saveAddressArr:(NSArray *)addressArr
- {
- if (addressArr && addressArr.count > 0) {
- NSData *data = [NSKeyedArchiver archivedDataWithRootObject:addressArr];
- [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"saveAddressArr"];
- }else
- {
- [[NSUserDefaults standardUserDefaults]setObject:nil forKey:@"saveAddressArr"];
- }
- [[NSUserDefaults standardUserDefaults] synchronize];
- }
當創(chuàng)建好pickView后以下面方法將本地緩存數(shù)據(jù)讀出,賦給items作為數(shù)據(jù)源。
注意:這也是為什么把創(chuàng)建pickView的代碼放在了填充cell數(shù)據(jù)的refreshContent:formModel:里,而不在創(chuàng)建cell界面元素時一氣創(chuàng)建pickView。因為那樣當用戶***次打開這個界面,有可能數(shù)據(jù)來的比較慢,當代碼執(zhí)行到賦數(shù)據(jù)源items時,本地還沒有被緩存上數(shù)據(jù)呢!這樣用戶***次進入這個界面時彈出的pickView是空的,沒有數(shù)據(jù)。而放在refreshContent:formModel:中是安全穩(wěn)妥的原因是,每次從接口拿到數(shù)據(jù)后我們會刷新tableView,便會執(zhí)行refreshContent:formModel:方法。它能保證先拿到數(shù)據(jù),再設置數(shù)據(jù)源的順序。
提交表單時校驗數(shù)據(jù):
在將表單數(shù)據(jù)提交前,要先校驗所填寫的表單是否有問題,該填的是否都填了,已填的數(shù)據(jù)格式是否是對的。若有問題,則要出現(xiàn)紅框和提示信息提醒用戶完善,等數(shù)據(jù)無誤后才可以提交給服務器。
數(shù)據(jù)校驗代碼很繁長,寫在控制器里不太好。因為它是對表單數(shù)據(jù)的校驗,那我們就寫在CreateShopFormModel里,這樣既可以給控制器瘦身,也可以降低耦合度,數(shù)據(jù)的歸數(shù)據(jù),邏輯的歸邏輯。
從前面CreateShopFormModel.h的代碼里我們其實已經(jīng)看到了這個校驗方法:submitCheck:。若某條CreateShopFormModel實例的數(shù)據(jù)不達要求,則在相應的CreateShopModel數(shù)據(jù)源對象的errText屬性賦值,意為提示信息。該方法的返回值類型為BOOL值,有數(shù)據(jù)不合格則返回NO。此時,在調(diào)用該方法的外部,應該將tableView重新加載,因為此時在該方法內(nèi)部,已將數(shù)據(jù)格式不合格的提示信息賦值給了相應的數(shù)據(jù)源model。
- - (BOOL)submitCheck:(NSArray*)dataArr
- {
- BOOL isSubmit=YES;
- if(self.groupName.length==0){
- if (dataArr.count>0) {
- CreateShopModel *cellObj=dataArr[0];
- cellObj.errText=@"網(wǎng)店名不能為空";
- }
- isSubmit=NO;
- }
- if(self.groupName.length>0){
- if(dataArr.count>0){
- if(self.groupName.length>30){
- CreateShopModel *cellObj=dataArr[0];
- cellObj.errText=@"最多30個字";
- isSubmit=NO;
- }
- }
- }
- if(self.tag.length==0){
- if (dataArr.count>1) {
- CreateShopModel *cellObj=dataArr[1];
- cellObj.errText=@"標簽不能為空";
- }
- isSubmit=NO;
- }
- if(self.introduction.length==0){
- if (dataArr.count>2) {
- CreateShopModel *cellObj=dataArr[2];
- cellObj.errText=@"簡介不能為空";
- }
- isSubmit=NO;
- }
- if(self.introduction.length>0){
- if(dataArr.count>2){
- if(self.introduction.length>30){
- CreateShopModel *cellObj=dataArr[2];
- cellObj.errText=@"最多500個字";
- isSubmit=NO;
- }
- }
- }
- if(self.regionId.length==0){
- if (dataArr.count>3) {
- CreateShopModel *cellObj=dataArr[3];
- cellObj.errText=@"市區(qū)不能為空";
- }
- isSubmit=NO;
- }
- if(self.address.length==0){
- if (dataArr.count>4) {
- CreateShopModel *cellObj=dataArr[4];
- cellObj.errText=@"地址不能為空";
- }
- isSubmit=NO;
- }
- if(self.telephone.length==0){
- if (dataArr.count>5) {
- CreateShopModel *cellObj=dataArr[5];
- cellObj.errText=@"電話不能為空";
- }
- isSubmit=NO;
- }
- if (self.contactMail.length>0) {
- if (dataArr.count>6) {
- CreateShopModel *cellObj=dataArr[6];
- if(![PubicClassMethod isValidateEmail:self.contactMail]){
- cellObj.errText=@"郵箱格式不合法";
- isSubmit=NO;
- }
- }
- }
- if(self.logoUrl.length==0&&!self.logo){
- if (dataArr.count>7) {
- CreateShopModel *cellObj=dataArr[7];
- cellObj.errText=@"logo不能為空";
- }
- isSubmit=NO;
- }
- if(self.coverUrl.length==0&&!self.cover){
- if (dataArr.count>8) {
- CreateShopModel *cellObj=dataArr[8];
- cellObj.errText=@"封面圖不能為空";
- }
- isSubmit=NO;
- }
- return isSubmit;
- }
上傳圖片到七牛:
當點擊了“提交”按鈕后,先校驗數(shù)據(jù),若所填寫的數(shù)據(jù)不合格,則給出提示信息,讓用戶繼續(xù)完善數(shù)據(jù);若數(shù)據(jù)無問題,校驗通過,則開始提交表單。但是,這里有圖片,圖片我們是上傳到七牛服務器的,提交表單是圖片項提交的應該是圖片在七牛的一個url。這個邏輯我在以前的這篇筆記已經(jīng)捋過了APP上傳圖片至七牛的邏輯梳理。
但是當時所有的邏輯都是寫在控制器里的。我們這個“創(chuàng)建網(wǎng)店”的控制器已經(jīng)很龐大了,寫在控制器里不太好。所以在這里我將上傳圖片的邏輯拆分了出去,新建了一個類`QNUploadPicManager。只暴露一個允許傳入UIImage參數(shù)的接口方法,便可以通過successBlock來返回上傳到七牛成功后的url。以及通過failureBlock來返回上傳失敗后的error信息。而將所有的邏輯封裝在QNUploadPicManager內(nèi)部,這樣控制器里便精簡了不少代碼,清爽了許多。
QNUploadPicManager.h
- @interface QNUploadPicManager : NSObject
- - (void)uploadImage:(UIImage *)image successBlock:(void(^)(NSString *urlStr))successBlock failureBlock:(void(^)(NSError *error))failureBlock;
- @end
QNUploadPicManager.m
總結:
這個界面比較核心的一個問題就是:要在控制器里提交表單,那怎樣把在UITableViewCell里的textField輸入的數(shù)據(jù)傳遞給控制器? 另外一個問題是一個邏輯比較復雜的界面,控制器勢必會很龐大,應該有意的給控制器瘦身,不能把所有的邏輯都寫在控制器里。有關視圖顯示的就考慮放入UITableViewCell,有關數(shù)據(jù)的就考慮放入model。這樣既為控制器瘦身,也使代碼職責變清晰,耦合度降低。