前言
貝聊的移動客戶端分別有家長端和老師端,一家公司裡同時維護多個業務上有關聯性的app這種情況其實很常見,例如一些提供 O2O 服務的公司,經常會分使用者端和商家端。這些客戶端雖然各自負責著一個業務環裡面的不同部分,看似不相關,但其實內在的設計、程式碼都有很多共同之處。
我們編寫程式碼時一條最重要的軍規是 DRY (Don`t Repeat Yourself),意思就是同樣或者相似的程式碼只寫一次,通過程式碼複用的技巧做成公用的元件。專案工期緊張時,其他的一些編碼守則都可以稍微變通一下,但唯獨 DRY 是絕對要遵守的。這樣做最大的好處是當發生需求變更、重構或者修復 bug 時,只要改動一處的程式碼就可以了。如果採用到處 copy 程式碼的方式,則需要在每一處引用到的地方做修改,很容易就會出現遺漏。並且時間一長,這些複製的程式碼很容易會漸行漸遠,衍生出許多不同的分支,維護難度呈指數級上升。稍有經驗的程式設計師應該都知道到處拷程式碼就是挖坑的開始,本文以一個較簡單的 UI 元件為例,介紹貝聊 iOS 組在設計可複用元件時的一點小技巧。
遇到的問題
貝聊的家長版和老師版針對的受眾不同,設計語言、配色等方面也有點不同,以最簡單的自定義提示框為例:
家長版:
老師版:
主要的不同點:
- 按鈕的圓角半徑 (cornerRadius)
- 按鈕的大小、位置 (frame)
- 文字的字號 (fontSize)
- 文字內容到提示框邊界的距離 (contentInsets)
- 其實之前連按鈕的顏色都不一樣,不過最近UI改版了
初看起來不同點很多,但仔細看其實只是一些設計上的元素有不同。事實上家長版和老師版的提示框其實底層用的都是同一套程式碼,這個彈框元件BLAlertController
是我們 iOS 組一個新入行的小夥寫的,很好地遵守了 DRY 原則,靈活性和程式碼質量都非常高。本文就用這個元件為例來說說,怎樣在多個 app 之間優雅地複用程式碼。
建立一個配置類
先來看看初始化方法,alertController
的命名是仿照系統的UIAlertController
,但是因為 UI 是高度可定製的,所以多加入了很多引數。
+ (instancetype)alertControllerWithTitle:(NSString *)title
message:(NSString *)message
buttonTextColor:(UIColor *)textColor
buttonBackgroundColor:(UIColor *)buttonBackgroundColor
cornerRadius:(CGFloat)cornerRadius
.... // 篇幅原因,點選回撥和其他配置項都省略,全部列出來的話超過二十項複製程式碼
這裡遇到的第一個問題就是引數列表過長,Objective-C 沒有預設引數也沒有方法過載,如果每次初始化都要填寫這一大堆引數,這樣的元件也未免太難用了。
其實 iOS SDK 的程式碼裡面就有很多優秀的設計模式的應用範例,遇到問題的時候參考一下,會有很多收穫。這裡遇到的問題主要是程式碼架構的問題,發散一下,發現 Foundation 框架的 NSURLSession
也是有很多可配置的屬性的。蘋果的工程師把這些可選引數專門構造成了一個NSURLSessionConfiguration
來管理這些可配置屬性。建立一個NSURLSession
時,需要傳入一個NSURLSessionConfiguration
來指定一些引數,而NSURLSessionConfiguration
的大部分屬性都是有預設值的,例如timeoutIntervalForRequest
。通過NSURLSessionConfiguration.defaultSessionConfiguration
方法可以建立一個預設的 configuration
,此時timeoutIntervalForRequest
的預設值是60,這個值能適用於大部分情況。如果有特殊的需求也可以自行調整。
我們在99%的情況下其實都只是想用預設樣式的彈框,這時建立一個可定製的,帶預設值的配置類就是很好的解決方法。
依葫蘆畫瓢,我們也建立一個BLAlertConfiguration
,定義大致如下:
@interface BLAlertConfiguration : NSObject <NSCopying> // 配置類實現了深拷貝
@property (nonatomic) UIColor *buttonTextColor;
@property (nonatomic) UIColor *buttonBackgroundColor;
@property (nonatomic) CGFloat cornerRadius;
// 預設的配置項
@property (class, nonatomic) BLAlertConfiguration *defaultConfiguration;
... //其他可配置項由於篇幅原因不一一列舉了
@end
@interface BLAlertController : UIViewController
- (instancetype)initWithTitle:(NSString *)title
message:(NSString *)message
configuration:(BLAlertConfiguration *)configuration;
- (instancetype)initWithTitle:(NSString *)title
message:(NSString *)message;
@end複製程式碼
BLAlertController
有兩個初始化方法,initWithTitle:message:
是個 convenience initializer,內部呼叫了 initWithTitle:message:configuration:
並把BLAlertConfiguration.defaultConfiguration
傳進去了。所以一般的使用就很簡單了,直接呼叫initWithTitle:message:
就好。
在不同的專案中設定不同的預設值
上面解決了引數列表過長的問題,但是還是沒有說明在兩個專案中怎麼設定不同的預設 UI 風格。答案其實呼之欲出,聰明的讀者應該已經想到了。
BLAlertConfiguration.defaultConfiguration
這個屬性是 Objective-C 新加的 class property 語法,用來打通 Swift 的類屬性。我們可以通過靜態變數和 getter setter,把 defaultConfiguration
變成一個可讀可寫的類屬性。
@implementation BLAlertConfiguration
static BLAlertConfiguration *defaultConfiguration;
+ (void)setDefaultAlertConfiguration:(BLAlertConfiguration *)configuration {
if (defaultConfiguration) { //只允許設定一次,有值的時候返回
return;
}
defaultConfiguration = [configuration copy]; // 通過拷貝物件,避免配置項後面被修改
}
+ (instancetype)defaultConfiguration {
NSAssert(defaultConfiguration, @"未設定 defaultConfiguration,應先呼叫 +[BLAlertConfiguration setDefaultAlertConfiguration:] 來進行初始化");
return defaultConfiguration;
}
@end複製程式碼
這樣只要在程式啟動的時候,例如在 AppDelegate 的application:didFinishLaunchingWithOptions:
回撥中設定一下 BLAlertConfiguration.defaultConfiguration
就可以了,在不同的專案中設定不同的預設值,就能達成不同的設計風格。
BLAlertConfiguration *configuration = [BLAlertConfiguration new];
configuration.buttonTextColor = [UIColor blackColor];
configuration.buttonBackgroundColor = [UIColor yellowColor];
configuration.cornerRadius = 4.0;
BLAlertConfiguration.defaultAlertConfiguration = configuration;複製程式碼
結語
本文作為系統的首篇和引子,內容相對簡單,但是很好地體現了 DRY 的精神。如果你很少接觸這類問題,這會是一個很好的開始。學會發現程式碼中的壞味道,思考改進的方法,保持專案整潔是提升架構設計能力的必經之路。這種程式碼複用的手法目前已經貫穿了我們整個公共程式碼庫,並且有很多變體,後面會陸續地介紹貝聊專案中其他關於程式碼複用方面的心得,敬請期待。