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

0.はじめに

JavaScriptによるゲーム開発を始めて2週間近く経過。

jsファイルを分割し、用途ごとにまとめて整理をし、現状でもコード管理はかなり良い状態ですが、 昨日作成したサウンドテスト用のIUを作成➔実装後、思ったより処理が増え、soundTest.jsを作成して関数をまとめたのですが、今までやってこなかったクラス設計に挑戦してみました。

それにあたり、やや苦労したところもあったので今回は、再学習を含め、JavaScriptのクラス設計の入門講座をまたAIに依頼してみました。

1. 何故クラス化したか?

サウンドテスト用の機能を実装するにあたり、以下のようなありがちな関数が増加。

  • playSound()
  • stopSound()
  • stopAll()
  • loadTracks()
  • modalClose()
  • moveToTrack()
  • createTrackListByType()
  • startNoteAnimation()
  • stopNoteAnimation()

少ないとはいえ以下のようなグローバル変数が増えたり、DOM操作の場所が散らかったりなど…。

  • currentSelectedId (選択中の曲ID)
  • playingId (再生中の曲ID)

関数が増えるとどの関数が何の責任を持っているのか分かりにくかったり、関数で共有したくなるという現象に直面。

尚且つ、

  • 再生と停止の状態管理
  • トラック選択とUI同期
  • カテゴリ別の絞り込み
  • 再生中のアニメーション制御(♬)

などが重なってくると、関数間の依存関係や、変数のスコープの境界がどんどん曖昧に。

そんな中で「クラスにまとめたほうがいいのでは?」と感じ、クラス設計として作り直しました。

クラス設計にしてパッケージ化した方が分かりやすくなる他、メンテナンスが楽なり、あとから直すのも楽なります。

そんな感覚から、自然な流れとしてクラス設計に踏み出したのが、この学びの始まりでした。

2. クラスって何?なぜ必要だった?

クラスって何?

超ざっくり言うと、状態(データ)と操作(メソッド)をまとめた設計図です。

class SoundTest {
  constructor() {
    this.currentSelectedId = null;
  }

  playSound() {
    // 再生処理
  }

  stopSound() {
    // 停止処理
  }
}

そして、これを使いたいときは:

const soundTest = new SoundTest();
soundTest.playSound();

というように、自分専用の部品としてインスタンス化して使う。

クラス化のメリット

Before(関数+グローバル変数) After(クラスで統一)
変数のスコープがバラバラ this.で状態がまとまる
関数が増えると命名が散らかる クラス内メソッドとして整理
UI操作の責任が曖昧 UIの制御単位をクラスで管理
何が今の状態か分かりづらい this.playingid などで可視化

結果

機能が増えてきたときに、関数だけではコードが煩わしく感じたので、 無理に設計を学ぶのではなく、実装の中で“必要になったから学ぶ” という自然な流れでした。

3.クラスの設計:状態と責務

クラスを使うと決めたとき、最初に考えたのは**「何をこのクラスに持たせるべきか」**でした。

関数をただまとめただけでは、クラスにする意味はありません。

大切なのは、「このクラスが何を“担当する”のか=責務(せきむ)」を明確にすることです。

SoundTestクラスの役割は?

今回の SoundTest クラスは、サウンドテスト用のUI機能を管理する専用ユニットです。

そこで、以下のような「責任の範囲」を定義しました:

✅ このクラスが担当すること:

  • 現在選択されているトラックIDの管理(currentSelectedId)
  • 実際に再生されているトラックの管理(playingid)
  • 再生・停止の操作
  • トラックリストの生成・更新
  • UI上の選択状態やアニメーション制御

❌ このクラスがやらないこと:

  • 音声ファイルのロード(AudioManager に任せる)
  • BGM/SEカテゴリの定義(JSONで与える)
  • 外部画面の開閉(必要なら呼び出し元で制御)

コードサンプル

実際の構造はこんな感じでした:

class SoundTest {
 constructor(audioManager, container) {
   this.audioManager = audioManager;
   this.container = container;

   this.currentSelectedId = null;
   this.playingid = null;
 }

 playSound() {
   // 再生中の音があれば停止
   this.stopSound();

   const id = this.currentSelectedId;
   const info = this.audioManager.audioData[id];
   if (!info) return;

   this.audioManager.play(id, info.type);
   this.playingid = id;

   // ♬アニメーションの開始など
 }

 stopSound() {
   if (this.playingid) {
     this.audioManager.stopBGM(this.playingid);
     this.playingid = null;

     // ♬アニメーションの停止など
   }
 }

 createTrackListByType(type) {
   // DOM要素を生成し、トラックをリスト化する処理
 }

 stopAll() {
   this.stopSound();
   this.currentSelectedId = null;
 }
}

ポイント

状態をクラス内に閉じ込めたことで、コードが落ち着いた

  • グローバル変数が消えてスッキリ
  • イベント処理で soundTest.playSound() など読みやすくなった
  • 関連する状態や処理が1カ所にまとまって保守がしやすくなった

💡 状態と責務を考えると、自然にクラスは形になる

「クラスはどう設計すればいいの?」と聞かれたら、 私は今ならこう答えます:

✅ 「このまとまりは何を“管理する責任”を持っているか?」

を考えよう

4. this と constructor() の理解

クラスを使い始めて最初につまずくのが、このふたつ――

  • this
  • constructor()

JavaScriptでは特にこの2つがややこしい・分かりづらい・覚えにくいと言われがちです。 でも、サウンドテストUIをクラス化してみて、ようやくその意味が「体感で理解」できました。

初期化関数 constructor()

constructor() は“クラスが生まれる瞬間の初期化関数”

class SoundTest {
  constructor(audioManager, container) {
    this.audioManager = audioManager;
    this.container = container;
    this.currentSelectedId = null;
    this.playingid = null;
  }
}

constructor() は、クラスを new で作った瞬間に実行される関数です。

const soundTest = new SoundTest(AudioManager, document.getElementById("sound-test-modal"));

この new SoundTest(…) が呼ばれたときに、constructor() が一度だけ走ります。

ここで this.audioManager = AudioManager のように、インスタンスの初期状態(設定)を決めておくわけですね。

this は「クラス内の“自分自身”」

クラスの中で this を使うと、それは**そのクラスの“インスタンス自身”**を意味します。

this.currentSelectedId = "bgm-title1";

はつまり:

「この SoundTest インスタンスに属する currentSelectedId を変更する」

という意味になります。

this の何がややこしいのか?

クラスの外で this を使うと、何を指してるか文脈によって変わるのが混乱の原因です。

例1:関数の中(クラス外)で使うと…

function hello() {
  console.log(this); // ← window や undefined(strictモードなら)
}

例2:クラスの中で使えば…

class MyClass {
  constructor() {
    this.name = "AI";
  }

  greet() {
    console.log(this.name); //  MyClass  name プロパティ
  }
}

→ クラスの中なら this は常にインスタンス自身なので、安心して使えます。

まとめ

thisは「このクラスの今の状態」にアクセスする手段

もし this がなければ、クラスは状態をうまく持てません。

関数の中から this.playingid にアクセスできるからこそ、状態を追跡でき、 複数のボタンや処理にまたがっても、整合性が保てるわけです。

5. メソッドにまとめるメリット

クラスを使う最大の魅力のひとつが、コードが“会話のように”読めるようになることです。

これは書いているときよりも、あとで読み返したときに圧倒的な違いとして現れます。

関数が増えてきたとき

クラスを使う前は、関数が独立していてこんな感じでした:

playSound();
stopAllSounds();
moveToTrack(+1);

パッと見は動きそうだけど、

  • どの関数がどの状態を操作しているのか分かりづらい
  • currentSelectedId や playingid を、いちいち関数の外に定義して使い回している
  • イベントの中でどの関数を呼ぶか悩む

そんな状態に、私もなっていました。

クラスのメソッドにまとめると…

soundTest.stopAll();
soundTest.createTrackListByType("bgm");
soundTest.playSound();

これだけで「サウンドテストUIに命令を出しているようなコード」に変わります。

読みやすさ・意味の明確さ・変更のしやすさが一気に向上しました。

メソッド化の3つのメリット

メリット 解説
💬 読みやすさ soundTest.playSound() で「誰が何をするか」が明確になる
🧩 責務の分離 「この操作は SoundTest クラスの責任」と見て分かる
🔁 再利用性 他の画面やイベントでも同じメソッドが使えるようになる

💡 例:イベントからの呼び出しも直感的に

document.getElementById("sound-test-button-play").addEventListener("click", () => {
  soundTest.playSound();
});

この1行で、「サウンドテストの再生ボタンを押したら、再生される」ことがはっきり分かります。

✨ “コードで会話する”という感覚 この感覚は、まさに人と人がやりとりするようにコードを書くということです。

soundTest.stopAll();
soundTest.createTrackListByType("se");
soundTest.playSound();

まるで「サウンドテストくん、全部止めて。次にSEリスト作って。んで再生ね」と話してるかのよう。

この自然な流れができると、コードはぐっと読みやすく、書きやすくなります。

6. クラスにしてよかったこと・つまずいたこと

クラスを導入して、コード全体の設計が明確になり、UIの動作も安定しました。

でも同時に、「やってみて初めてわかるつまずきポイント」もたくさんありました。

この章では、クラス設計を通して感じたリアルなメリットと、正直ちょっと大変だったところを振り返ってみます。

⭕ 1.状態が見えるようになった

今までは let currentSelectedId や let playingid などをグローバルに置いていたため、 どの関数で書き換えられているか分からず、追跡が大変でした。

クラス化後はこうなります:

this.currentSelectedId
this.playingid

→ 「このインスタンスが持っている状態」として明示されるので、 後から読み返しても「誰が何を持ってるのか」が一目でわかります。

⭕ 2.命令が整理された

playSound() や stopSound() をクラスに入れておくと、呼び出しが自然になります。

soundTest.playSound();

これは単なる文法以上に、「コードで会話するように命令を出せる」という実感がありました。

⭕ 3.イベントハンドラとの親和性が高い

document.getElementById("button-stop").addEventListener("click", () => {
  soundTest.stopSound();
});

関数名と状態がクラスにまとまっているから、イベント処理の中身がすっきりし、 他のファイルやボタンでも同じ処理を再利用できるようになりました。

❌ 1.this の中身がよく分からない

「this は何を指しているのか?」問題。

とくに setInterval() や addEventListener() の中で使うと、意図しない this になって混乱しました。

対策:

  • 矢印関数(アロー関数)で this を固定
  • イベント内で soundTest.playSound() のようにインスタンスから呼ぶ

❌ 2.クラスの責任範囲

「どこまでをクラスに入れて、どこから外に出すべきか」が最初はよくわかりませんでした。

たとえば:

  • モーダルの表示切替はクラスに入れる?
  • JSONのデータ処理もここに入れる?

気づき: → 「その機能の“主語”が何か」で判断するといい。 「サウンドテストに関係してる」なら入れる、そうでなければ外に出す。

❌ 3.AudioManager の扱いに困った

元々 window.AudioManager でグローバルに使っていたものを、 どう渡せばいいか迷ったときもありました。

結論:

const soundTest = new SoundTest(AudioManager, containerElement);

のように、**外から依存を注入する(DI的な考え方)**ことで整理できました。

まとめ

クラスは最初こそ構えがちですが、実際に自分で「整理したい」と感じた瞬間に使ってみると、 その効果をリアルに体感できます。

しかも、1つのクラスをしっかり設計できたことで、 今後のUI部品やシステム設計にも自信と再利用性が生まれました。

まとめ

ちょっとJavaScriptクラス設計入門の一回目とはいえ、内容が優しすぎるのではないかと、 読んでて感じましたが、初回なので難しすぎない方が良いかと思い、このままにします。

実際は、シングルトンについてなど、他にも相談内容はありました。

記事の中にあるように、何処までをクラス化するか?など、相談したり、関連してイベントハンドラの中に さらにイベントを入れる事による多重処理問題など、分かりにくいバグ問題などもありました。

トラブルや不明な点は、AIに聞くことで理解度が深まり、コード全体も見通せるようになりますし、読解力も上がりますので、どんどんAIを活用する事をお勧めします。

余談で、JavaScriptによるゲーム開発をしてると、1つの画面ですべての処理をこなしていくとになりますが、 流行りのモダンなWEB開発の集合体というか、それ以上にざまざまなスキルが求められます。

最近、AIと話してた雑談の中で、

「今の私だと最近流行りのモダンなWEBサイトって実は簡単にできるかもしれない」

と話したことがあり、AIの回答は、ゲーム開発に求められるスキルはモダンWEBサイトの集合体であり、それ以上なので当然のような話を聞き納得したりしました。

ポートフォリオに役立つJavaScriptプロジェクト40選

上記のような記事を読んでみても、今私がやって来た事の方が遥かにレベルが高いように感じて、夢中で2週間ぐらいゲーム開発を進めていく中で、物凄く沢山の事を自然に学んだと思います。

それでも、今回のクラス設計も含めて、分からない事も多々あるので、まだまだ勉強は必要だと感じています。