musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

シングルトンの活用方法(依存性の注入2)

今回もmusicLineの内部実装の話。(前回の続き)
Transformを末端のクラスNoteへ共有する方法(依存性の注入)において、シングルトンを用いた方法について考えてみました。

Transformの共有



はじめに

musicLineでは、画面上に音符を表示して、タップやスワイプにより音符を編集します。そのため、 音符位置をスクリーン座標系へ座標変換するTransformが必要です。

Transformの情報


前回の記事では、ルートModelで保持しているTransformを末端のNoteへ共有する際にはシングルトンを使用することでコードがスッキリするが、使用は最小限に抑えることが望ましいとまとめました。

Transformをシングルトンへ

詳細はこちら


シングルトンはどこからもでアクセスできて便利ですが、状態を複数持てないことや依存関係が分かりづらくなるデメリットがあります。
今回はシングルトンを利用することの問題について考え、改善案を提示していきたいと思います。




シングルトンの問題

シングルトンパターンは便利で簡単に使いがちですが、使い所が難しいデザインパターンとして知られています。

下手に使用すると不具合の温床になりかねないため、特徴を深く理解することが大切です。

シングルトンの問題として、

  • 状態を複数持てない
  • 依存関係が複雑になる

があります。


状態を複数持てない

Transformは作曲画面で1つの状態しかないですが、musicLineでは編集曲を変更するときに前回の保存したTransformを適用する仕様があります。曲を読み込む時にTransformの状態が複数(編集中の曲、読み込む曲)存在することになり、シングルトンだと一時的にTransformがどちらか一方の状態になります。

そのため、Transformはシングルトンではなく、もう少し工夫する必要があります。

EditManager編集中の管理クラス

Transformは状態が複数になる可能性があるため、シングルトンを解除し、代わりに編集中のインスタンスを管理するクラスEditManagerをシングルトンにします。Transformの状態は複数になる可能性がありますが、編集中の画面に表示するTransformは必ず1つの状態なるはずです。

インスタンスを共有する流れとしては、

  1. 曲変更時に、EditManagerで管理しているtransformを更新
  2. Noteの作成時にEditManagerからtransformを取得
  3. 座標変換時にNoteで保持しているtransformを使用

となります。

作成時のコンストラクタでTransformを保持することで編集状態に影響されず、インスタンス作成から破棄までのライフサイクルで必ず同じTransformを共有することができます。

なお、関数内で直接シングルトンにアクセスするとTransformのシングルトンを解除した意味がなくなり、複数の状態を表現できていないことになります。この状態だとTransformが複数ある時(曲を読み込む場合)に参照しているインスタンスが代わり、予期せぬインスタンスへのアクセスで不具合が発生する可能性があります。


状態を複数持てないことでの不具合例 Android開発では編集中の拍子情報をEditManagerで管理しており、曲を読み込む時に拍子情報を参照してマイグレート(保存バージョンを新しいバージョンへ変換する作業)をしてました。
しかし、曲読み込みの際はまだ編集中の拍子情報は更新されていないため、マイグレートで異なる拍子情報を参照し、保存データが変わってしまう不具合がありました。



依存関係が複雑になる

本来ならば、TransformはルートModelから末端Noteへクラスを跨ぐ必要がありますが、シングルトンはその上層から下層に情報を橋渡しする冗長さをなくすことができます。
さらに、シングルトンEditManagerのプロパティでインスタンスTransformを保持することで、シングルトンではないクラスも簡単に共有できます。

しかし、どこからでもアクセスできる便利さが(グローバル変数と同様)依存関係を複雑にします。ツリー構造(階層的なデータ構造)であれば、他ブランチのデータからの影響はなく、依存関係がわかりやすくなりますが、シングルトンに頼ると他ブランチからでもデータアクセスができるため、ツリー構造の利点が破壊されます。

データアクセスの自由度が高くなっても(シングルトンの利用)依存関係を最小限に留めるためには、共有するインスタンスTransformの使い方を限定することが有効的だと思います。 使い方を限定するとは、Transformを直接共有するのではなく、サービスクラスを作りTransformをパッケージして共有することです。

ServiceManagerサービスクラスを共有

Transformを直接共有すると、意図しない使い方ができてしまい、クラス間の影響を把握することが難しくなります。
例えば、Transformの共有は「頂点位置を算出する目的」を意図しているのに、Transformの状態を変更したり、プロパティ(スクロールの位置等)を単体で取得したりとやりたい放題できてしまいます。
シングルトンで共有する目的を明確にするためにも、サービスクラスを作成してTransformを利用する処理をメンバー関数で提供します。

役割を限定したサービスクラスを共有することでTransformの使い方を制限し、インスタンスを共有する目的をコードで説明できていることが望ましいです。


依存関係が複雑になることでの不具合例 Android開発ではEditManagerで編集曲editSongを共有したことが、依存関係を複雑にしました。
編集曲の情報を共有すれば、末端Noteからも編集曲の拍子やトラックの楽器等を簡単に取得できて便利になりました。しかしシングルトンに依存すると、上層から降ろすはずの依存関係について、不自然なところが気づかなくなり、実際は構造上の問題があるにも関わらず無理やり実装を完了したような状態になります。このような些細な歪みが積み重なることで、構造を破綻することになり、多くの不具合に繋がりました。




DIコンテナー

シングルトンの問題は共有インスタンスTransformをサービスクラスにパッケージして、シングルトンServiceManagerで管理することで解消しました。
今回は具体例としてTransformの場合を考えましたが、他にも同様のパターンがあると思われます。その際に、ServiceManagerのプロパティを増やすよりは、クラスとインスタンスを紐づけて管理する連想配列がいいでしょう。
ちなみに、このようなオブジェクトの依存関係を管理する役割をDIコンテナーと言います。また依存関係を解決することを依存性の注入 (DI)と言います。

DIコンテナー

あとDIコンテナーでサービスクラスを共有する利点として、末端Noteで同じサービスクラスを何度も作成しなくて済み、メモリを節約することができます。

シングルトンの代わりとしてDIコンテナーを利用する時に、なるべくシンプルな依存関係になるようにサービスクラスに限定して共有します。その際に、サービスクラスでもDIすると依存関係が複雑になり、依存解決する(インスタンス作成する)順番を考慮する必要性が出てきます。そのため、サービスクラスで他のサービスを使用する時はシンプルにコンストラクタ引数で渡すようにします。
musicLineでは編集曲が変わるタイミング(編集サイクル)でDIコンテナーを更新するようにしています。

SwiftでDIコンテナーを実装する記事はこちら




おわりに

今回の調査で、シングルトンパターンは正しく理解し、使いどきを見極めることが大切だとわかりました。

シングルトンの扱いに注意しないと、

  • 状態を複数持てず、異なるインスタンスを参照してしまう
  • 依存関係が複雑になり、思わぬ循環参照に陥ってしまう

と不具合に繋がります。

シングルトンパターンの性質を理解し、

  • 状態が複数になり得ない状況に限定して管理する
  • 使用用途を限定したサービスクラスで共有する

と対策を取ることで不具合を事前に防ぐことができます。

ちなみに、アプリ内で必ず状態を1つにしたい場合はシングルトンの使いどきです。musicLineでは、アカウント管理(ログイン状況等の管理)や音楽プレイヤー(再生曲等の管理)に対してシングルトンが向いていると思います。