6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unityでの設計メモ

Last updated at Posted at 2021-07-14

Unityでは比較的自由にコードを書いていくことができますが
実際にそれなりの規模になってくると後で処理が追いづらくなることがあります。
自分の中で指針が欲しい、ということでクリーンアーキテクチャなど設計に関して勉強し、
それを今後継続的に使用していくためのメモです。
また間違った認識で書いている場合もありえますので、やさしくマサカリ投げていただけると助かります。

#依存関係逆転の原則
上位レベル: 変更を加えたくないベースとなる処理等
下位レベル: UIや表示アニメーションなど上位に対してより具体的な処理
とした場合に
下位レベルは上位レベルと比較して変更される可能性が高いため、
上位レベルを下位レベルに依存するよりも
下位レベルを上位レベルに依存させる方が変更に強くなります。

以下の画像はwikipediaの依存関係逆転の原則にある画像です。
izonn.png
この図ではAが上位レベル、Bが下位レベルとすると
左の図では処理を呼ぶためAがBを参照しており、Bに変更があった際にAに影響が出るかもしれません。(ObjectAはObjectBに依存している)
右の図のようにObjectBで継承するinterfaceがPackageAに定義されている場合、
ObjectAはInterfaceAを参照すれば良いためPackageA内で処理が完結します。(ObjectAはInterfaceAに依存している)

このように処理としては上位->下位でも、
interfaceなど抽象化したものを使って依存方向を下位->上位にしてコードをより変更に強くて安全に保つことができます。
これは依存関係逆転の原則と呼ばれています。

#Extenject
上記の例の場合でInterfaceAを継承しているObjectBをどうやって指定しよう、といった場合に便利なのが依存性の注入です。英語ではDependency injectionと言い、DIという略称でも呼ばれます。
注入という表現が若干分かりにくいですが、参照するクラス内で直接指定せずに設定ファイルなどでInterfaceAをObjectBとして扱うよう指定できるというものです。これによりテストがしやすくなったりObjectBとObjectB´を比較したい時にも設定ファイル一発で切り替えることができます。

Unityでこれを手軽に実現するためのライブラリがExtenjectです。
assetstoreからダウンロード、importして使用できます。

↓使用方法参考

##Extenject使用例
インターフェース×1とそれを継承したクラス×2を用意しました。

public interface ITestable
{
  string GetText();
}
//----------------------------------------------
public class Hoge : ITestable
{
  public string GetText()
  {
    return "hoge";
  }
}
//----------------------------------------------
public class Huga : ITestable
{
  public string GetText()
  {
    return "huga";
  }
}

インターフェースを使用して処理を行うスクリプトを用意して適当なオブジェクトにアタッチします。この時インターフェースに[Inject]属性を指定します。

using UnityEngine;
using Zenject; // 以前はExtenjectではなくZenjectという名称であったためその名残らしい。 

public class ExtenjectSample : MonoBehaviour
{
  ITestable test; // [Inject]属性を指定することで依存性の注入が可能になる

  [Inject]
  public void Constractor(ITestable test)
  {
    this.test = test;
  }
  void Start()
  {
    Debug.Log(test.GetText());
  }
}

Projectビューで右クリック>Create>Zenject>Mono InstallerでInstallerを作成します。Installerでどのinterfaceをどのクラスとして使用するか設定します。

ExtenjectTestInstaller.cs
using UnityEngine;
using Zenject;

public class ExtenjectSampleInstaller : MonoInstaller
{
  public override void InstallBindings()
  {
    Container.Bind<ITestable>().To<Huga>().AsCached();
  }
}

Hierarchyビューで右クリック>Zenject>Scene Contextを作成して、
・先ほどのExtenjectSampleInstaller.csをアタッチ
・InspectoorビューでSceneContextのMono InstallersにExtenjectSampleInstaller.csを指定

これにより依存性の注入が可能になります。

#クリーンアーキテクチャ

設計の際にこれを守ることで変更に強くできる、というものです。

ca.jpeg
よく見かける図ですね。
これはそれぞれ分けるべきレベル(依存関係逆転の原則の項に書いていた上位レベル、下位レベル)のコード群を表しています。
黄色になっているEntitiesが一番上位にあり、青色がここでは一番下位のレベルとして表現されています。

Screen Shot 2021-07-13 at 20.35.26.png
またEntitiesの左の矢印はそれぞれ望まれる依存方向を示しています。
例えば青色のUIが緑色Presenterに依存し、PresenterUse Casesに、Use CasesEntitiesに依存するようにします。

Screen Shot 2021-07-13 at 20.36.04.png
処理の流れに関しては右下の図が参考になります。Controllerは依存先のUseCaseInputPort<interface>の処理を行い、UseCaseInputPort<interface>を継承してその具体的な処理を記したUseCaseInteractorPresenterのインターフェースであるUseCaseOutputPortの処理を行うということを図示しています。

#実装例
ボタンを押すとボタンを押した回数を表示する、という処理をクリーンアーキテクチャで試しました。
以下の図のように実装。
Screen Shot 2021-07-14 at 11.45.59.png

具体的な内容としては
CountButton : Monobehaviorを継承。UIボタンにアタッチしてクリックされたらControllerの処理を呼びます。
Countroller : 入力をInteractorで扱いやすいように加工。ですが今回はそのまま渡しています。
Interactor : ビジネスロジック。今回は入力があったらCounterをインクリメントして出力。もしデータを保存する際はここからIDataGatewayを用意して下位レベルでそれを継承したDataGatewayを用意して、それに保存処理を呼びます。
Presenter : 出力をViewで扱いやすいように加工。今回はintからstringに変換。
CountTextView : Monobehaviorを継承。UIテキストにアタッチしてPresenterからテキスト反映を行います。

処理の流れとしては
CountButton->Controller->Interactor->Presenter->CountTextView
です。

本当はControllerからInteractor、InteractorからPresenterに渡す際に後から値を追加や変更がしやすいように入力/出力用のクラスを用意してそれを渡すのがベターな気がしますが今回は簡易テストということで省略しています。

##Assembly Difinition
これを使用することでC#のビルドファイル(アセンブリ)を個別に出力することができ、
・変更をしていない箇所は毎度ビルドをしなくて済む
・依存関係がより明確になる
・internalなど便利な機能が使える
・後述のテストが可能になる(?)
などメリットがあります。

こちらの記事でかなり詳しく書かれており参考になりました。

##テスト駆動開発とユニットテスト
ひとつのクラス(ユニット)など書く際に、先に「こういう挙動してほしいなー」というのをテストコードとして書き、それに沿って実際にクラスを書いていく、さらにその次も同じようにユニットテストを書いてそのテストに合格するコードを書いていく手法としてテスト駆動開発というのがあります。
これにより、より確実に開発を進めていくことができます。

Arrange: 事前準備
Act: 実行するアクション
Assert: 検証
から成るAAAパターンでテストを書くことで整理されて何をしているか分かりやすくなります。

UnityではUnity Test Runnerでこれを実現できます。
↓導入方法参考

##Unity Test Runner使用例
実装例でのEntities内のCounterをテストします。
Projectビュー内の任意の場所で右クリックしCreate>Testing>Tests Assembly Folderを選択。ひとまず名前はTestsとし、Assembly Definition ReferencesからUnityEditor.TestRunnerを削除します。
同じ階層内で右クリック>Create>Testing>C# Test Scriptを作成します。
名前をCounterTestとし、以下のように作成しました。

CounterTest.cs
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests
{
  public class CounterTest
  {
    [Test]
    public void CounterTest_L1Entities()
    {
      // arrange: 事前準備
      var counter = new Counter();

      // act: アクション実行
      counter.Increment();

      // assert: 検証
      Assert.That(counter.value == 1);
    }
  }
}

もちろんですがこの時、テストには通りません。
次にテストしたいコードを配置するフォルダを作成し、そのフォルダ内で右クリック>Create>Assembly Difinitionを選択し、分かりやすいようフォルダと同じ名前にします。(ここではL1_Entitiesという名前にしました)
さらにテスト対象となる、実際のコードを同じフォルダ内に作成します。

Counter.cs
public class Counter
{
  public int value { get; private set; }
  public Counter()
  {
    value = 0;
  }
  public void Increment()
  {
    value++;
  }
}

そしてTestsアセンブリでこのアセンブリで参照するため
Testsアセンブリを選択し、表示されるInspectorビューのAssembly Definition Referencesにこのアセンブリを指定します。
Screen Shot 2021-07-14 at 12.20.02.png

これでテストが可能になりました。
Window>General>Test Runnerでテスト確認画面を開き、PlayModeが選択されているのを確認しRun All/Run Selectedでテストを実行することができます。

##その他の実装
https://github.com/migizo/CleanArchitectureSample/tree/master

こちらにその他の実装サンプルがあります。

#感想
クリーンアーキテクチャに関してはラピッドプロトタイピング的な用途では使わずにある程度の規模になりそうであれば使えば幸せになれそうです。
テストを書く前に作りたいコードを書いてしまってたのに気づいたので、慣れるまでは少し苦戦しそうです。
あとクリーンアーキテクチャー的格言で
「 フレームワークと結婚するな! ( = フレームワークに依存しない設計をしよう) 」
というのがあるのですが、どうしてもExtenjectやその他Unityのお作法みたいなのに引っ張られてしまいがちなのでしばらく試行錯誤が続きそうです。

#参考
https://qiita.com/MinoDriven/items/2a378a09638e234d8614
interfaceの使いみちがよく分かっていない時に読んで設計に関して勉強するきっかけになりました。
適切なクラス設計に関して"役割駆動"という言葉でわかりやすく説明されています。

https://www.amazon.co.jp/dp/B07FSBHS2V/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1
クリーンアーキテクチャの本。少し冗長で分かりづらい気もしましたが、設計に関してかなり勉強になりました。(SOLID原則, 特に依存関係逆転の原則)

https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
クリーンアーキテクチャの本は分かりづらい表現が所々ありましたが、こちら冒頭の動画を事前に見ていたため理解しやすかったです。

https://logmi.jp/tech/articles/324456
Unityでの設計に関して一番納得した記事です。

https://mogi0506.com/unity-scenemanager-loadscene/
テストにおいてシーン中のMonobehaviorを継承したコードをアタッチする際に参考になりました。

https://booth.pm/ja/items/1520608
Extenject(Zenject)本。webより情報がまとまっていて参考になりました。

https://www.amazon.co.jp/dp/B07DJ2BL4Y/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1
クリーンアーキテクチャの本より文字が小さくて量も多く、文章も少し読みづらいですがC#での設計やクリーンアーキテクチャでも紹介されているSOLID原則などについて詳しく書かれています。

https://qiita.com/toRisouP/items/97c4cddcb735acde2f03
Github for Unityの使用方法。いつもはSourceTreeで管理していましたが試しに使ってみたところpushし忘れが減りそうで良さそうです。

6
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?