3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UnityAdvent Calendar 2024

Day 17

オニオンアーキテクチャを使って、Unityと.NETでコードを共有する

Last updated at Posted at 2024-12-17

こちらは Unity Adnvent Calender 2024 その2 17日目の記事になります。

概要

オニオンアーキテクチャを使って、Unityと.NETでコードを共有しながらTODOアプリを作成する方法を解説します。

アプリの説明

Unityと.NETコンソールアプリの2つのアプリのスクリーンショットです。

  • Unity
    image.png

  • .NETコンソールアプリ
    image.png

TODOアプリとしてのベーシックな機能があります。

  • 文字列を入力したらTODOメモが追加される
  • 完了したら Done になる

加えてアプリの特徴として、以下のようにユーザーを甘やかさない厳し目のTODOアプリになっています

  • 必ず一定時間で期限切れにする → TODO決めたら、すぐやれ!
  • 有効なTODOは一定個数に制限する → TODOを積み続けるな! すぐやれ!

(アドベントカレンダーの締め切り間に合わなくて申し訳ありません。)

本文

オニオンアーキテクチャとは

オニオンアーキテクチャは、システムを複数のレイヤーに分割し、依存性を内側のレイヤーに向けるアーキテクチャです。以下の特徴があります。

  • ドメイン中心設計: アプリケーションのビジネスルールを中心に設計
  • 依存性の方向: 外側のレイヤーが内側のレイヤーに依存するが、逆はない
  • テスト容易性: ドメイン層が他のレイヤーから独立しているため、ユニットテストが容易になる

主要なレイヤーは以下です。

  • ドメイン層: ビジネスルールやエンティティ
  • ユースケース層: ユースケースやアプリケーションロジック
  • インフラ層: データベースや外部サービスとの連携
  • UI層: ユーザーインターフェース

TODOアプリの設計

TODOアプリでは、以下の要素を設計します:

  • ドメイン層: Todo エンティティや TODOの制限ルール
  • ユースケース層: TODOリストの追加・削除・一覧取得のユースケース
  • インフラ層: 永続化のためのデータアクセス
  • UI層: ユーザーが直接ふれるユーザーインターフェース部分
    • ここはプラットフォーム固有になるので、Unityと.NETで個別に実装

プロジェクト構成

コンソールアプリのソリューション内に共通コードを別csprojとして配置します。

/LifeTodoConsole
  /Domain
    - LifeTodo.Domain.csproj
  /UseCase
    - LifeTodo.UseCase.csproj
  /Infra
    - LifeTodo.Infra.csproj
  /ConsoleApp
    - LifeTodo.ConsoleApp.csproj
  /Test
    - LifeTodo.Test.csproj
/LifeTodoUnity
  /Assets/Scripts
    /Views
      - Views.asmdef
    /Installers
      - Installers.asmdef
  • LifeTodoConsole: コンソールアプリと共通コードを含んだソリューション
  • LifeTodoUnity: Unityアプリ。

コード共通化の方法

今回は LifeTodoConsole 内の共通コードを含んだDLLを LifeTodoUnity から参照しています。

image.png

共通コードの変更のたびにDLLを手でコピーするのは面倒なので本格的に実装する場合は、nuget配信・ソリューションごと同居・git submoduleなど別の手段での共通化も検討してください。

ドメインモデリング

今回のTODOアプリのドメインモデリングをPlantUMLのユースケース図で書いたものです。
前述のように、以下のようなアプリの特徴があり、この部分をドメインモデリングでも表現します。

  • 必ず一定時間で期限切れにする → TODO決めたら、すぐやれ!
  • 有効なTODOは一定個数に制限する → TODOを積み続けるな! すぐやれ!

以下は、ドメイン層のモデルをPlantUMLで表現した例です。

コード

全体コード

全体コードはここにおいてあります。

Domain層

ドメイン層ではアプリケーションのビジネスロジックを表現します。

下記部分の Do() メソッドでは 「一度期限切れになったTODOは変更できない」というルールを表現しています。

public class Todo
{
    public TodoId Id { get; init; }
    public string Text { get; init; }
    public DateTime CreatedDate { get; init; }

    public TodoStatus Status { get; private set; }

...

    public void Do()
    {
        if (Status == TodoStatus.Active)
        {
            Status = TodoStatus.Done;
        }
    }
...
}

ユースケース層

ユースケース層ではアプリケーションとして成り立たせるために必要な機能が実装されます。
実装の詳細はinterfaceを経由してインフラ層に移譲する場合もあります。

下記部分では、TODOの内容文を指定してTodoを追加できること、有効なTODO(=未完了)だけを抜き出す機能が実装されています。

public class TodoAppService
{
...
    public void AddTodo(string? todoTextNew)
    {
        var todoNew = new Todo(todoTextNew!);
        todos.Add(todoNew);
    }

    public List<TodoDto> GetActiveTodos()
    {
        return todos.GetActiveTodos().Select(t => new TodoDto(t)).ToList();
    }
...

インフラ層

インフラ層ではドメイン層・ユースケース層におくべきでない実装の詳細が書かれています。

下記部分ではドメイン層の ITodoRepository を継承した、インメモリなレポジトリ実装が示されています。
TODOが外部から追加されたら、フィールドの List<Todo> にそのまま追加します。
TODOの量が肥大化してDBで管理したくなった場合も、interfaceさえ守っていれば、インフラ層の実装が変わっても他の階層に影響はないはずです。

public class InMemoryTodoRepository : ITodoRepository
{
    private List<Todo> todos = new();
...
    public void Add(Todo todoNew)
    {
...
        todos.Add(todoNew);
    }
...
}

UI層 - コンソールアプリ

UI層はプラットフォームごとに異なります。
コンソールアプリでは Program.cs に直接書いています。

下記部分ではコンソールに数字が入力されたら、指定した番号の既存のTODOを完了にします。それ以外の文だったら、新規TODOとして追加しています。

class Program
{
...
    static void Main()
    {
...
        while (true)
        {
            bool isFinished = Update();
            if (isFinished)
            {
                break;
            }
        }
...
}

    private static bool Update()
    {
        string? todoTextNew = RecieveText();

        if (int.TryParse(todoTextNew, out int indexDone))
        {
            TodoDto itemDone = currentActiveTodos.ElementAt(indexDone);

            appService.DoTodo(itemDone);
        }
        else
        {
...
                appService.AddTodo(todoTextNew);
...
    }

    private static string? RecieveText()
    {
        Console.Write("> ");
        string? todoTextNew = Console.ReadLine();
...
    }
...
}

UI層 -Unityアプリ

UI層はプラットフォームごとに異なります。
Unityアプリでは複数の MonoBehaviour 継承したスクリプトを使用します。

下記部分ではPrefabを実体化させて、Todoの内容を反映しています。

public class TodoPanelView : MonoBehaviour
{
    [SerializeField]
    private GameObject todoPrefab;  
...
    private void CreateTodoUi(int index, TodoDto todoDto)
    {
        GameObject todoGo = Instantiate(todoPrefab, transform.position, Quaternion.identity, transform);
        TodoCellView todoView = todoGo.GetComponent<TodoCellView>();

        todoView.SetTodo(index, todoDto);
    }
...
}
public class TodoCellView : MonoBehaviour
{
    [SerializeField]
    private TMP_Text title;

    [SerializeField]
    private TMP_Text status;
...
    private TodoDto todo;
    private int index;

    public void SetTodo(int indexList, TodoDto todoDto)
    {
        this.todo = todoDto;
        this.index = indexList;

        UpdateTodoView();
...
    }

    private void UpdateTodoView()
    {
        TimeSpan remainTime = expireService.CalcRemainTime(todo.CreatedDate);
        string statusText = todo.Status == TodoStatus.Active
            ? $"残り{remainTime:dd}日"
            : todo.Status.ToString();
...
        title.text = $" {index}:\t{todo.Text}";
        status.text = statusText;
    }
...
}

image.png

Test

コンソールアプリ側で共通コードのユニットテストができます。
UnityEditorが必要ないため、高速にテスト実行が可能です。

下記部分では空文字など無効な内容のTodo生成を実行すると例外が発生することをテストしています。

public class Todo_Test
{
...
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("  ")]
    public void Todo_CreateInvalidTextTodo_Fail(string textTodo)
    {
        var funcTodo = () => new Todo(textTodo) { CreatedDate = new DateTime(2020, 12, 3) };
        funcTodo.Should().Throw<ArgumentException>();
    }
}

まとめ

オニオンアーキテクチャを使って、Unityと.NETでコードを共有しながらTODOアプリを作成する方法について説明しました。
この方法のメリットとして以下が挙げられます。

  • Unityへの依存を減らす
    • View層以外はUnityに依存しなくなります
    • これにより、他のプラットフォームへの移植性が高まります
  • テスト容易性の向上
    • Unityの実行環境を必要としない単体テストが可能になります
    • UnityEditorのロードが必要ないので、コード修正→テスト実行の待ち時間が減少します

環境

  • UnityEditor: Unity 2022.3.22
  • VisualStudio: 2022 17.12.3

参考文献

3
2
0

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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?