【C#設計】SOLID原則をUnity公式サンプルで学ぼう~S:単一責任の原則~

はじめに

講座トップに戻る

この講座ではプログラミングの設計を勉強する際に避けては通れない「SOLID原則」について学ぶことができます。

SOLID原則は有名ですので名前を知っている人も多いかもしれませんが、全然知らない人でも大丈夫です!

初心者でも分かりやすいようにできるだけ丁寧に解説します。

「Unityの使い方は分かってきたけど、コードが綺麗に書く方法が分からない」

「設計を勉強するといいらしいけど、どうやって勉強したらいいか分からない」

こんな悩みを持っているあなたは、この記事で解決できるかもしれません!

この記事ではUnity公式のサンプルをもとに解説していきます。

公式サンプルのダウンロード方法はこちらの記事を参考にしてください。

SOLID原則とは?

SOLID原則とはオブジェクト指向において、拡張性・保守性が高く保つために守る原則のことです。

これは、ソフトウェアエンジニアのRobert C. Martinが提唱した多くの設計原則を5つにまとめたものです。

それぞれの頭文字をとってSOLID原則と呼ばれています。

  • S … Single Responsibility Principle: 単一責任の原則
  • O … Open-Closed Principle: 開放閉鎖の原則
  • L … Liskov Substitution Principle: リスコフの置換原則
  • I … Interface Segregation Principle: インターフェイス分離の原則
  • D … Dependency Inversion Principle: 依存性逆転の原則

何も考えずにプログラムを書いていると、段々と拡張性・保守性が下がってきます。

  • 機能の追加、変更に時間がかかる
  • コードに再利用性がない
  • 膨大な機能を持ったクラスが出来上がる(こういうクラスを神クラスと言ったりします笑)
  • 依存関係が複雑でクラス同士の関係性がハッキリしていない

SOLID原則を学ぶことで、上記のような悩みを解決できる可能性があります。

今回はSOLID原則のS「単一責任の原則」を解説していきます。

SOLID原則などの設計に関する学習をしていると「モジュール」という言葉が良く出てきます。

モジュールというと分かりにくいかもですが、C#の場合クラスに置き換えて考えてください。

単一責任の原則とは?

これは「クラスが担う責任は1つだけにする」という原則です。

例えばPlayerは移動、弾を発射という2つの行動ができるとします。

この時Playerクラスに2つの機能を実装した場合は単一責任の原則に反しています。

これが良くないのは「移動の修正を行ったら、弾を発射する部分に影響が出てしまった」とい事が起きうるからです。

単一責任の原則を守るにはMoveクラスとShotクラスに分けて実装します。

この原則を意識することで変更や修正に強くなります。

Unity公式サンプルを見てみよう

Assets/1 SingleResponsibility/Scriptsというフォルダの中に5つのスクリプトがあります。

これらが単一責任の原則に関するサンプルコードです。

スクリプトはそれぞれ以下のような内容になっています。

  • Player.cs・・・プレイヤーの各機能をまとめるためのクラス
  • PlayerAudio.cs・・・オーディオ関連の機能をまとめたクラス
  • PlayerInput.cs・・・入力関連をまとめたクラス
  • PlayerMovement.cs・・・移動関連をまとめたクラス
  • UnrefactoredPlayer.cs・・・単一責任の原則に反しているプレイヤーを実装したクラス

UnrefactoredPlayer.csが原則を破っている良くない例で、そのほかが単一責任の原則に従って作成した例となります。

改善前のダメな点と、改善後の良い点を見ていきたいと思います。

そのままでは動かない部分があったので一部サンプルに付け足している箇所があります。

気にしなくても大丈夫ですが一応混乱を防ぐために書いておきます。

改善前のコード

public class UnrefactoredPlayer : MonoBehaviour
 {
     //入力軸の名前
     [SerializeField] private string _inputAxisName;
     //座標関連
     [SerializeField] private float _positionMultiplier;
     private float _yPosition;
     //サウンド関連
     private AudioSource _bounceSfx;
     
     private void Start()
     {
         _bounceSfx = GetComponent<AudioSource>();
     }
 
     private void Update()
     {
     	//入力を受け取って位置を変更する
         float delta = Input.GetAxis(_inputAxisName) * Time.deltaTime;
         _yPosition = Mathf.Clamp(_yPosition + delta, -1, 1);
         transform.position = new Vector3(transform.position.x, _yPosition * _positionMultiplier, transform.position.z);
     }
 
     private void OnTriggerEnter(Collider other)
     {
     	//何かにヒットしたら音を鳴らす
         _bounceSfx.Play();
     }
 }

現状の悪い点

UnrefactoredPlayerクラスには入力、移動、サウンドの機能が詰め込まれています。

これでは仕様の変更・修正・追加があった際に分かりにくくミスが起きやすい状態です。

改善後のコード

・プレイヤー全体の制御用のクラス

[RequireComponent(typeof(PlayerAudio), typeof(PlayerInput), typeof(PlayerMovement))]
 public class Player : MonoBehaviour
 {
     [SerializeField] private PlayerAudio playerAudio;
     [SerializeField] private PlayerInput playerInput;
     [SerializeField] private PlayerMovement playerMovement;
 
     private void Start()
     {
         playerAudio = GetComponent<PlayerAudio>();
         playerInput = GetComponent<PlayerInput>();
         playerMovement = GetComponent<PlayerMovement>();
     }
     
     private void Update()
     {
     	playerMovement.CalcYPosition(playerInput.Delta);
     }
 }

・プレイヤーのサウンド関連の機能をまとめたクラス

public class PlayerAudio : MonoBehaviour
 {
     private AudioSource bounceSfx;
 
     private void Start()
     {
         bounceSfx = GetComponent<AudioSource>();
     }
 
     private void OnTriggerEnter(Collider other)
     {
     	//何かにヒットしたら音を鳴らす
         bounceSfx.Play();
     }
 }

・プレイヤーの入力関連をまとめたクラス

 public class PlayerInput : MonoBehaviour
 {
     [SerializeField] private string inputAxisName;
     public float Delta{get; private set;}
      
     private void Update()
     {
         Delta = Input.GetAxis(inputAxisName) * Time.deltaTime;
     }
 }

・プレイヤーの移動関連をまとめたクラス

 public class PlayerMovement : MonoBehaviour
 {
     [SerializeField] private float positionMultiplier;
     private float yPosition;
     
     //入力結果を受け取って位置を計算する
     public CalcYPosition(float delta)
     {
     	yPosition = Mathf.Clamp(_yPosition + delta, -1, 1);
     }
 
     private void Update()
     {
         transform.position = new Vector3(transform.position.x, yPosition * positionMultiplier, transform.position.z);
     }
 }

改善後の良い点

入力、移動、サウンドの機能がそれぞれ別クラスに分離されました。

そしてPlayerクラスはそれぞれのスクリプトの参照を持っており、機能間のやり取りを担当しています。

各機能が分離されたことによって、変更・修正・追加がやりやすくなりました。

Playerクラスの前についているRequireComponentはスクリプトがアタッチされたオブジェクトに対象のコンポーネントがなければ自動でアタッチしてくれるAttributeです。

まとめ

これくらい短いコードで済むのであれば、正直ここまで分ける必要ないと私は考えています。

これはサンプルなので短いですが、ちゃんとしたものを作るとなるともっと機能が多くなります。

そうなった場合は単一責任の原則を意識してコーディングしていきましょう!

👇その他の原則を学ぶ

S:単一責任の原則←今ここ

O:開放閉鎖の原則

L:リスコフの置換原則

I:インターフェイス分離の原則

D:依存性逆転の原則

👇あなたにお勧めの書籍

コメント