依存性の注入でクラス間を疎結合にしてRealmを使いやすくする

Realm
依存性の注入でクラス間を疎結合にしてRealmを使いやすくする

やりたいこと

知っていたけれども使ったことがなかった依存性の注入を試してみようと思います。英語ではDI(Dependency Injection)と呼ばれるものですね。

Google先生に尋ねると(私にとって)難しい話ばかりが出てきて、果たしてどこで使って良いものかがよくわからなくなるんですよね。それでも、使い所がわからないままではもったいない気がしていまして、使えそうなタイミングを伺っていたのです。

今回使えそうなタイミングを掴んだので、そのご紹介です。

今回のターゲット

次の2つのクラスに対して依存性の注入を適用してみます。

  • Realmにデータの保存や読み出しをするためのクラス
  • Realmのデータモデル(Objectクラスを継承して作るあれ)

なお、今回は話を簡単にするため、データの保存と読み出しだけに絞ります。
画面上で何らかのデータを入力し、その入力されたデータをRealmデータベースに保存し、保存したデータをそのまま全部読み出す流れを考えます。

依存性の注入をする前

おおまかな流れ

入力されたデータでRealmのデータモデルクラスのインスタンスを作り、それをRealmにデータの保存や読み出しをするためのクラスに渡して保存処理を行います。

続いて、読み出すデータモデルを示してRealmデータベースから保存されているデータをすべて読みだします。

コードはどうなっているか

言葉で示すよりコードで示したほうがわかりやすいですかね。

データモデルはRealmのObjectクラスを継承して作ります。ここでは投稿時刻と投稿されたメッセージを保存していくものを考えます。

class Message: Object {
    @objc dynamic var id:String = ""
    @objc dynamic var date:Date = Date()
    @objc dynamic var message:String = ""

    func convert(dictionary:Dictionary<String,Any>) {
      // DictionaryのデータをRealmのデータクラスインスタンスの各フィールドに代入する処理
    }

    func convert() -> Dictionary<String,Any> {
      // RealmのデータクラスをDictionaryのデータに変換する処理
    }
}

convertメソッドが2つ入っていますが、私の趣味です。Realm公式のサンプルコードのようにフィールド1つずつに値を入れていっても良いのですが、せっかくなのでDictionaryデータを渡してさっとデータの代入してくれるようにしようと考えました。
逆方向も同様です。

ちなみにフィールド名とその中身のセットという意味合いでDictionaryデータにしていますが、このあたりはお好みで。

データの保存と読み出しをする処理はRealmの公式ドキュメントを拝借しつつで作りました。

class RealmDB:NSObject {
  func save(data:Dictionary<String, Any>) {
      let realmData = Message()
      realmData.convert(dictionary: data)

      let realm = try! Realm()

      try! realm.write {
          realm.add(realmData)
      }
  }

  func load() -> [Dictionary<String, Any>] {
      let realm = try! Realm()

      let messages = realm.objects(Message.self)

      var result = [Dictionary<String, Any>]()
      for message in messages {
          result.append(message.convert())
      }

      return result
  }
}

この方法で不便なこと

なんといっても、

  • Realmにデータの保存や読み出しをするためのクラス
  • Realmのデータモデル(Objectクラスを継承して作るあれ)

がセットになってしまっていて、データモデルが変わる都度、Realmにデータの保存や読み出しをするためのクラスもそれに見合ったものを用意しないといけないことです。

今回のようにシンプルなデータモデルで、かつRealmを使うのが今回限りならば、さして問題はないのでしょうが、実際はそんなことはないはずです。

似たようなことを何度も書くのはできることならば避けていきたい。似たようなことを書くとなると、コピーアンドペーストという禁断の黒魔術(大袈裟)に手を出してしまう事になり、後々自分で自分の首を締めることになります。

また、セットになってしまっているということは、つまり両者が密結合になってしまっていることを意味します。ですので、動作確認のテストをするために両方のクラスが必要になってしまうことになります。このままでは「はて単体テストってなんだったっけ?」という話になってしまいます。

依存性の注入を試してみる

大まかな変更の方針

密結合になってしまっている2つのクラスをなんとかして疎にしてあげます。疎ーシャルディスタンス。

Realmにデータの保存と読み出しをするクラスに対して明示的にデータモデルを示すアプローチで行ってみます。

コードはどうなるか

抽象的なデータモデルを作ってみる

まずは、具体的な内容を持つデータモデルから少し離れて、抽象的なデータモデルを考えます。

class RealmDataStructure:Object {
  func convert(dictionary:Dictionary<String,Any>) {    
  }

  func convert() -> Dictionary<String,Any> {
    return Dictionary<String,Any>()
  }
}

フィールドは無く、メソッドの中身はほぼ何もしていないも同然のスッカスカのクラスです。

スッカスカデータモデルを使う

このスカッスカデータモデルをRealmにデータの保存と読み出しをするクラスで使うようにします。

class RealmDB:NSObject {
  func save(data:Dictionary<String, Any>) {
      let realmData = RealmDataStructure()
      realmData.convert(dictionary: data)

      let realm = try! Realm()

      try! realm.write {
          realm.add(realmData)
      }
  }

  func load() -> [Dictionary<String, Any>] {
      let realm = try! Realm()

      let messages = realm.objects(RealmDataStructure.self)

      var result = [Dictionary<String, Any>]()
      for message in messages {
          result.append(message.convert())
      }

      return result
  }
}

何やってるのかイマイチ分からないと思いますが、もう少し我慢を。

データモデルのポリモーフィズム化

ここでRealmDataStructureクラスとMessageクラスを見比べてみると良い感じに似通っています。まぁ意図して似せたのですが……

これがの、こうじゃ!

protocol RealmDataStructure:Object {
  func convert(dictionary:Dictionary<String,Any>)
  func convert() -> Dictionary<String,Any>
}
class Message: Object,RealmDataStructure {
    @objc dynamic var id:String = ""
    @objc dynamic var date:Date = Date()
    @objc dynamic var message:String = ""

    func convert(dictionary:Dictionary<String,Any>) {
      // DictionaryのデータをRealmのデータクラスインスタンスの各フィールドに代入する処理
    }

    func convert() -> Dictionary<String,Any> {
      // RealmのデータクラスをDictionaryのデータに変換する処理
    }
}

RealmのデータモデルはRealmDataStructureプロトコルに準拠することとしました。いわゆるポリモーフィズムですね。

このルールに基づいてデータモデルを用意しておくと、Realmにデータの保存と読み出しをするためのクラスで汎用的に使うことができる道筋がついたことになります。

あともう一息

もう一息です。今のままでは、データモデルのフィールドが使えません。

ここでジェネリクスの出番です。
今やりたいことは、

RealmDataStructureプロトコルに準拠した任意のデータモデルを、Realmに対してデータの保存と読み出しをするクラスで使いたい

です。

この目的を達成するために、RealmDataStructureプロトコルに準拠した或るクラスTを使うことをジェネリクスを使って明示してあげます。

class RealmDB:NSObject {
  func save<T:RealmDataStructure>(data:Dictionary<String, Any>, structure:T) {
      let realmData = structure
      realmData.convert(dictionary: data)

      let realm = try! Realm()

      try! realm.write {
          realm.add(realmData)
      }
  }

  func load<T:RealmDataStructure>(structure:T) -> [Dictionary<String, Any>] {
      let realm = try! Realm()

      let messages = realm.objects(T.self)

      var result = [Dictionary<String, Any>]()
      for message in messages {
          result.append(message.convert())
      }

      return result
  }
}

Realmのデータベースに保存したり読み出したりするときには、何のデータモデルを使うかを引数に渡してやればOKです。

どう変わったか

大きく変わった点はRealmにデータを保存したり読み出したりするRealmDBクラスから特定の具体的なデータモデルを示すものがなくなりました。

代わりにRealmDataStructureプロトコルとひっついたといえるかもしれませんが、具体的なデータモデルと切り離しができたことで単体テストが遥かにやりやすくなりました。RealmDataStructureプロトコルに準拠してさえいれば何だって良いのですから。

また、結合が疎になったことで、RealmDataStructureプロトコルに準拠してさえいればデータモデルの内容が変わったとしても、Realmにデータを保存したり読み出したりするRealmDBクラスの中身まで影響が及びにくくなくなりました。

まとめ

長年頭の片隅で気になっていたもの手がつけられなかった依存性の注入を、ようやく試すことができました。見慣れていないせいでしょうかコードの流れに多少の違和感がありますが、慣れるまでの辛抱です。これまでよりもメンテナンスや改良がやりやすくなりそうです。

ちなみに、今回は試行なのでユルユルですが、プロトコルの実装不備の対策などのもろもろの詰めは実用上は必須ですね。