如何優雅地解決 Objective-C 不支持方法默認參數的問題

背景

平常開發中必定會遇到這種場景,在某個類中提供了這樣一個方法:優化

@interface TTDoSomething:NSObject
+ (void)doSomethingWithName:(NSString *)name configA:(ConfigA)configA configB:(ConfigB)configB configC:(ConfigC)configC;
@end
複製代碼

這個方法經過四個參數name, configA, configB, configB提供了 doSomeThing的功能,但是這個方法的參數實在是有點多,有的時候我只須要一個默認 config 就行了,並非每一個 config 都須要傳入一個值,這時候能夠把這個多參數的方法做爲全能方法,而後以它爲基礎實現幾個便利方法:ui

@interface TTDoSomething:NSObject
+ (void)doSomethingWithName:(NSString *)name configA:(ConfigA)configA;
+ (void)doSomethingWithName:(NSString *)name configB:(ConfigB)configB;
+ (void)doSomethingWithName:(NSString *)name configC:(ConfigC)configC;
+ (void)doSomethingWithName:(NSString *)name configA:(ConfigA)configA configB:(ConfigB)configB configC:(ConfigC)configC;
@end

@implementation TTDoSomething

+ (void)doSomethingWithName:(NSString *)name configA:(ConfigA)configA {
    [self doSomethingWithName:name configA:configA configB:nil configC:nil];
}

+ (void)doSomethingWithName:(NSString *)name configB:(ConfigB)configB {
    [self doSomethingWithName:name configA:nil configB:configB configC:nil];
}

...

@end
複製代碼

可是這麼作的缺點也顯而易見:atom

  1. 組合不靈活,若是須要增長 2 個種 config 組合的便利方法,須要增長新方法;
  2. 擴展不便,若是全能方法增長了新的配置 configD 須要在現有便利方法的基礎上增長更多的便利方法;

鏈式表達式

今天忽然靈光一閃,想到能夠用鏈式表達式解決這個問題。在 iOS 開發領域由於 Masonry 這個庫的影響,鏈式表達式知名度已經很高,所以這篇文章再也不贅述其原理。直接進入主題: 首先聲明一個鏈式表達式的鏈釦類:spa

.h
@interface TTDoSomethingMaker:NSObject //這裏命名也參考 Masonry, 定義爲 Maker
// 聲明須要的 block 屬性,包括 Name 和 Config
@property (nonatomic, copy, readonly) TTDoSomethingMaker *(^name)(NSString *name);
@property (nonatomic, copy, readonly) TTDoSomethingMaker *(^configA)(ConfigA configA);
@property (nonatomic, copy, readonly) TTDoSomethingMaker *(^configB)(ConfigB configB);
@property (nonatomic, copy, readonly) TTDoSomethingMaker *(^configC)(ConfigC configC);

@end

.m
@interface TTDoSomethingMaker ()
// 在.m 中聲明保存 name 和 config 的屬性,這裏不須要暴露給外部,因此不須要聲明在 .h
@property(nonatomic, copy) NSString *nameValue;
@property(nonatomic, assign) ConfigA configAValue;
@property(nonatomic, assign) ConfigB configBValue;
@property(nonatomic, assign) ConfigC configCValue;

@end

@implementation TTDoSomethingMaker

// 實現全部的 block getter
- (NSString *)name {
    return ^TTDoSomethingMaker *(NSString *name) {
        self.nameValue = name;
     return self;
    };
}

- (ConfigA)configA {
    return ^TTDoSomethingMaker *(ConfigA configA) {
        self.configAValue = configA;
     return self;
    };
}

- (ConfigB)configB {
    return ^TTDoSomethingMaker *(ConfigB configB) {
        self.configBValue = configB;
     return self;
    };
}

- (ConfigC)configC {
    return ^TTDoSomethingMaker *(ConfigC configC) {
        self.configCValue = configC;
     return self;
    };
}

@end

複製代碼

而後在提供功能的 TTDoSomething 類中增長一個方法:code

+ (void)doSomething:(void(^)(TTDoSomethingMaker *maker))block {
    TTDoSomethingMaker *maker = TTDoSomethingMaker.new;
    block ?: block(maker);
    // todo
    [self doSomethingWithName:maker.nameValue configA:maker.configAValue configB:configBValue configC:configCValue]
}
複製代碼

因而當咱們這樣調用:對象

[TTDoSomething doSomething:^(TTDoSomethingMaker *maker) {
    maker.name(@"Demo").configA(ConfigAOne);
}];
複製代碼

就至關於調用了索引

[TTDoSomething doSomethingWithName:@"Demo" configA:ConfigAOne configB:0 configC:0];
複製代碼

若是此時想要將 ConfigB 配置爲 ConfigBOne, 只須要在原有的鏈式表達式後面加一個 .configB(ConfigBOne) 便可。 而若是想給萬能方法增長一個 ConfigD, 也只須要修改一下 doSomething:TTDoSomethingMaker 的實現,完美地遵循了開閉原則。開發

小優化一發

上面 doSomething: 的實現裏我加了一個 // todo 的註釋,在這個位置能夠作一些默認實現,好比說合法性判斷和默認值:get

+ (void)doSomething:(void(^)(TTDoSomethingMaker *maker))block {
    TTDoSomethingMaker *maker = TTDoSomethingMaker.new;
    !block ?: block(maker);
    NSAssert(maker.nameValue, @"Parameter 'name' should not be nil.");
    // 將 ConfigB 配置爲 ConfigBTwo
    if (maker.configBValue == 0) {
        maker.configBValue = ConfigBTwo;
    }
    [self doSomethingWithName:maker.nameValue configA:maker.configAValue configB:configBValue configC:configCValue]
}
複製代碼

另外,上面的 block getter 能夠封裝一個宏,減小重複性的代碼:編譯器

#define TTDoSomethingMakerProperty(TYPE, NAME) - (TTDoSomethingMaker *(^)(TYPE))NAME {\
return ^TTDoSomethingMaker *(TYPE NAME) {\
    self.NAME##Value = NAME;\
    return self;\
};\
}\

@implementation TTDoSomethingMaker

TTDoSomethingMakerProperty(NSString *, name)
TTDoSomethingMakerProperty(ConfigA, configA)
TTDoSomethingMakerProperty(ConfigB, configB)
TTDoSomethingMakerProperty(ConfigC, configC)

@end
複製代碼

總結

Masonry 利用鏈式表達式將繁瑣的 AutoLayout API 優化成了更表意、靈活、簡潔的語法。而鏈式表達式能夠隨意自由組合的特性,剛好也是 Objective-C 不支持方法默認參數的優雅解決方式,雖然和 Masonry 的使用場景不同,可是仔細想一想,這兩個場景都是對開閉原則的實現。

補充

這篇文章在寫完後次日,同事也和我提到了參數對象這個方案,當時和他進行了一番討論,最後結論仍是鏈式表達式更可取,原本犯懶不想補充在文章裏了,鑑於評論有同窗提到,我就再寫一點。

參數對象

參數對象就是說聲明一個新的類,裏面收攏了全部的方法參數,相似這樣:

@interface TTDoSomethingObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) ConfigA configA;
@property(nonatomic, assign) ConfigB configB;
@property(nonatomic, assign) ConfigC configC;
@end
複製代碼

而後新增一個方法:

- (void)doSomething:(TTDoSomethingObject *)object;
複製代碼

這樣確實在必定程度上解決了問題也遵循了開閉原則,可是鏈式表達式的方案能最大限度地利用 IDE 的提示/自動補全讓你少些不少代碼,先來看看上面的調用示例:

[TTDoSomething doSomething:^(TTDoSomethingMaker *maker) {
    maker.name(@"Demo").configA(ConfigAOne);
}];
複製代碼

上面的代碼中,除了 maker 這個 block 參數須要額外地手動敲一個 m, 其餘的代碼均可以經過 IDE 提示並補全,而若是使用參數對象,代碼會是這樣:

[TTDoSomething doSomething:({
    TTDoSomethingObject *object = TTDoSomethingObject.new;
    object.configA = ConfigAOne;
    object.name = @"Demo";
    object;
})];
複製代碼

爲了美觀我這裏用了 ({}) 的語法糖,能夠看出來要手動多寫不少代碼,在便利程度上孰優孰劣一目瞭然。 評論裏提到的方案是用 C struct 來作參數,這裏也舉個例子,我把上面的 Class 換成 C struct:

typedef struct TTDoSomethingObject {
    NSString *name;
    ConfigA configA;
    ...
}TTDoSomethingObject;
複製代碼

因而調用方式能夠變成:

[TTDoSomething doSomething:(TTDoSomethingObject){@"Demo", ConfigAOne, 0, 0}];
複製代碼

或者

//須要強轉否則會報錯
[TTDoSomething doSomething:(TTDoSomethingObject){.name = @"Demo", .configA = ...}];
複製代碼

再或者

TTDoSomethingObject object;
    object.name = @"Demo";
    object.configA = ConfigAOne;
    [TTDoSomething doSomething:object];
複製代碼

再再或者

[TTDoSomething doSomething:^(TTDoSomethingObject *object) {
    object->name = @"Demo";
    object->configA = ConfigAOne;
    ...
}];
複製代碼
  • 第一種寫法代碼簡潔了很多,可是並無解決參數默認值和參數值省略的問題,同時也須要本身手寫較多的代碼;
  • 第二種寫法相對比第一種要多寫一些代碼,由於輸入 .n 時編譯器沒法索引出 .name, 可是解決了省略參數的問題,然而仍是沒有默認參數值;
  • 第三種寫法和上面的 Class 方案區別不大;
  • 第四種的問題仍是同樣,須要多手寫代碼。

有人可能會以爲了,不就手動多寫一點代碼嗎,這也算缺點?對此我想說,這篇文章的出發點就是爲了解決方法參數過長帶來的不便利,也就是說個人初衷就是不想手動多寫代碼,就這個目的來講,手動多寫一個字母都是更不具優點的方案。綜上,我認爲除了改寫編譯器 or 更換語言,鏈式表達式仍是最優解。歡迎討論

相關文章
相關標籤/搜索