SwiftでExtensionのプロパティの黒魔術感をなくす(追記アリ)
SwiftでExtensionに追加するプロパティの黒魔術感が異常。
クラス全体には関係ないけど、特定のextension内に閉じ込めたいpropertyが欲しくなることがあると思います。
しかし、Swiftではextensionローカルなプロパティを持とうとするとobjc_getAssociatedObject
を使うため、
急に黒魔術感が異常なほどに感じられるソースが出来上がります。
associatedObject
は設計で回避できますが、より簡潔にextensionにpropertyにアクセスしたいという場合に有効です。
Associated Objectをつかう
// Model.swift protocol Model { func load(id: String) } // Dog.swift class Dog { var name: String } // Dog+Model.swift fileprivate struct AssociatedKeys { static var disposeBagKey = "disposeBagKey" } extension Dog: Model { fileprivate(set) var disposeBag: DisposeBag { get { if let disposeBag = objc_getAssociatedObject(self, &AssociatedKeys.disposeBagKey) as? DisposeBag { return disposeBag } let initialValue = DisposeBag() objc_setAssociatedObject(self, &AssociatedKeys.disposeBagKey, initialValue, .OBJC_ASSOCIATION_RETAIN) return initialValue } set { objc_setAssociatedObject(self, &AssociatedKeys.disposeBagKey, newValue, .OBJC_ASSOCIATION_RETAIN) } } func load(id: String) -> Dog { api.load(id: id) .subscribe(onNext: { [weak self] model in self?.name = model.name }).disposed(by: disposeBag) } }
この実装がいけてるかは別としてこんな感じでdisposeBag
をfileprivate
でアクセスすることができます。
disposeBagをDogに持たなくて済むので近いところにデータを置くことができてハッピーです。
黒魔術にフタをする
ExtensionPropertyというものを用意します。またキーはenumで定義出来るといいので、RawRepresentable
かつ、RawValue == String
でkeyStringでアクセスすればいい感じにキーを取得できるようにしています。
// MARK: - ExtensionPropertyKey /// Extension内にセットするデータのキー public protocol ExtensionPropertyKey: RawRepresentable { var keyString: String { get } } // MARK: - ExtensionPropertyKey /// A default implementation for enum which is extend String public extension ExtensionPropertyKey where Self.RawValue == String { var keyString: String { return self.rawValue } } // MARK: - ExtensionProperty /// A strategy for manage extension local property. public protocol ExtensionProperty: class { func getProperty<K: ExtensionPropertyKey, V>(key: K, defaultValue: V) -> V func setProperty<K: ExtensionPropertyKey, V>(key: K, newValue: V) } // MARK: - ExtensionProperty public extension ExtensionProperty { public func getProperty<K: ExtensionPropertyKey, V>(key: K, initialValue: V) -> V { var keyString = key.keyString if let variable = objc_getAssociatedObject(self, &keyString) as? V { return variable } setProperty(key: key, newValue: initialValue) return initialValue } public func setProperty<K: ExtensionPropertyKey, V>(key: K, newValue: V) { objc_setAssociatedObject(self, key.keyString, newValue, .OBJC_ASSOCIATION_RETAIN) } }
実際につかってみる
// Dog+Model.swift fileprivate enum ExtensionPropertyKey: String, ExtensionPropertyKey { case disposeBag } extension Dog: Model { fileprivate(set) var disposeBag: DisposeBag { get { return getProperty(key: ExtensionPropertyKey.disposeBag, initialValue: DisposeBag()) } set { setProperty(key: ExtensionPropertyKey.disposeBag, newValue: newValue) } } }
こんな感じで、若干黒魔術感にフタをすることができました。
キーかぶりの可能性を排除する
現状ですと、disposeBag
が他の拡張で上書きされる可能性があります。また、型が違うキャストに失敗するのでバグを埋め込むことになります。そこで#file
, #line
の出番です
fileprivate enum ExtensionPropertyKey: String, ExtensionPropertyKey { case disposeBagKey var keyString: String { return [#file.description, String(#line), self.rawValue].joined(separator: "::") } }
こうすることで、ExtensionPropertyKey
が定義されているファイルのパスとソースの行番号が入るので絶対にかぶることはありません。
黒魔術感が復活した気がしますがこんな感じでextensionにpropertyを閉じ込められたので見通しも良くなりそうです。