iOS-APP重構之路(二) Model的設計

ios8988發表於2018-09-12

前言

很多的app使用MVC設計模式來將“使用者互動”與“資料和邏輯”分開,而model其中一個重要作用就是持久化。下文中設計的Model可能不是一個完美的,擴充套件性強的model範例,但在我需要重構的app中,這樣的設計能夠滿足我的需要。

關於Model

Model層包含了app的資料與邏輯,Model層中的類需要關心的是資料的表現,儲存,以及操作。Model層是整個app生態中相對獨立的一個部分,因為它不會直接與controller層或者是View層進行通訊,而是在其他層需要請求它的資訊的時候進行間接通訊。

Model有什麼用?

想要寫好一個model,首先要清晰Model的作用。

  • 屬性存取:將檔案中的一些特性和資料以屬性的形式儲存

  • 可變性:屬性可以readwrite,所以能夠被改變,並儲存到本地

  • KVO:可以觀察一個屬性的值並在它改變的時候受到通知,並以此對UI或其他地方進行控制

  • 處理資料:根據業務邏輯處理網路獲取資料與本地儲存資料

如何定義Model類

我們可以建立一系列的Model類,它們之間可以互相繼承,同時每一個Model類是與當前app中的實體對應。比如在我當前需要重構的app中,使用者資料對應的是UserInformationModel,班級資訊對應的是StudentClassModel。

另一方面,在Model類的實現過程中,有許多問題需要解決,所以我下面會根據我當前正在重構的app的一些情況結合來解釋。

資訊的儲存格式處理

資料可以以各種不同的格式儲存,在我重構的app中,資料等資訊是使用普通的資料結構來存放,比如使用資料或者是字典來儲存Model的資訊。在一開始建立Model的時候並沒有太大的問題,但是當需求不斷增加,一個Model類的資訊開始變得龐大起來的時候,問題就開始浮現了。比如下面我要輸出使用者的姓名、年齡、班級、班主任、年級等資料。

// never do this !!!
- (void)printInformation
{
NSLog(@"name: %@", [user objectForKey:@"name"]);
NSLog(@"age: %@"m [user objectForKey:@"age"]);
NSLog(@"class: %@"m [user objectForKey:@"clazz"]);
NSLog(@"teacher: %@"m [user objectForKey:@"teacher"]);
NSLog(@"grade: %@"m [user objectForKey:@"grade"]);
}

這樣取出資料似乎沒什麼問題,但是當資料多起來的時候就會發現程式碼會十分混亂,而且不!美!觀!,同時最主要的問題是,在從字典中取出資料的過程中會把key打錯,導致資料取出失敗。

所以在設計Model過程中,儘可能將Model設計成一個類而不是一個結構,在類中可以使用屬性(Property)來存取資訊,它能夠提供給開發者基本的拼寫檢查,完全杜絕打錯key導致的資料獲取失敗的情況,同時方便其他開發者看到Model中儲存的資料型別,也方便日後的擴充套件與維護。

// YES! do this!
- (void)printInformation
{
NSLog(@"name: %@", user.name);
NSLog(@"age: %@", user.age);
NSLog(@"class: %@", user.clazz);
NSLog(@"teacher: %@", user.teacher);
NSLog(@"grade: %@", user.grade);
}

 

網路資料處理

由於我重構的app大部分功能是基於網路的,所以網路資料在Model層的處理會是重點。因為網路資料是非同步獲取的,而且獲取過程很容易失敗。所以網路資料的獲取比本地資料的獲取難度要更大。另一方面,很多app的網路請求框架中都提供了快取功能,可以在下一次請求中從快取中更快的獲取資料,而不需要再次進行網路請求,這裡又涉及到本地資料獲取等問題,使得網路資料的處理顯得尤其繁瑣。

在一個同步的網路環境下,我們可以把錯誤處理放到其他地方,可以簡單的做快取,甚至可以像處理本地資料一樣來更新、刪除、新增新的網路資料。但很不幸的是,網路是非同步的,所以我們需要處理這個重要問題。

首先我在上文提到的快取、失敗處理,這些應該是交給網路請求框架來進行處理,最後得到一個responseObject,再對responseObject進行model轉換等操作。

比如我在當前的專案中,是利用了MJExtension來進行字典與模型之間的轉換,而這個轉換,我是放到了每個單獨的API中,比如對於庫存的請求,我在InventoryAPI中進行對responseObject轉換成InventoryModel的操作,而不是放到vc中進行,這樣在vc中僅僅是進行網路請求,請求完成後返回的是我想要的model。

InventoryAPI.m

- (id)modelingFormJSONResponseObject:(id)JSONResponseObject
{
    NSUInteger count = ((NSArray *)JSONResponseObject).count;
    NSMutableArray *modelsArray = [NSMutableArray array];
    for(int i = 0; i < count; i ++)
    {
        InventoryModel *model = [InventoryModel mj_objectWithKeyValues:JSONResponseObject[i]];
        [modelsArray addObject:model];
    }
    return modelsArray;
}

 

InventoryViewController

- (void)requestInformation
{
    [api startWithBlockSuccess:^(__kindof YXYBaseRequest *request) {
        // request.responseObject 將會返回modelsArray
    } failure:^(__kindof YXYBaseRequest *request, NSError *error) {

    }];
}

 

本地資料處理

本地資料有多種方式儲存,比較常見的做法是使用.plist檔案儲存非常簡單的資料,例如設定,而會使用SQLite資料庫來儲存其他複雜的資料。另一方面,可以試著使用Core Data來對儲存資料model,雖然Core Data會帶來更多問題,甚至會影響效能,但它的NSFetchResultsController、懶載入、資料處理工具等也是十分的好用,所以……看自己。

在本地資料處理中,最重要的是如何獲取和修改資料。在我現在需要重構的專案中並沒有太多的本地內容,絕大部份的資料都是通過網路獲取,所以我並沒有準備詳細講對Data Model的重構與規範化處理。但基本的原則是對每個model配備一個存取器,裡面可以提供fetchALl、fetchAllUsingPredicate、createInstance、save等操作,每個model都可以使用這些操作來獲取資料,而這些操作的邏輯隱藏起來,在呼叫的時候我不需要知道我這個model究竟是存放在資料庫中,還是.plist檔案中,還是在快取中。我只需要知道當我呼叫這些方法的時候,我可以獲取到我想要的model,並可以取出我想要的資料。

而這篇博文裡面詳細講到了SQLite、Core Data、FMDB,想要了解資料庫儲存方面內容的朋友可以看一下。

業務邏輯處理

在model中不只是能夠處理資料的儲存,還可以對業務邏輯進行處理。在我需要重構的專案中,無論是強弱業務邏輯都有散落在vc的情況,導致了vc十分臃腫,如下面的程式碼中,就是需要實現從DateModel中取出一個時間,將NSDate格式的時間轉換到NSString,並在View中展示出來的情況:

DateModel.h

@interface DateModel : BaseModel

@property (copy, nonatomic) NSDate *currentDate;

@end

 

DateModel.m

- (instantcetype)init
{
if (self = [super init])
currentDate = [NSDate date];
return self;
}

 

aViewController.m

- (void)showTheDate
{
DateModel *dateModel = [DateModel new];
NSDate *date = dateModel.currentDate;
NSDateFormatter *format = [[NSDateFormatter alloc] init];
format.dateFormat = @"yyyy年MM月dd號 HH:mm:ss";
NSString *string = [format stringFromDate:date];
self.dateLabel.text = string;
}

事實上這段程式碼完全可以分開放置在model層中,讓整個vc的程式碼更加清晰,如:

NSDate+dateTransform.h

@interface NSDate (dateTransform)

+ (NSString *)transformStringFromDate:(NSDate *)date;

@end

 

NSDate+dateTransform.m

+ (NSString *)transformStringFromDate:(NSDate *)date
{
NSDateFormatter *format = [[NSDateFormatter alloc] init];
format.dateFormat = @"yyyy年MM月dd號 HH:mm:ss";
NSString *string = [format stringFromDate:date];
return string;
}

 

建立一個NSDate的分類並將轉換的程式碼放到其中,並由DateModel呼叫他來完成轉換

DateModel.h

@interface DateModel : BaseModel

@property (copy, nonatomic) NSString *currentDate;

@end

 

DateModel.m

- (instantcetype)init
{
if (self = [super init])
{
currentDate = [NSDate transformStringFromDate:[NSDate date]];
}
return self;
}

 

然後在vc中,我們只需要簡單的呼叫:

aViewController.m

- (void)showTheDate
{
DateModel *dateModel = [DateModel new];
self.dateLabel.text = dateModel.currentDate;
}

 

這樣整個vc的程式碼結構就清晰了很多,model層取出的屬性也能直接交付到view層去顯示,而日期轉換這類程式碼也能輕易的被複用。總之,針對這類的model,可以把呼叫方法儘可能的抽象,當需要解耦這部分程式碼的時候可以應用IOC模式,否則,在日後改變這些邏輯會變得十分困難。

結論  ios開發稽核交流群 869685378 歡迎各位大牛來分享交流 IOS,馬甲包,低要求,內容開發沒有限制,報酬豐厚,實力誠信 Q:782675105 

iOS的app中通常很少考慮model層的設計功能,而然,由於各種原因,比如各種散落的程式碼或者是容易出錯的網路連線,讓問題變得十分複雜。所以為了避免這些問題,model層的設計必須嚴格遵循這些原則,比如是把處理資料的程式碼獨立開來、設計一個給本地資料的存取器、設計一個給網路資料的存取器等等。但不管我們怎麼設計這個model層,最重要一點是這些功能的實現要完全對呼叫者隱藏起來,而且這些功能的實現要足夠簡單,以便將來對其進行修改與更新。

總而言之,在開發一段時間後很可能會更換網路庫、業務邏輯、資料庫工具等等,良好的Model層設計能讓你不用修改任何一行controller層和view層的程式碼,就完成了底層庫的更新換代。

相關文章