musicLineの作曲モデルを設計した話。
はじめに
musicLineでは全体的な依存関係がわかりやすくなるようにマルチモジュールで開発しています。
さらに、単一モジュールだと依存が複雑になりやすく、不必要な依存関係で予期せぬ不具合が発生することもあります。
モジュールを役割ごとに分けることで、その役割で関係のないプロパティや関数を除外して、役割の目的に専念してコーディングできます。つまり、DDDでいうドメイン
(問題解決する領域)に対して、モジュール分割により境界付けられたコンテキスト
を実現しています。
作曲の問題を解決する場合
例えばドメイン
が作曲の場合、Modelが作曲の問題を解決します。ただし、ModelはUI(ユーザーが操作する画面)を提供する機能はないので、ViewModelとViewで画面表示し、UIによりデータを操作します。
また、一括りに作曲の問題を解決するといっても様々な側面があります。
- データの永続化
- 音符の操作
- 譜面の再生
側面毎に必要なデータ構造が異なるため、モジュールにより境界を作ります。
役割 | モジュール |
---|---|
データの永続化 | Domain |
音符の操作 | Composition |
譜面の再生 | Midi |
モデル設計
マルチモジュールでは様々なメリットがありますが、モジュール間を跨ぐ時に各々のデータ構造へ変換する作業が肝になります。各々のモジュールを深く理解して、変換できるようにモデル設計することが大切です。
今回はDomain
とComposition
の音符のデータ構造を例に設計を紹介します。
Domain
Domain
はデータを保存する役割であり、継承しているモジュールのデータ構造を適切に復元できるように網羅性を考えます。また、余計なプロパティを保持せずシンプルな構造にすることで、保存データのサイズを小さくします。
NoteContainer
は位置と幅、NoteBlock
とNote
はNoteContainer
からの相対位置を保持します。また、NoteBlock
の個数により、NoteBlock
とNote
の幅が決定します。
全体のクラス図を表示
全体の構造としては曲 > トラック > フレーズ > 音符
の階層となります。
トラックには音階があるトラックやドラムのためのトラック等種類があり、トラックの種類に応じてフレーズや音符の種類が異なります。
Composition
データ構造の詳細を表示
音符の3要素
- 座標
- サイズ
- 状態
は音符の種類ごとにクラスを作ります。
音符種別 | 座標 | サイズ | 状態 |
---|---|---|---|
Vertex | VertexCoord | - | - |
RootNote | RootNoteCoord | RootNoteSize | RootNoteState |
TupletNote | TupletNoteCoord | TupletNoteSize | BaseNoteState |
ChordNote | ChordNoteCoord | ChordNoteSize | NoteState |
これにより、音符種別ごとに3要素の細かな制御ができるようになります。
音符を編集するためには、クラス同士が作用する仕組みが必要になり、Domain
より複雑なデータ構造になります。
また、編集するための機能も追加で実装する必要があります。
- レンダラー(画面表示)
- 変更通知(画面更新)
- 当たり判定(音符操作)
音符の関係がDomain
のような内包構造ではなく、RootNote
からChordNote
へ枝分かれしていくようなツリー構造になっています。
例えば、音符の移動と選択の挙動について考えてみます。
このようにツリー構造にすることで、親の音符が動いた時に子の音符を追従させることができます。選択に関しても、ツリー構造にすることで子のノードに影響を与えることができます。
おわりに
今回はmusicLineのDomain
とComposition
モジュールの音符データ構造を紹介しました。同じ音符データでも役割によってデータ構造がかなり違うことがわかりました。
また、モデル設計ではいかに抽象化して、処理を共通化できるかが腕の見せどころです。
例えば、フレーズの種類が3種類あるため、ScaleTrack
かDrumTrack
の違いで、作成するクラスがトラック種別 x フレーズ種別
の数だけ増えます。
しかし、ScaleTrack
もDrumTrack
も格納している要素がVertex
かBeatContainer
の違いだけで、基本的な役割は変わりません。
うまく抽象化することで、ツールでの操作でもトラック種別を気にせずに共通化して操作することができます。このあたりのProtocolやGenericsを使って抽象化する方法はまたの機会に紹介したいと思います。