Edited at

swiftでAbstract Classを表現しようとして、protocol extensionで実現できないかと検討したが断念した

More than 1 year has passed since last update.


目的

いくつかのclassが複数の同じ変数を持っていて(例えば、createdAt、updatedAt等)、

また、その変数を利用した同様の(ちょっとだけ違う)メソッドを持っていて、

これらの変数やメソッドを引き上げたいな、というのが目的でした。


目的の理由


  • 同様の処理がコピペに違い状態で、各classに分散していると、変更を複数箇所に適用しなければならないから

  • 変更に弱いというか、記述漏れも発生しそうで怖いから

  • 同じ処理を書くのが面倒だから


環境


  • swift 4.0

  • Xcode9.1

  • Realm 3.0.2


元々の状態(before/after のbefore)


RealmObjectA.swift

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)
}
}



RealmObjectB.swift

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)


RealmStoredObject.swift

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)
}
}



RealmObjectA.swift

// 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
}
}



RealmObjectB.swift

// 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の問題点


  1. 親クラスであるrealmStoredObjectをインスタンス化できてしまう

  2. よく分からないけど、swiftっぽくないのでは?と思えた


1. 親クラスであるrealmStoredObjectをインスタンス化できてしまう について

realmStoredObjectはあくまで、realmに格納されるclassの共通部分だけを集約した抽象的なclassであり、インスタンス化させるものではない。本当ならインスタンス化を禁止したく


RealmStoredObject.swift

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に書いてみようと思います。


RealmStoredProtocol.swift

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はどうなるでしょうか。


RealmObjectA.swift

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)で定義する必要はありません。


SomeProtocol.swift

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を呼び、処理を追加、みたいなことをしたいのですが…)


今回は素直に継承でよいのでは…?

結局良い方法にたどり着けず、


  • ストアドプロパティの実装を共有したい

  • サブクラス側でデフォルト実装に処理を足したい

自分の中では、この場合は継承の方が適しているのかな、という結論に終わりました。

「いやいや、何を言っとるんやお前は。こうすればええやん」というご指摘があれば、随時大募集中です。

少々歯切れの悪い状態で申し訳ないですが、ひとまず記事はここまでです。

まだまだ師走も始まったばかりですが、

みなさま素敵なクリスマスシーズンをお過ごしください。