重構時順便還的技術債

Peter Chang
SWAG
Published in
7 min readMar 17, 2019

一個iOS APP從2016至今已經快三年了,這段時間除了iOS系統本身的變動外,也多了新的語言Swift 加入,而且在這段期間內歷經各種大大小小因應營運的需求及急速功能變動,因此逐漸累積了不少技術債,為了面對未來更多的挑戰,償還歷史技術債是在所難免的,但是專案本身要進行大範圍重構是很困難的,畢竟產品已經在營運中是不等人的,因此我們的目標是縮限在有限的時間內達到有效的優化目的的重構,因此我們team根據目前的架構以及所遭遇的問題,綜合時間及範圍的掌握程度,選擇了幾個方向進行,其中一個就是APP中常用的user model。

問題評估

原本的程式大約如下:

@interface SWUserModel: MTLModel  <MTLJSONSerializing>
@property (copy ,nonatomic) NSString *identifier;
@property (copy ,nonatomic) NSString *username;
- (void)checkMembership:(void (^)(BOOL isMember))result;
@end

針對user model進行評估後,決定以下三個項目為目標
⁃ Objective-C to Swift
⁃ No Mantle
⁃ No API call

首先是APP本身早期是用Objective-C開發的,但是目前ject已經逐漸轉以Swift 為主要開發語言,雖然可以用Swift extension方式擴展,但為了可以充分發揮新的語言帶來的好處及便利,因此想要藉此機會轉換過去。
而Mantle則是用來將JSON轉成object用的套件,但是改用Swift後就有decodable可以直接取代了。
再來就是在資料model中有進行呼叫API的行為,這雖然是一個很方便(偷懶)的方式,但是不符合SRP原則,因此也一直是我們想要下手處理的目標。

解決方案

考量不影響既有架構下抽換掉舊的data model的方式,而且也有可能新舊架構要並存一段時間的情形,決定採用protocol方式把data model實體藏起來,用統一的介面來溝通,這樣外界存取時就完全不需要管拿到的model實體是誰,之後就算拔掉整個舊model或者以後再更換model時會受到影響就會相當低。

因此先用Objective-C方式整理出介面,之後重構完成後就算用Swift方式重新寫一次也花不了多少功夫。

@protocol UserProtocol <NSObject>
@property (copy ,nonatomic) NSString *identifier;
@property (copy ,nonatomic) NSString *username;
@end

因此新的Swift版本的User Model就會如下

@objcMembers class UserModel:NSObject ,Codable ,UserProtocol {
var identifier: String
var username: String?
private enum CodingKeys:String ,CodingKey {
case userID ="id"
case username
}
func encode(with aCoder: NSCoder) {
aCoder.encode(userID, forKey: "identifier")
aCoder.encode(username, forKey: "username")
}
required init?(from decoder:Decoder )throws {
let container = try decoder.container(keyedBy:CodingKeys.self)
self.userID =try container.decodeIfPresent (String.self, forKey: .userID)
self.username = try container.decodeIfPresent(String.self , forKey: .username) ?? ""
}
}

然後找了個module將原本使用SWUserModel部分全部以UserProtocol取代後,進行小部分的測試並確認無誤後,但是這時突然發覺還有個重要問題還沒有解決,就是APP本身為了暫存目前使用者資料,所以將整個model object存下來在本地端,如果不處理這部分的話,會造成既有的使用者在升級到新版時,因為與新格式不符而變成需要重新進行登入,更甚者可能發生crash的狀況,因此還是必須有一個舊轉新的升級動作,以避免影響使用者體驗。

因此另外寫了個CurrentUserArchiver class,用來處理存取本地端資料時的介面,當新舊user model進行載入本地端使用者物件時,如果是用舊的model時會進行轉換成新的,並將轉換的行為包裝起來。

@objcMembers class CurrentUserArchiver: NSObject, NSKeyedUnarchiverDelegate {
static let shared = CurrentUserArchiver()
private let userDefaults = CurrentUserDefaults()
func transfer(_ object: Any) -> Data {
return NSKeyedArchiver.archivedData(withRootObject: object)
}

func revert(from data: Data) -> Any? {
do {
let obj = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
if let obj = obj as? SWCurrentUserModel {
let user = CurrentUserModel.from(swCurrentUser: obj)
return user
}
return obj
}
catch let (err) {
debugPrint(err)
}
return nil
}

func unarchived() -> Any? {
guard let archivedData = userDefaults.currentUser else { return nil }
return revert(from: archivedData)
}

func archived(user: Any) {
var object = user
if let user = user as? SWCurrentUserModel {
object = CurrentUserModel.from(swCurrentUser: user) as Any
}

userDefaults.currentUser = transfer(object)
}
}

這樣之後就可以無痛從舊的轉換成新的了。

至於呼叫API的處理,以根據使用的需求來分析,所以選擇的處理方式是用Singleton的class,由需要的View Controller來跟Singleton請求,這樣也可以避免掉重複的API request。

結論

雖然沒有一一帶到所有細節,到此基本上已經達到原本預期目標,其實在整個過程中還會需要一併納入考量的像是流程優化,去除多餘的、不必要處理等等。而花了那麼多功夫,其實也只是還了小部分技術債,但是可以讓產品本身開始往正確的方向發展。

--

--