耳コピアプリの個人開発記録:ドキュメントドリブンで描くプロダクト設計
この記事は、Medley(メドレー) Advent Calendar 2024 の1日目の記事です。
はじめに
こんにちは、メドレーのナユ @nayucolony です。
メドレーでは、「Our Essentials」という形で大切にしている価値観を明文化しています。その中の一つに「ドキュメントドリブン」があります。
ドキュメントドリブン
私たちは、仕事を始める際にドキュメントを先に書き出し、それを駆動力として活用して自らやチームを効率的に動かします。ドキュメント化されていないことにより多くのメンバーが同じことを1から考えたり、読めばわかることを口頭伝承したりするような時間の浪費を予防します。ドキュメントもシンプルで無駄のないものを志向することで、将来のチームの生産性にも大きく貢献します。
私はもともとドキュメントに関心が強く、過去にはesaやnotionを活用して設計やアイデアをまとめることに多くの時間を割いてきました。メドレーでの新入社員研修の際、この「ドキュメントドリブン」の文化に触れたとき、大変バイブスが高まりました。
このブログでは、趣味で進めているプロダクト開発を題材にして、「ドキュメントドリブンなプロダクト開発」を実践することについて考えていきます。
目的とアプローチ
今回のプロダクト開発では、「ドキュメントを活用しながら進める」というテーマに取り組みます。
趣味のプロジェクトとはいえ、ドキュメントを通じて「なぜこのプロダクトを作るのか」「どのような価値を提供するのか」を深く考え、全体の構造を整理することが目的です。
さらに、ドキュメントは「設計の記録」以上の価値を持ちます。開発中に得られた知見や判断を記録することで、後から振り返りやすくなり、長期的な生産性の向上にもつながります。これを小さなプロジェクトで実践することを通じて、ドキュメントドリブンのメリットを具体的に示していきます。
この文章の目的
- 趣味のプロダクト開発を題材に、「ドキュメントドリブンなプロダクト開発」の実践例を記録する。
- 設計プロセスを通じて得られた知見を、視覚的な補足(図やテーブル)とともに共有する。
- ドキュメントを活用することで得られる「思考の整理」「意思決定の記録」の価値を具体的に示す。
この文章でしないこと
- 詳細な技術実装やコード全体の解説には踏み込まない。
- ドキュメントドリブンの普遍的な理論や哲学を深く掘り下げるのではなく、あくまで実践例に基づく。
ドメインの理解と設計
プロダクト開発において、対象とするドメイン(領域)を深く理解することは極めて重要です。今回の趣味プロジェクトでは、自分自身のベース練習を支援するアプリを開発しています。このプロジェクトでは、ドメイン駆動設計(DDD: Domain-Driven Design) を取り入れ、設計を進めました。
ドメイン駆動設計の視点
DDDでは、ソフトウェア設計の中心に「ドメイン」を据え、その専門知識や構造を反映させることを目指します。特に重要なのが、以下の2つの概念です:
ドメインエキスパート
ドメインに関する深い知識を持つ人を指します。通常は実際の業務の専門家や、分野に精通したユーザーが該当します。今回のプロジェクトでは、自分自身がユーザーかつドメインエキスパートとして振る舞いました。
ユビキタス言語
プロジェクトに関わるメンバー全員が共通して理解できる言葉の集合です。これをコードやモデルに直接反映させることで、コミュニケーションロスを防ぎ、設計の一貫性を保つ役割を果たします。
自分自身がドメインエキスパートとしての役割を果たす
今回のプロジェクトでは、私自身がドメインエキスパートとして耳コピの経験を振り返り、設計に反映しました。とはいえ、当初は十分な知識があったわけではありません。次のような方法で知識を補強しました:
動画や書籍からの学び
音楽理論や耳コピのコツについて、YouTubeでの解説動画や音楽理論の書籍を通じて学び直しました。
実践による理解の深化
実際に自分の好きな楽曲を耳コピし、得た知識を活用しながら試行錯誤を繰り返しました。
耳コピにおける課題の特定
自分自身の耳コピの経験を振り返る中で、特に次のような課題が顕著であると感じました:
音程の正確な聞き取りが難しい
楽曲の中で鳴っている音を正確に聞き取る作業は非常に困難です。
指板上の音の配置に不慣れ
聞き取った音を再現する際に、どのフレットを押さえればよいか直感的に分からないことが多くありました。
効率的な反復練習の手段が不足している
練習したい箇所を正確に設定し、繰り返し再生する作業が煩雑でした。
設計に反映した課題の解決策
上記の課題を解決するために、次の機能をアプリケーションのコアとして設計しました
キーとスケールの解析
鳴っている音を記録し、楽曲のキーやスケールを特定。これにより、楽曲全体の音の配置を把握しやすくします。
指板へのスケールの対応
特定されたスケールをベースの指板上に可視化することで、どのフレットを押さえればよいかを直感的に理解できるようにします。
補足: 動画の再生や反復再生機能についても重要な要件ではありますが、この記事ではこれらには触れず、スケール解析と指板表示に焦点を当てて解説を進めます。
ユビキタス言語の設定
今回のプロジェクトでは、以下のようなユビキタス言語を設定しました:
用語 | 定義 | ユースケースでの使用例 | 備考 |
---|---|---|---|
指板データ | ベースの弦とフレットに基づく音名の情報 | 「指板データを生成して、スケール解析結果を可視化するために使用」 | スケール解析コンテキストで生成される |
スケール | 音階の集合。例: Cメジャースケール、Aマイナースケール | 「ユーザーがスケールを選択し、指板データに反映」 | 初心者向けのUIで説明を追加するべき |
ループ再生 | 特定セクションを繰り返し再生する機能 | 「練習したい箇所を指定してループ再生を設定」 | シンプルなインターフェースが求められる |
トニック音 | キーを構成する基本音(例: CメジャーのC) | 「トニック音を基準に構成音をハイライトする」 | - |
ポジション | 指板上の特定の弦とフレットを指す情報 | 「スケールに基づき、押さえるべきポジションをハイライト」 | - |
ユビキタス言語は、設計段階で曖昧さを排除するための重要なツールです。これをプロジェクト全体で統一することで、コードやモデルにおける表現が一貫し、設計の精度が高まりました。
モデリング
今回のアプリケーションでは、スケールとキーの解析および指板表示の2つのコア機能を中心にモデリングを行いました。以下はその結果を示した図です。
DDDの視点での説明
Fretboard(指板): エンティティ
Fretboardは、チューニング情報(例: EADG)とポジション情報(各弦・フレットの音)を保持するバリューオブジェクトです。同じチューニングとスケール適用結果から生成されたFretboardは同一と見なされます。
また、生成ロジックはFactory(後述)に委譲され、Fretboard自体には状態変更を伴う振る舞いを持たせていません。
ScaleAnalyzer(スケール解析機): ドメインサービス
ScaleAnalyzerは、入力された音階データを解析し、スケールとキーを特定する責務を持つドメインサービスです。この処理は、複数の音階データを操作するため、個々のオブジェクトではなく、ドメインサービスとして切り出されています。
Scale(スケール): バリューオブジェクト
スケールは、スケール名(例: Cメジャースケール、Aマイナースケール)、スケールタイプ(例: メジャー、マイナー)、構成音を属性として持つバリューオブジェクトです。
スケールの同一性は、スケール名、スケールタイプ、構成音の組み合わせで判断されます。これにより、音楽的な文脈に基づいた設計が可能になります。
Position(ポジション): バリューオブジェクト
Positionは、弦番号、フレット番号、音名を属性として持つバリューオブジェクトです。これらの属性が同じであれば、異なる指板間でも同一と見なされます。PositionはFretboardのデータ構造を構成する基本的な単位です。
アーキテクチャの設計
今回のアプリケーションでは、キーとスケールの解析を全自動で行うのではなく、ユーザー自身が「鳴っていると思う音」を手動で記録する形を採用しています。これにより、楽曲のキーを特定するプロセスそのものが学習の一環となります。
この方針に基づき、以下のようなアーキテクチャを設計しました。
アーキテクチャの全体像
アーキテクチャは、以下の4層構造に基づいています。
各層の責務は以下の通りです:
各層の責務
プレゼンテーション層(ユーザーインターフェース)
-
責務:
- ユーザー操作を受け取り、解析結果や指板を表示する。
-
具体例:
- 「音記録パネル」や「指板表示」などのコンポーネントを提供する。
アプリケーション層
-
責務:
- ドメイン層のロジックを利用し、ユースケースを統括する。
- ユーザーの入力を受け取り、スケール解析や指板生成を調整し、結果をUI層に提供する。
ドメイン層
-
責務:
- ビジネスロジックを管理。
- スケール解析や指板生成といったコアな処理を提供する。
インフラストラクチャ層
-
責務:
- 外部依存(データベースや外部ライブラリなど)を抽象化。
- データ取得や保存の実装を担う。
スケール解析と指板生成のフロー
ユーザーが「この音が鳴っている」と思った音を記録し、その音階のリストからスケールを特定し、指板に反映するフローを以下に示します。
視覚的補足とコンポーネント設計
ドキュメントドリブンの実践では、視覚的な補足が重要です。今回のプロジェクトでは、アプリケーション設計を視覚的に示すためにマーメイド図を活用しました。特にUIコンポーネントの設計については、責務やデータフローを具体的に表現し、設計意図を明確にしています。
視覚補足の意義
視覚的な補足は、次のような場面で役立ちます:
- 設計意図の共有: 図や表があることで、設計意図やデータフローが明確になり、チーム内外での共有がスムーズになります。
- 実装の指針: コンポーネントやロジックの役割を把握しやすくなり、実装がスムーズになります。
今回は、コンポーネントの設計をマーメイド図で表現し、ユーザー操作やデータフローを具体化しました。
音記録パネルの設計
音記録パネルは、ユーザーが「この音が鳴っている」と思った音を手動で記録するためのコンポーネントです。このパネルは、記録された音データをアプリケーション層に送信し、解析されたスケール情報を取得して結果を表示します。
音記録パネルの責務
- 記録: ユーザーが記録した音を管理し、アプリケーション層に送信。
- 解析結果の表示: 解析結果を受け取り、次のアクションを促す。
- ステート管理: 記録された音と解析結果をローカルステートとして保持。
指板表示コンポーネントの設計
指板表示コンポーネントは、スケール解析結果を視覚化し、ユーザーに「どのフレットを押さえればよいか」を示します。指板データは外部から渡され、コンポーネント内で状態を持たない設計にしています。
指板表示コンポーネントの責務
- 表示: 指板上の各ポジションを描画し、構成音をハイライト。
- 非ステートフル: データの受け渡しに専念し、内部状態を持たない。
シーケンス図:音記録から指板表示まで
以下は、音記録パネルから指板表示に至るデータフローを示したシーケンス図です。
補足:視覚補足を活用するポイント
- 具体性を重視: マーメイド図では、コンポーネントの責務やプロパティ、メソッドを具体的に記載しています。これにより、設計意図を簡潔に伝えられます。
- 抽象化と具体化のバランス: 複雑な部分は詳細に記載し、補助的な部分は省略することで読みやすさを確保します。
具体的なコード例と実装の考察
ドキュメントで設計した内容を、実際のコードにどのように反映したのかを示します。このセクションでは、スケール解析と指板表示を中心に、具体的なコード例を紹介します。今回はすべてを記述するのではなく、構造やロジックの流れを重点的に解説します。
ディレクトリ構成
src/
├── application/
│ ├── services/
│ │ └── ScaleAnalysisService.ts # スケール解析と指板生成を調整
├── domain/
│ ├── services/
│ │ ├── ScaleAnalyzer.ts # スケール解析ロジック
│ │ └── FretboardFactory.ts # 指板データ生成
│ ├── value-objects/
│ │ ├── Position.ts # 指板のポジションデータ
│ │ ├── Scale.ts # スケールデータ
│ │ └── Note.ts # 音データ
└── presentation/
├── components/
│ ├── ScaleAnalysisPanel.tsx # 音記録用パネル
│ └── FretboardDisplay.tsx # 指板表示コンポーネント
└── routes/
└── scale-analysis.tsx # スケール解析のルート
※実際はRemixで実装しており、ディレクトリ構成は異なりますが、ごちゃっとしてしまうので簡略化しています。
スケール解析サービスの実装
アプリケーションサービス層では、記録された音階リストをもとにスケールを解析し、指板データを生成する役割を担います。
import { ScaleAnalyzer } from "../domain/services/ScaleAnalyzer";
import { FretboardFactory } from "../domain/services/FretboardFactory";
import { Position } from "../domain/value-objects/Position";
export class ScaleAnalysisService {
constructor(
private scaleAnalyzer: ScaleAnalyzer,
private fretboardFactory: FretboardFactory
) {}
/**
* 記録された音階リストからスケールを解析し、指板データを生成する
* @param notes 記録された音階リスト
* @param tuning ベースのチューニング情報
* @returns 指板データ(ポジションのリスト)
*/
public analyzeAndGenerateFretboard(
notes: string[],
tuning: string[]
): Position[] {
const scale = this.scaleAnalyzer.determineScale(notes);
return this.fretboardFactory.generatePositions(scale, tuning);
}
}
スケール解析機のロジック
スケール解析機は、記録された音階リストを基にスケールを特定します。このロジックでは、全音・半音のインターバルパターンを基に、メジャー、マイナー、モードスケールなどを解析可能にしました。
export class ScaleAnalyzer {
private static chromaticScale = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
];
private static scalePatterns = [
{ name: "Major", intervals: [2, 2, 1, 2, 2, 2, 1] },
{ name: "Minor", intervals: [2, 1, 2, 2, 1, 2, 2] },
{ name: "Dorian", intervals: [2, 1, 2, 2, 2, 1, 2] },
{ name: "Mixolydian", intervals: [2, 2, 1, 2, 2, 1, 2] },
{ name: "Harmonic Minor", intervals: [2, 1, 2, 2, 1, 3, 1] },
{ name: "Melodic Minor", intervals: [2, 1, 2, 2, 2, 2, 1] },
];
/**
* 記録された音階リストからスケールを特定
* @param notes 音階リスト
* @returns スケール名と信頼度
*/
public determineScale(notes: string[]): { name: string; confidence: number } {
const uniqueNotes = new Set(notes);
return ScaleAnalyzer.scalePatterns
.map((pattern) => {
const generatedScale = this.generateScale("C", pattern.intervals);
const matchCount = this.getMatchCount(uniqueNotes, generatedScale);
const confidence = (matchCount / generatedScale.length) * 100;
return { name: pattern.name, confidence };
})
.filter((result) => result.confidence > 0)
.sort((a, b) => b.confidence - a.confidence)[0];
}
private generateScale(root: string, intervals: number[]): string[] {
const scale = [];
const chromatic = ScaleAnalyzer.chromaticScale;
let index = chromatic.indexOf(root);
scale.push(root);
for (const interval of intervals) {
index = (index + interval) % chromatic.length;
scale.push(chromatic[index]);
}
return scale;
}
private getMatchCount(inputNotes: Set<string>, scaleNotes: string[]): number {
return Array.from(inputNotes).filter((note) => scaleNotes.includes(note)).length;
}
}
指板データ生成のロジック
指板データ生成は、スケール情報を基に各弦・フレットの構成音を計算し、適切なポジションデータを生成します。
import { Position } from "../value-objects/Position";
export class FretboardFactory {
private static chromaticScale = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
];
public generatePositions(scale: { name: string; notes: string[] }, tuning: string[]): Position[] {
const positions: Position[] = [];
tuning.forEach((openString, stringIndex) => {
for (let fret = 0; fret <= 24; fret++) {
const note = this.calculateNote(openString, fret);
if (scale.notes.includes(note)) {
positions.push(new Position(stringIndex + 1, fret, note));
}
}
});
return positions;
}
private calculateNote(openString: string, fret: number): string {
const chromatic = FretboardFactory.chromaticScale;
const startIndex = chromatic.indexOf(openString);
return chromatic[(startIndex + fret) % chromatic.length];
}
}
補足: ファクトリの役割
ファクトリ(Factory)は、特定のルールやロジックに基づいて複雑なオブジェクトを生成する役割を担います。本プロジェクトでは、FretboardFactory を設計し、以下のような利点をもたらしています。
なぜファクトリを使うのか
-
複雑な生成ロジックの隠蔽:
- 指板データ生成には、チューニング情報、スケール情報、弦とフレットの計算が関わります。これらの複雑性を外部に公開せず、簡潔なAPIを提供できます。
-
再利用性の向上:
- ファクトリが提供する生成メソッドは、他のユースケースでも再利用可能です。
-
単一責務の実現:
- ファクトリは生成のみを責務とし、他の層やロジックと分離することで、モジュールの独立性を高めます。
指板表示コンポーネントの統合
指板表示コンポーネントは、指板データを受け取り、視覚化します。
import React from "react";
import { Position } from "../domain/value-objects/Position";
interface FretboardDisplayProps {
tuning: string[];
positions: Position[];
}
export const FretboardDisplay: React.FC<FretboardDisplayProps> = ({ tuning, positions }) => {
return (
<div>
{positions.map((position) => (
<div key={`${position.string}-${position.fret}`}>
{`String: ${position.string}, Fret: ${position.fret}, Note: ${position.note}`}
</div>
))}
</div>
);
};
処理フロー図: スケール解析から指板表示まで
以下は、スケール解析から指板表示までの処理フローを示した図です。各ロジックの依存関係とデータフローを明確化しました。
図の説明
-
ユーザー操作から開始:
- ユーザーは音記録パネルに音階を記録。
-
アプリケーション層での処理:
- 音記録パネルがアプリケーションサービスに音データを送信し、スケール解析と指板生成を依頼。
-
ドメイン層の役割:
- スケール解析機が音階リストからスケールを特定。
- 指板ファクトリがスケール情報とチューニング情報を基に指板データを生成。
-
UI層での視覚化:
- 指板表示コンポーネントが生成された指板データを表示。
この設計の強み
-
明確な責務分離:
- 各コンポーネントがそれぞれの層の責務を明確に果たしており、モジュール間の依存関係が直感的に理解できる。
-
拡張性:
- 新たなスケールや指板表示方法を追加する際も、特定の層に変更を限定できます。
-
視覚的補足:
- 処理フローを視覚化することで、全体像の把握が容易になり、設計意図の共有がスムーズになります。
プロダクト開発とドキュメントの意義
今回の趣味プロジェクトでは、設計段階からドキュメントを重視しました。設計意図や判断を可視化することで、スムーズな開発を目指したこのアプローチについて振り返ります。
設計とドキュメントの連動
設計中にドキュメントを取り入れることで、以下の効果を実感しました:
-
設計意図の明確化:
- モデリングや視覚化を通じて、プロダクトの全体像が整理され、設計の意図を共有しやすくなりました。
-
効率的な修正と拡張:
- 設計プロセスが記録されていることで、変更や機能追加の際にも背景や意図を把握した上で進められました。
-
学びの促進:
- 自動解析を避けた設計方針を取る中で、音階やスケールについての学習がプロダクトそのものに反映され、設計を通じた理解が深まりました。
ドキュメントが生む価値
今回のプロジェクトを通じて、ドキュメントの意義を再認識しました:
-
設計の背景を記録する:
- 設計の理由や判断を記録することで、透明性が向上し、プロジェクトの持続性が強化されます。
-
「過去の自分」との対話:
- 趣味プロジェクトでも、過去の設計意図を簡単に振り返ることで、迷いなく作業を進められました。
-
視覚的な補足:
- マーメイド図や表を活用し、設計内容を簡潔かつ分かりやすく示すことができました。
終わりに
ドキュメントは設計の副産物ではなく、開発を支える核です。趣味プロジェクトのような小規模な取り組みでも、設計に対する考えを深めることで、プロダクトの質を向上させることが可能です。
今回得た経験を活かし、今後もドキュメントドリブンの手法を用いて、透明性と効率性の高いプロダクト開発を目指していきたいと思います。
おまけ:最終的にどんなアプリになったのか
全体像
デザインはDAWのような感じにしてみました。
パネルUIは配置が難しすぎますね。
YouTubeは地味にループ再生などもできます。
音の特定用ピアノUI
Web Audio APIを使って波形生成して音を鳴らすピアノUI。
これがあることで、動画を聴きながらPCのみで音の特定ができます。
1ミリも触れてませんが、シンセサイザーの容量で波形をいじることで音を鳴らす仕組みで、かなりコード量があります。
(これがブラウザでしか動かないということが盲点で、作り終わった後に全部SPAにしました。)
キー検出とスケール表示
今回ロジックで触れたキー検出とスケール表示のUIです。
音楽理論のインプットにとても時間がかかりました。
気が向いたらいつか公開したいと思います。
採用情報
メドレーでは、メドレーでは多様なスペシャリストを探しています。興味がある方はぜひお声かけください!
筆者宛のDMもカジュアルに受け付けています!
2日目の担当は@1plus4さんです。お楽しみに!
Discussion