【JavaScript入門講座】 JavaScriptクラス設計入門:関数が増えてつらくなったら読む講座

はじめに

前回の記事の続きです。

内容が物足りなかったので続編をすぐ企画してもらいやってもらう事にしました。

という事で、「JavaScript入門講座 クラス設計 #02」です。

1. 設計は最初から完璧じゃなくていい

JavaScriptでクラスを導入したとき、最初は「とにかく動くようにする」ことが第一目標でした。

それはそれで正しいステップです。
でも、いざ使い続けていくと、こんなふうに感じることが出てきます。

  • 「このクラス、だんだん肥大化してきたな…」
  • 「一部の処理、他と密結合しすぎてない?」
  • 「一度分けたつもりが、結局また似たような関数が増えてきた…」

そう、設計というのは1回作って終わりではなく、使いながら“育てていく” ものだと、
実際の開発を通じて実感するようになりました。

「設計をやり直す」より「育てる」

クラス設計というと、「最初から完璧な構造を考えないといけない」と思いがちです。

でも実際には、

  • 実装しながら見えてくる使い勝手
  • 状態の持ち方への違和感
  • UIとの結びつきが強くなりすぎる問題

などが徐々に浮かび上がってきます。

そうなったときに、「失敗した」と思う必要はありません。
むしろそのときこそが、クラスを本当の意味で“育て始める”タイミングなんです。

この記事で伝えたいこと

この記事では、前回作成した SoundTest クラスを題材に、

  • メソッドの分離・再構成
  • UIと状態の責務分離
  • クラスの拡張性を高めるリファクタ術

などを、“育てる”視点から実例で紹介していきます。

2. クラス肥大化で感じる違和感

クラスを導入すると、状態と処理をひとまとめにできてスッキリします。
最初のうちは、「あぁ、クラスって便利だな」と思えるんです。

でも、機能をどんどん追加していくうちに、ふとした違和感が湧いてきました。

「あれ、このクラス大きすぎない?」

例えば SoundTest クラスは、最初は音の再生・停止・リスト生成といった、シンプルな責務で始まりました。
しかし、機能が増えてくると、こんな状況に陥りやすくなります:

  • 🎧 アニメーション制御(♬)が加わる
  • 📃 カテゴリ選択によるリスト更新処理が入る
  • 🔁 ランダム再生機能が追加される
  • 💬 「選択中:◯◯」の表示UI更新が組み込まれる

→ 「あれ?これ、UI処理とロジック処理が混ざってきてない?」
→ 「何でも SoundTest クラスに詰め込みすぎてない?」

変数が“クラスの中で迷子”になる

  • this.currentSelectedId がどのメソッドで書き換えられているか追うのが面倒
  • this.noteInterval がアニメーションなのに再生処理の近くにあって気持ち悪い
  • 「この関数、もうちょっと分けたいな…」と思っても、副作用(UIや音)が絡んでいて触りにくい

🧠 この違和感は、「クラスが責務オーバーになりかけている」サイン

オブジェクト指向の考え方では、「ひとつのクラスは、ひとつの責任に集中すべき」 とよく言われます。
でも実際の開発では、「どこまでが“ひとつの責任”なのか」がとても曖昧になりやすい。

だからこそ、違和感を感じた時点で一度立ち止まり、設計を“育て直す” ことが大切になります。

違和感チェックリスト

違和感チェックリスト(あなたのクラス、膨らみすぎてない?)

チェック項目 気づきの例
🔲 クラス内のメソッド数が10以上ある まとめすぎか、機能を分けるべき時期かも
🔲 状態変数(this.◯◯)が5個以上ある 状態の整理や分離が必要かも
🔲 似たような処理が2箇所以上に登場する リスト生成やイベント登録が重複してないか
🔲 UI処理とロジック処理が混ざっている 描画部分は別のクラスや関数に分けられるかも

こうした兆候が見えてきたら、クラスを「作り直す」のではなく、
「育て直す」タイミングに来たと捉えると、とても前向きにリファクタができます。

3. クラスを育てる3つの視点

クラスが大きくなってきたとき、
「どこから整理すればいいのか分からない」というのは、よくある悩みです。

でも、ある程度使ってきたクラスだからこそ、
「使ってみてわかった問題点」から整理できるという強みがあります。

ここでは、クラスを“育てる”ときに役立つ3つの視点を紹介します。

1. 責務(せきむ):クラスの担当

クラス設計で最も大切な考え方が「責務」です。
責務とは、「このクラスは何を管理すべきか、何をすべきでないか」 という役割の境界線です。

✋ よくある責務の混在例:

this.audioManager.play(...);       // ← 再生処理
this.noteInterval = setInterval(); // ← アニメ制御
document.getElementById(...)       // ← DOM操作

これらが1つのメソッド、1つのクラスの中に混ざっていたら、
「音を鳴らす」「表示を変える」「状態を持つ」責任が分散してしまっているサインです。

✅ 対策:

  • UI更新処理は別メソッドに分離
  • 外部依存(例:AudioManagerやDOM) は引数で渡す or 別モジュールに逃がす
  • 明確に「◯◯はこのクラスの担当」と言えるようにする

2. 再利用:処理の使いまわし

同じような処理が複数の場所にあると、メンテナンス性が急激に下がります。

たとえば、createTrackListByType(type) のような処理は:

  • 初期化時
  • カテゴリ変更時
  • モーダル再表示時

…など、複数の場所から呼ばれる可能性があります。

✅ 対策:

  • 共通処理は明示的なメソッド化(例:this.renderTrackList())
  • 同じロジックは「再利用前提」で切り出して設計

3. 状態の分離:this.◯◯ の増加

クラスの中に this.currentSelectedId, this.playingid, this.noteInterval, this.container, this.audioManager …
と、状態がどんどん増えていくと、クラスの内部で状態が迷子になってしまいます。

✅ 状態整理の指針:

状態の種類 整理方法
UI用一時データ(DOM参照など) できるだけメソッド内ローカルに
管理すべき永続状態(IDや再生中) 明示的に this.◯◯ として定義してOK
外部依存(audioManagerなど) コンストラクタで渡して this.依存名 として保持

✨ まとめ:育てるとは、不要なものを手放していくこと

  • 責務を絞る
  • 再利用可能な形に整える
  • 状態を見える化して、必要な分だけに減らす

こうやって少しずつ整えていくことで、
「書いたクラスを、後からも安心して触れるクラス」に育てていけるのです。

4. SoundTest クラスをリファクタ

ここからは、実際に使用していた SoundTest クラスの中から、具体的なリファクタ対象を抜き出して整理していく過程を紹介します。

今回リファクタ対象として取り上げるのは以下の3つです:

  1. 再生中アニメーション処理(♬)
  2. トラックリストの生成処理
  3. 再生・停止処理とUI更新の分離

1. 再生中アニメーション処理の分離

Before(再生処理に直接組み込み):

this.noteInterval = setInterval(() => {
  iconSpan.textContent = notes[index++ % notes.length];
}, 300);
  • playSound() の中で直接 setInterval() を使っていた
  • 停止処理との連動が曖昧で、再生が終わったあとも残ってしまう場合があった

After(専用メソッドに分離):

startNoteAnimation(targetElement) {
  this.stopNoteAnimation(); // 既存アニメーション停止
  this.noteInterval = setInterval(() => {
    this.iconSpan.textContent = this.notes[index++ % this.notes.length];
  }, 300);
}

stopNoteAnimation() {
  clearInterval(this.noteInterval);
  this.noteInterval = null;
  if (this.iconSpan) this.iconSpan.textContent = "";
}

→ ✅ アニメーションの開始・終了を明確に分離
→ ✅ 他の処理と独立してメンテできるようになった

2. トラックリストの生成処理の分離と汎用化

Before:
openSoundTest() や カテゴリ変更イベント など、複数の場所で同じコードをコピペ:

const scrollList = document.getElementById("sound-test-scroll-list");
scrollList.innerHTML = "";
for (const id in audioData) {
  if (audioData[id].type !== type) continue;
  ...
}

After:

createTrackListByType(type) {
  const scrollList = this.container.querySelector("#sound-test-scroll-list");
  scrollList.innerHTML = "";

  const audioData = this.audioManager.audioData;
  for (const id in audioData) {
    const info = audioData[id];
    if (info.type !== type) continue;
    const item = this._createTrackItem(id, info);
    scrollList.appendChild(item);
  }
}

→ ✅ DOM構築処理を明示的に分離
→ ✅ type に応じて再利用できるように汎用化

3. 再生・停止処理とUI操作の責務を明確に

Before:

playSound() {
  const id = this.currentSelectedId;
  this.audioManager.play(id);
  this._startNoteAnimation();
  document.querySelector(".now-playing").textContent = "🎶 " + id;
}
  • 再生処理とUI更新(表示)が混ざっている
  • 単体テストや処理の再利用がしづらい

After:

playSound() {
  const id = this.currentSelectedId;
  if (!id) return;

  this.audioManager.play(id);
  this._handleUIAfterPlay(id); // UI更新は別関数へ
}

_handleUIAfterPlay(id) {
  this.startNoteAnimation(id);
  this.updateNowPlayingLabel(id);
}

→ ✅ メソッド単体の責務が明確に
→ ✅ UI表示だけを切り離して、後からでもカスタマイズしやすくなった

💡 リファクタのポイントまとめ

目的 手法
👓 読みやすさ UI処理とロジック処理の分離
♻️ 再利用性 共通処理(トラック生成)を関数化
🔄 保守性 アニメーションやUI更新処理を独立管理

5. 外部依存との付き合い方

クラスが成長してくると、必ず出てくる疑問があります。

「この処理、クラスの中でやるべき?
それとも、外に出すべき?」

特に悩ましいのが、「外部との関係をどう扱うか」です。
サウンドテストUIで言えば、主に以下のような外部依存がありました:

  • 🎧 AudioManager(音声の再生・停止)
  • 🧱 document.getElementById()(DOM操作)
  • 📄 audioData(JSONなどの外部データ)

🧩 外部依存が混ざると何が起こる?

  • クラスのテストが難しくなる(外部がなければ動かない)
  • 拡張性が落ちる(別のUIに使い回しづらい)
  • 「このクラスだけ読んでも全体の流れがつかめない」

→ つまり、クラスが“自立できない状態”になってしまうのです。

✅ 解決のカギは「注入」と「境界線」

1. 依存は外から“注入”する

const soundTest = new SoundTest(AudioManager, containerElement);

AudioManager をグローバルではなく明示的に渡すことで、「このクラスは何に依存しているか」が見えるようになります。

  • ✅ テスト時にはモック(ダミーAudioManager)を渡すことも可能
  • ✅ 複数のAudioManagerを切り替えて使うこともできる

2. DOM操作は「UIロジック」として分離しておく

updateNowPlayingLabel(id) {
  const label = this.container.querySelector(".now-playing");
  if (label) label.textContent = `🎶 ${id}`;
}

このように、UI表示部分だけを関数に閉じ込めることで:

  • DOMが変わっても修正箇所が限定される
  • ロジック(再生処理)と見た目(表示)の責任が分かれる
  • 他のUI(たとえばスマホ用UI)にも同じロジックが使い回せる

3. 状態と見た目を分けて考える

クラス内部では、「音が再生されているか」という状態と、 「♬マークが点滅しているか」という見た目の処理は、分けて扱うと設計がスッキリします。

  • 状態:this.playingid にIDを保存
  • 表示:アニメーション処理は startNoteAnimation() に委譲

→ これにより、UIが変わってもロジックをそのまま使えるようになります。

💡 まとめ:クラスを“自立させる”ために

原則 具体策
依存は隠さずに渡す コンストラクタで受け取る
UI操作はまとめる updateUI() のような関数で管理
外部との接点は1ヶ所にする render()handleEvent()など役割を限定

6. まとめ:設計は育てながら覚えていく

クラス設計というと、「最初にしっかり設計して、完成したら終わり」――
そんなイメージを持っていた頃が、私にもありました。

でも実際に SoundTest クラスを使いながら開発を続けていくと、
**クラスは“完成させるもの”ではなく、“育てていくもの”**だと気づきました。

設計の正解は、書いてから見えてくる

最初は正直、よく分からないまま this.currentSelectedId や playSound() を作っていたと思います。 でも使っていくうちに、

  • 「この状態はこのクラスが持つべきだ」
  • 「この処理はUIと分けた方が良さそうだ」
  • 「ここは一度共通化できるかも」

といった、実際に使ってみなければ気づけない“設計のヒント” が見えてきました。

クラスを育てる5つの心得

  1. 関数が増えてきたら「まとまり」にする勇気を持つ
  2. this. による状態管理で、読みやすさと明確さが生まれる
  3. コードは“会話のように書ける”と使いやすくなる
  4. 責務・再利用・状態の分離でクラスが洗練される
  5. 外部との境界線を意識すれば、クラスが自立する

クラスは「ツール」ではなく「仲間」

ある程度大きなUIを作りはじめると、
コードは自分の意図を表現するための言語になってきます。

そのとき、よく設計されたクラスは**“自分の代わりに動いてくれる小さな相棒”**のような存在になります。

  • so0undTest.playSound(); → 自分の代わりに、ちゃんと再生してくれる
  • soundTest.stopAll(); → 状態もUIもきれいにリセットしてくれる

最初から完璧な設計 はいらない

この記事シリーズを通して一番伝えたかったのは、

「クラスは最初から完璧に書く必要はない」 ということ。

むしろ、動かしながら、育てながら、少しずつ整えていけばいい。
そのプロセスそのものが、設計の理解につながっていきます。