目的
いくつかのclassが複数の同じ変数を持っていて(例えば、createdAt、updatedAt等)、
また、その変数を利用した同様の(ちょっとだけ違う)メソッドを持っていて、
これらの変数やメソッドを引き上げたいな、というのが目的でした。
目的の理由
- 同様の処理がコピペに違い状態で、各classに分散していると、変更を複数箇所に適用しなければならないから
- 変更に弱いというか、記述漏れも発生しそうで怖いから
- 同じ処理を書くのが面倒だから
環境
- swift 4.0
- Xcode9.1
- Realm 3.0.2
元々の状態(before/after のbefore)
import RealmSwift
// realmに格納される propertyA というIntを保存するclass
class RealmObjectA: Object {
@objc dynamic var propertyA = 0
@objc dynamic var createdAt = Int(Date().timeIntervalSince1970)
@objc dynamic var updatedAt = Int(Date().timeIntervalSince1970)
@objc dynamic var deleteFlg = false
// propertyAとdeleteFlgを更新すると同時にupdatedAtを現時刻に更新
func setAttributes(propertyA:Int, deleteFlg:Bool = false){
self.propertyA = propertyA
self.deleteFlg = deleteFlg
self.updatedAt = Int(Date().timeIntervalSince1970)
}
}
import RealmSwift
// realmに格納される propertyB というStringを保存するclass
class RealmObjectB: Object {
@objc dynamic var propertyB = ""
@objc dynamic var createdAt = Int(Date().timeIntervalSince1970)
@objc dynamic var updatedAt = Int(Date().timeIntervalSince1970)
@objc dynamic var deleteFlg = false
// propertyBとdeleteFlgを更新すると同時にupdatedAtを現時刻に更新
func setAttributes(propertyB:String, deleteFlg:Bool = false){
self.propertyB = propertyB
self.deleteFlg = deleteFlg
self.updatedAt = Int(Date().timeIntervalSince1970)
}
}
このRealmObjectがA〜Jまで10個存在するような状態でした。
これを一度、同様の処理をスーパークラスへ持たせるように変更しました。
クラスの継承を利用した変更(before/after の after その1)
import RealmSwift
// realmに格納されるobjectの親クラス
class RealmStoredObject: Object {
@objc dynamic var createdAt = Int(Date().timeIntervalSince1970)
@objc dynamic var updatedAt = Int(Date().timeIntervalSince1970)
@objc dynamic var deleteFlg = false
func setAttributes(deleteFlg:Bool = false){
self.deleteFlg = deleteFlg
self.updatedAt = Int(Date().timeIntervalSince1970)
}
}
// realmに格納される propertyA というIntを保存するclass
class RealmObjectA: realmStoredObject {
@objc dynamic var propertyA = 0
func setAttributes(propertyA: Int, deleteFlg:Bool = false){
super.setAttributes(deleteFlg: deleteFlg)
self.propertyA = propertyA
}
}
// realmに格納される propertyB というStringを保存するclass
class RealmObjectB: realmStoredObject {
@objc dynamic var propertyB = ""
func setAttributes(propertyB: String, deleteFlg:Bool = false){
super.setAttributes(deleteFlg: deleteFlg)
self.propertyB = propertyB
}
}
realmObjectAとrealmObjectBがさっぱりしました。
ここにはsetAttributesというメソッドしか記述していませんが、
実際にはAPI連携のために各変数をjsonパラメータ化するメソッドなど
いくつものメソッドがあったので、変数が減ると一気にさっぱりしました。
この時はとても気分が良かったです。
変数引き上げの理由の再掲
- 同様の処理がコピペに違い状態で、各classに分散していると、変更を複数箇所に適用しなければならないから
- 変更に弱いというか、記述漏れも発生しそうで怖いから
- 同じ処理を書くのが面倒だから
after その1で解決出来たこと
- 冗長になる処理を一つのclassのみに集約でき、変更があってもそのclassを修正すればよい
- 記述漏れも発生しない(たとえば「realmObjectBでだけupdatedAtを更新してない!」が無い)
- 同じ処理を書く面倒から開放された
after その1の問題点
- 親クラスであるrealmStoredObjectをインスタンス化できてしまう
- よく分からないけど、swiftっぽくないのでは?と思えた
1. 親クラスであるrealmStoredObjectをインスタンス化できてしまう について
realmStoredObjectはあくまで、realmに格納されるclassの共通部分だけを集約した抽象的なclassであり、インスタンス化させるものではない。本当ならインスタンス化を禁止したく
abstract class RealmStoredObject: Object {
}
のようにしたい所なのですが、swiftにabstract classの概念はなく、抽象クラスは実現できません。
そのため、
let realmStoredObject = RealmStoredObject()
のように宣言するとインスタンス化できてしまい、最悪この本来存在しないobjectをrealmに格納してしまう、なんてことにも成り得るかもしれません。
2. よく分からないけど、swiftっぽくないのでは?と思えた について
そもそも何故swiftにはabstract classが用意されていないのでしょうか。
未熟な自分にはその理由を察することはできませんでしたが、
用意していないにも理由があり、
「swiftの哲学が抽象クラスを作らせたくない」ということなのでしょう。
なので、別の方法を模索してみることにしました。
protocol & protocol extensionでの実現妄想(before/after の after その2)
- 「classよりできるだけstruct」(今回はrealm objectを継承するため無理ですが)
- 「継承よりもprotocol」
swiftといえば、そんなイメージがなんとなくあります。
そのためスーパークラスによる抽象化ではなく、
振る舞いを定義できるprotocolと、
その振る舞いにデフォルトの動きを定義できるprotocol extensionで
実現できないかを検討してみることにしました。
変数引き上げの理由の再々掲
- 同様の処理がコピペに違い状態で、各classに分散していると、変更を複数箇所に適用しなければならないから
- 変更に弱いというか、記述漏れも発生しそうで怖いから
- 同じ処理を書くのが面倒だから
それでは、protocolでswiftyに書いてみようと思います。
import SwiftyJSON
protocol RealmStoredProtocol {
var createdAt :Int { get set }
var updatedAt :Int { get set }
var deleteFlg :Bool { get set }
func setAttributes(measuredOn: Date, deleteFlg:Bool)
}
extension RealmStoredProtocol {
mutating func setAttributes(deleteFlg:Bool = false){
self.deleteFlg = deleteFlg
self.createdAt = Int(Date().timeIntervalSince1970)
self.updatedAt = Int(Date().timeIntervalSince1970)
}
}
protocol と protocol extensionはこんな感じになりそうですね。
ではRealmObjectAはどうなるでしょうか。
class RealmObjectA: Object, RealmStoredProtocol {
@objc dynamic var propertyA = 0
@objc dynamic var createdAt = Int(Date().timeIntervalSince1970)
@objc dynamic var updatedAt = Int(Date().timeIntervalSince1970)
@objc dynamic var deleteFlg = false
func setAttributes(measuredOn: Date, propertyA :Int, deleteFlg:Bool = false) {
// ↓extension のデフォルト実装が呼べない…
setAttributes(measuredOn: measuredOn, deleteFlg: deleteFlg)
self.propertyA = propertyA
}
すでに発生した問題点
1. 変数は引き上げられないので、全部定義しなきゃだめ
protocolでデフォルトを定義したコンピューテッドプロパティ(値を保持せず、既に存在する値を引っ張ってくるプロパティ)は
準拠する側(今回のRealmObjectA)で定義する必要はありません。
protocol SomeProtocol {
var defaultNum :Int { get }
}
extension SomeProtocol {
// SomeProtocolに準拠するclassでは定義しなくてもdefaultNumが使える。
var defaultNum :Int {
return 1
}
}
ただし、今回はrealm objectであることもあり、ストアドプロパティ(値を保持するプロパティ)であるため、この方法は使えません。
とはいえ、protocolに準拠するためには、それらの変数を定義しないとコンパイルエラーになってくれるので、
定義漏れの心配は無いため、とりあえず良しとしましょう。
2. protocol extensionで実装したメソッドはoverrideできない
これが困っていて、今回のsetAttributesや、その他定義しているメソッドでは
共通しない変数によって追加の動作が存在しています。
(正確にはsetAttributesの場合は引数が変わるため、overrideではなくオーバーロードして、protocol extension側のsetAttributesを呼び、処理を追加、みたいなことをしたいのですが…)
今回は素直に継承でよいのでは…?
結局良い方法にたどり着けず、
- ストアドプロパティの実装を共有したい
- サブクラス側でデフォルト実装に処理を足したい
自分の中では、この場合は継承の方が適しているのかな、という結論に終わりました。
「いやいや、何を言っとるんやお前は。こうすればええやん」というご指摘があれば、随時大募集中です。
少々歯切れの悪い状態で申し訳ないですが、ひとまず記事はここまでです。
まだまだ師走も始まったばかりですが、
みなさま素敵なクリスマスシーズンをお過ごしください。