musicLineアプリ開発日記

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

作曲のモデル設計

musicLineの作曲モデルを設計した話。

モデル設計
音符選択動作



はじめに

musicLineでは全体的な依存関係がわかりやすくなるようにマルチモジュールで開発しています。

モジュール構成

単一モジュールは保守が大変 Android開発では単一モジュールで、ModelもViewmodel、Viewも同じモジュールでした。そうなると、機能追加や修正を行うときは、膨大なファイル数から変更が必要な部分を探し出す必要があり大変でした。
さらに、単一モジュールだと依存が複雑になりやすく、不必要な依存関係で予期せぬ不具合が発生することもあります。


モジュールを役割ごとに分けることで、その役割で関係のないプロパティや関数を除外して、役割の目的に専念してコーディングできます。つまり、DDDでいうドメイン(問題解決する領域)に対して、モジュール分割により境界付けられたコンテキストを実現しています。


作曲の問題を解決する場合

例えばドメインが作曲の場合、Modelが作曲の問題を解決します。ただし、ModelはUI(ユーザーが操作する画面)を提供する機能はないので、ViewModelとViewで画面表示し、UIによりデータを操作します。

また、一括りに作曲の問題を解決するといっても様々な側面があります。

  • データの永続化
  • 音符の操作
  • 譜面の再生


側面毎に必要なデータ構造が異なるため、モジュールにより境界を作ります。

役割 モジュール
データの永続化 Domain
音符の操作 Composition
譜面の再生 Midi

ドメインの境界をモジュールで表現
(全Modelで共通事項はCommonへまとめる)



モデル設計

マルチモジュールでは様々なメリットがありますが、モジュール間を跨ぐ時に各々のデータ構造へ変換する作業が肝になります。各々のモジュールを深く理解して、変換できるようにモデル設計することが大切です。

今回はDomainCompositionの音符のデータ構造を例に設計を紹介します。


Domain

Domainのデータ構造

Domainはデータを保存する役割であり、継承しているモジュールのデータ構造を適切に復元できるように網羅性を考えます。また、余計なプロパティを保持せずシンプルな構造にすることで、保存データのサイズを小さくします。


Domainデータ構造の図解

NoteContainerは位置と幅、NoteBlockNoteNoteContainerからの相対位置を保持します。また、NoteBlockの個数により、NoteBlockNoteの幅が決定します。

全体のクラス図を表示

全体の構造としては曲 > トラック > フレーズ > 音符の階層となります。
トラックには音階があるトラックやドラムのためのトラック等種類があり、トラックの種類に応じてフレーズや音符の種類が異なります。


Composition

Compositionのデータ構造

データ構造の詳細を表示

音符の3要素

  • 座標
  • サイズ
  • 状態

は音符の種類ごとにクラスを作ります。

音符種別 座標 サイズ 状態
Vertex VertexCoord - -
RootNote RootNoteCoord RootNoteSize RootNoteState
TupletNote TupletNoteCoord TupletNoteSize BaseNoteState
ChordNote ChordNoteCoord ChordNoteSize NoteState

これにより、音符種別ごとに3要素の細かな制御ができるようになります。

音符を編集するためには、クラス同士が作用する仕組みが必要になり、Domainより複雑なデータ構造になります。
また、編集するための機能も追加で実装する必要があります。

  • レンダラー(画面表示)
  • 変更通知(画面更新)
  • 当たり判定(音符操作)


Compositionデータ構造の図解

音符の関係がDomainのような内包構造ではなく、RootNoteからChordNoteへ枝分かれしていくようなツリー構造になっています。

内包構造
ツリー構造

例えば、音符の移動と選択の挙動について考えてみます。

音符移動
音符選択

このようにツリー構造にすることで、親の音符が動いた時に子の音符を追従させることができます。選択に関しても、ツリー構造にすることで子のノードに影響を与えることができます。



おわりに

今回はmusicLineのDomainCompositionモジュールの音符データ構造を紹介しました。同じ音符データでも役割によってデータ構造がかなり違うことがわかりました。

また、モデル設計ではいかに抽象化して、処理を共通化できるかが腕の見せどころです。
例えば、フレーズの種類が3種類あるため、ScaleTrackDrumTrackの違いで、作成するクラスがトラック種別 x フレーズ種別の数だけ増えます。

トラックの違いで派生するクラス
2 (トラック種別) x 3 (フレーズ種別)


しかし、ScaleTrackDrumTrackも格納している要素がVertexBeatContainerの違いだけで、基本的な役割は変わりません。

基本的な構成は変わらない

うまく抽象化することで、ツールでの操作でもトラック種別を気にせずに共通化して操作することができます。このあたりのProtocolやGenericsを使って抽象化する方法はまたの機会に紹介したいと思います。