【C++】クラス分割、モジュール分割、責務の分離 まとめ

C/C++

この前、推し活でCDを購入するためTSUTAYAへ赴いたのですが、CD販売コーナーがありませんでした。

今は皆さんもうCDなんて買わないんですねΣ(゚Д゚)エー

いつからか、時代は音楽配信サービスへ。私もサブスク重課金勢(いわゆるヘヴィユーザー)でapple music利用者なのですが、如何せん気に入らないところもあったりします。

私が子供の頃はCDコンポというものが自宅にありました。
懐かしい。

当時はCD全盛期で、オリコンチャート上位組はCD売上がミリオンだとか、よく話題に上がっていました。

もう20年以上前の話です。

当時は自分織りのベスト盤的なデータ持ち歩きはMDだとか、CD-Rとかそんな感じでした。
流石にDAT(カセットテープ)の時代は過ぎちゃっていましたが(;・∀・)

音質なんてものはやっぱりCDだからこそ聴きごたえのあるものでしたね。CD-Rなんかに焼き直しをすると当然ながら非可逆圧縮音楽データは音質が悪くなります。

今もapple musicなんかは全てデジタルデータですが、音量がまばらなのがちょっと気に食わないですね。

ノーマライズ(音量一定化)なんかは無料ツールで簡単にできますが、手間が掛かるのがね・・・

まあ便利な物の裏側にはリスクが伴ってるって言いますもんね(・・・言うんだっけ( ̄▽ ̄)??


さてさて、前置きちょっと長いですね。はい。

今回はC++のためになる(かもしれない)お話です。

自作ロックマンの開発を進める上でも重視しているのですが、「責務の分離」というプログラミング界隈の思想があります。

プログラムが大きくなってくるとソースファイルが増え、管理するデータが増え、誰(オブジェクト)が何(ファンクション)を担当するのかを明確に決めなければいけなくなってきます。

それを放置した時にどうなるのかと、クラスの管理や責任範囲の明確化がどのようにしてプログラミングに影響するのかを解説していこうと思います。

例はゲーム開発っぽい内容になるかもしれませんが、どの分野のシステム開発でも応用できる内容だと思いますので、よかったら読んでみてくださいd(・∀・)


クラスについて

まずはクラスについて考えていきましょう。

オブジェクト指向プログラミングの基礎的なお話になりそうなので、簡潔に(・ω・)σ

class Player {
    void Update();     // 更新処理
    void Draw();       // 描画
};

コードで書いてみるとこんな感じのやつです(^∇^)

例なのでどうしてもイメージしにくい表現になってしまうのですが、超簡単に説明すると

クラスとは「ある責任を専門的に引き受ける存在」

というものになるかと思います。

ここで間違いやすい部分があります。それは、

  • データの袋 ❌
  • 関数の寄せ集めパック ❌

ではありません。

あくまで大事なのは、役割を持った専門家、という認識でいきましょう。


責務(役割)とは何か

では、次にクラスが担うべき「責務」とはなんなのかについて説明しましょう。
役割の事ですね。

クラスを定義するためにまず考える事は、そもそもクラスがどういう目的で作られるかという事です。

数が増えてきた変数をまとめる?
似たような関数を一つにする?

よくある思考だとそのように考えるかと思います。

しかしここでは、実体として見えない役割を与える箱としてクラスを作る事を考えます。

基準は3つです。

  1. 何の専門家か説明できるか?
  2. 変更理由は1つか?
  3. 責務が名前に現れているか?

順番に見ていきましょう(・∀・)/

①「何の専門家か説明できるか?」

このクラスは何を知っていて、何を決める存在であるか。

例:

  • InputManager → 入力状態の専門家
  • SaveSystem → 永続化の専門家
  • Player → プレイヤー状態と振る舞いの専門家

例えば、プレイヤーとフィールドの当たり判定確認処理と、プレイヤーのバウンディング処理(壁にめり込まない反射的挙動)が同じクラスに駐留すると、何の専門家であるか説明できなくなってしまいます。
それはよくないね。

②「変更理由は1つか?(SRP)」

これはオブジェクト指向におけるSOLID原則の一つである SRP の核心です。

このクラスが修正されるとしたら、どんな理由なのか。
UI変更?
仕様変更?
それともフォーマット変更?

2つ以上浮かんだら、もう役割が混ざっています。

SRPについては後で説明しますね。

③「責務が名前に現れているか?」

良いクラス名はコメントより雄弁です。

クラス名を見ただけで8割以上何の処理が書かれているか、どんな風な振る舞いがされるかが分かるような名前を付けるように心がけましょう。

これがポイントです(・∀・)

コメントはあまり書かないほうがいいです。
もちろんそれは、コメントを全く書かずソースコードが解読できないのは仕方がないという意味ではなく、コメントを書かなくとも構造が理解しやすい美しいソースコードになるようにしようという意味です。

オブジェクト指向だけじゃなくプログラミング全般に言える事だけど、コーディングって「名づけ作業」なんだよね。
完成したソースコードを眺めた時に、直感的に内部の動きが掴めるような関数名(メソッド名)になっていると、コードを見る必要もなくなる。
そのためにはクラスやメソッドが持つ責務が単一である事が前提でなければいけないのね。

class PlayerRenderer; // 描画責務
class PlayerInput; // 入力責務
class PlayerState; // 状態責務

名前で「何しないか」まで分かるのが理想。

これ、超重要です( ̄▽ ̄)/

まずはじめに考える事として、役割を持った人を作るイメージを持つ事が大事です。

例えば、プレイヤーキャラクターのクラスを作る例で考えてみます。
まずはアンチパターンから。

class Character {
    int hp;
    void Draw();
    void Save();
    void Load();
    void HandleInput();
};

これは、
・キャラクターの状態管理
・描画
・永続化
・入力
などが全て含まれています。

しかし実際にはこれらは全て別の次元の要素であり、複数の責務が混ざり合ってしまっています。

ではどうするべきでしょうか?

解決例として、以下のようにしてみましょう。

class Character {
    int hp;
    void Damage(int);
};

class CharacterRenderer {
    void Draw(const Character&);
};

class CharacterSerializer {
    void Save(const Character&);
    void Load(Character&);
};

class CharacterController {
    void Update(Character&);
};

え~?
なんかクラス増えすぎ~(´・д・`)ヤダ

とか思うかもしれませんがリソースを抱える事とリスクを抱える事とでどちらが将来的投資になるか、という視点で考えていきましょう。

もちろん、あえて一つのクラスでまとめる事もNGとは言い切れません。

そこでどう判断するべきなのかは、先ほども言った通り、

  1. 何の専門家か説明できるか?
  2. 変更理由は1つか?
  3. 責務が名前に現れているか?

この点を遮る事なく実現できているか?という基準が必要になってきます。

メソッド自体が1つだけで、コード量が10行くらい、パッと見なんの処理をしているか一目瞭然だよ、という場合はクラスを分ける必要はありません。

クラス分割もやりすぎるとアフターパフォーマンスに影響が出てきます。

そのためにプログラム全体(すべてのクラス)を見て、全体の構成をどうするべきなのかを考えるのです。

これがオブジェクト指向設計です。


さて、それではさきほどから連呼している責務(責任・役割)についての重要な概念である SRP について解説していきましょう(*’ω’*)ノ

クラスを作る上でとても大事な事なので、是非読んでみてください。

コメント

タイトルとURLをコピーしました