15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift その2Advent Calendar 2017

Day 5

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

Last updated at Posted at 2017-12-04

目的

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

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

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

  • ストアドプロパティの実装を共有したい
  • サブクラス側でデフォルト実装に処理を足したい

自分の中では、この場合は継承の方が適しているのかな、という結論に終わりました。
「いやいや、何を言っとるんやお前は。こうすればええやん」というご指摘があれば、随時大募集中です。

少々歯切れの悪い状態で申し訳ないですが、ひとまず記事はここまでです。
まだまだ師走も始まったばかりですが、
みなさま素敵なクリスマスシーズンをお過ごしください。

15
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?