インベントリシステム(持ち物)サンプルのリファクタリングいろいろ

2023年10月16日

インベントリシステムは業務アプリケーションやゲームなど幅広く使われている仕組みです
プログラミングの基本構成の1つと考えていいでしょう

ここでは、どのように考えていくのかをみていくことにしましょう

仕様を考える

今回は、インベントリシステムのアイテム管理に絞って考えていきます

アイテム管理

ゲーム内のアイテムやリソース(武器、防具、食料、材料など)を一元管理し、プレイヤーキャラクターが持っているものを表示します。

クラス図で確認する構成

アイコンの説明

最初にクラス図で使われているアクセス修飾子の説明をしておきます

アクセス修飾子は、クラス設計やソフトウェアアーキテクチャの設計において、情報隠蔽やセキュリティの観点から非常に重要です。 PlantUMLを使用することで、これらのアクセス修飾子を含むクラス図を効果的に表現できます。

クラス図

以降のコードの「List版に戻して比較」をベースにしています

クラス図の説明

  1. ItemType (列挙型): ItemType は列挙型で、アイテムの種類を表します。この列挙型には “斧" と “剣" の2つの値が含まれています。
  2. OwnedItem (クラス): OwnedItem は、所有しているアイテムを表すクラスです。このクラスには以下のフィールドとメソッドが含まれています:
    • Type フィールド: アイテムの種類を保持します。
    • Number フィールド: アイテムの数量を保持します。
    • OwnedItem(type: ItemType, number: int = 0) コンストラクタ: アイテムの種類と数量を指定して OwnedItem オブジェクトを初期化します。
    • IncreaseNumber(number: int) メソッド: アイテムの数量を増やします。
  3. Inventory (クラス): Inventory はアイテムの在庫を管理するクラスで、所有している複数の OwnedItem オブジェクトを保持します。このクラスには以下のフィールドとメソッドが含まれています:
    • ownedItems フィールド: OwnedItem オブジェクトのリストを保持します。
    • Add(type: ItemType, number: int = 1) メソッド: アイテムを在庫に追加し、既存のアイテムがある場合は数量を増やします。
    • PrintOwnedItems() メソッド: 所有しているアイテムの一覧をコンソールに出力します。
  4. Program (クラス): Program はアプリケーションのエントリーポイントを含むクラスで、Inventory クラスを使用してアイテムの追加と表示を行います。

クラス図はこれらのクラスとそれらの間の関連を視覚的に示しており、アイテムの種類、在庫管理、アイテムの数量増加、およびアプリケーションのエントリーポイントとの関連を表現しています。

様々なリファクタリング

リファクタリングをしていく経緯を含め、複数のコードサンプルを示します
どのコードも同じ動きをします
様々な実装方法があり、その仕様に応じて考えていくことになります

最初からベストを求める必要はないでしょう
また、自分の知識範疇から始めるのが良いでしょう
知識がついてくれrば、新しく知った仕組みを採用してみると良いでしょう
Gitが管理すれば、自分の成長も感じることができるでしょう

初期バージョン

Mainメソッドを記述しないトップレベルステートメントとしています
class Inventoryの宣言前までがMainメソッドのブロック内に該当します

Inventory inventory = new Inventory();

inventory.ownedItems.Add(new Inventory.OwnedItem(Item.ItemType.斧));
inventory.ownedItems.Add(new Inventory.OwnedItem(Item.ItemType.剣));

inventory.Add(Item.ItemType.斧, 3);
inventory.Add(Item.ItemType.剣, 10);

foreach (var item in inventory.ownedItems)
{
    Console.WriteLine($"{item.Type}:{item.Number}");
}

class Inventory
{
    public List<OwnedItem> ownedItems = new List<OwnedItem>();

    public class OwnedItem
    {
        public Item.ItemType Type { get; }
        public int Number { get; set; }

        public OwnedItem(Item.ItemType type)
        {
            Type = type;
        }

        public void Add(int number)
        {
            Number += number;
        }
    }

    public void Add(Item.ItemType type, int number)
    {
        var item = GetOwnedItem(type);
        item.Add(number);
    }
    public OwnedItem GetOwnedItem(Item.ItemType type)
    {
        return ownedItems.FirstOrDefault(x => x.Type == type);
    }
}

public class Item
{
    public enum ItemType
    {
        斧,
        剣,
    }

    public ItemType itemType;

    public Item(ItemType itemType)
    {
        this.itemType = itemType;
    }
}

斧:3
剣:10

アイテムの追加で種類だけを引数にすると新しいインスタンスを作成

using System.Linq;

Inventory inventory = new Inventory();

inventory.Add(Item.ItemType.斧);
inventory.Add(Item.ItemType.剣);

inventory.Add(Item.ItemType.斧, 3);
inventory.Add(Item.ItemType.剣, 10);

foreach (var item in inventory.ownedItems)
{
    Console.WriteLine($"{item.Type}:{item.Number}");
}

class Inventory
{
    public List<OwnedItem> ownedItems = new List<OwnedItem>();

    public class OwnedItem
    {
        public Item.ItemType Type { get; }
        public int Number { get; set; }

        public OwnedItem(Item.ItemType type)
        {
            Type = type;
        }

        public void Add(Item.ItemType type, int number)
        {

            Number += number;
        }
    }

    public void Add(Item.ItemType type)
    {
        var item = new OwnedItem(type);
        ownedItems.Add(item);
    }

    public void Add(Item.ItemType type, int number)
    {
        var item = GetOwnedItem(type);
        item.Number += number;
    }
    public OwnedItem GetOwnedItem(Item.ItemType type)
    {
        return ownedItems.FirstOrDefault(x => x.Type == type);
    }
}

public class Item
{
    public enum ItemType
    {
        斧,
        剣,
    }

    public ItemType itemType;

    public Item(ItemType itemType)
    {
        this.itemType = itemType;
    }
}

Dictionaryを採用

using System;
using System.Collections.Generic;
using System.Linq;

class Inventory
{
    private Dictionary<Item.ItemType, OwnedItem> ownedItems = new Dictionary<Item.ItemType, OwnedItem>();

    public void Add(Item.ItemType type)
    {
        if (!ownedItems.ContainsKey(type))
        {
            ownedItems[type] = new OwnedItem(type);
        }
    }

    public void Add(Item.ItemType type, int number)
    {
        if (!ownedItems.ContainsKey(type))
        {
            ownedItems[type] = new OwnedItem(type);
        }

        ownedItems[type].IncreaseNumber(number);
    }

    public void PrintOwnedItems()
    {
        foreach (var item in ownedItems.Values)
        {
            Console.WriteLine($"{item.Type}:{item.Number}");
        }
    }
}

class OwnedItem
{
    public Item.ItemType Type { get; }
    public int Number { get; private set; }

    public OwnedItem(Item.ItemType type)
    {
        Type = type;
    }

    public void IncreaseNumber(int number)
    {
        Number += number;
    }
}

public class Item
{
    public enum ItemType
    {
        斧,
        剣,
    }
}

class Program
{
    static void Main()
    {
        Inventory inventory = new Inventory();

        inventory.Add(Item.ItemType.斧);
        inventory.Add(Item.ItemType.剣);

        inventory.Add(Item.ItemType.斧, 3);
        inventory.Add(Item.ItemType.剣, 10);

        inventory.PrintOwnedItems();
    }
}

主な変更点は以下の通りです:

  1. Inventory クラス内で所有アイテムの情報を Dictionary で管理するように変更し、アイテムの種類をキーとして扱います。これにより、アイテムの検索が効率的に行えます。
  2. OwnedItem クラスの Number プロパティを private set に変更し、外部から直接アクセスできないようにしました。アイテムの数を変更するために、 IncreaseNumber メソッドを使用します。
  3. PrintOwnedItems メソッドを Inventory クラスに追加し、所有アイテムの一覧を表示するための専用メソッドを提供しました。
  4. Main メソッドを含む Program クラスを追加し、実行可能なプログラムのエントリーポイントとして使用します。

これにより、コードがより整理され、拡張性が向上し、アイテム管理が効率的に行えるようになります。

メソッドのオーバーロードをデフォルト引数に置き換え

using System;
using System.Collections.Generic;
using System.Linq;

class Inventory
{
    private Dictionary<Item.ItemType, OwnedItem> ownedItems = new Dictionary<Item.ItemType, OwnedItem>();

    public void Add(Item.ItemType type, int number = 1)
    {
        if (!ownedItems.ContainsKey(type))
        {
            ownedItems[type] = new OwnedItem(type);
        }

        ownedItems[type].IncreaseNumber(number);
    }

    public void PrintOwnedItems()
    {
        foreach (var item in ownedItems.Values)
        {
            Console.WriteLine($"{item.Type}:{item.Number}");
        }
    }
}

class OwnedItem
{
    public Item.ItemType Type { get; }
    public int Number { get; private set; }

    public OwnedItem(Item.ItemType type)
    {
        Type = type;
    }

    public void IncreaseNumber(int number)
    {
        Number += number;
    }
}

public class Item
{
    public enum ItemType
    {
        斧,
        剣,
    }
}

class Program
{
    static void Main()
    {
        Inventory inventory = new Inventory();

        inventory.Add(Item.ItemType.斧);
        inventory.Add(Item.ItemType.剣);

        inventory.Add(Item.ItemType.斧, 3);
        inventory.Add(Item.ItemType.剣, 10);

        inventory.PrintOwnedItems();
    }
}

ドキュメントコメントの追加

using System;
using System.Collections.Generic;

/// <summary>
/// アイテムの種類を表す列挙型
/// </summary>
public enum ItemType
{
    斧, // 斧のアイテムタイプ
    剣, // 剣のアイテムタイプ
}

/// <summary>
/// アイテムの在庫を管理するクラス
/// </summary>
public class Inventory
{
    private Dictionary<ItemType, OwnedItem> ownedItems = new Dictionary<ItemType, OwnedItem>();

    /// <summary>
    /// アイテムを在庫に追加します。
    /// </summary>
    /// <param name="type">アイテムの種類</param>
    /// <param name="number">アイテムの数量 (デフォルト値: 1)</param>
    public void Add(ItemType type, int number = 1)
    {
        if (!ownedItems.ContainsKey(type))
        {
            ownedItems[type] = new OwnedItem(type); // アイテムを所有アイテムリストに追加
        }

        ownedItems[type].IncreaseNumber(number); // アイテムの数を増やす
    }

    /// <summary>
    /// 所有しているアイテムの一覧をコンソールに出力します。
    /// </summary>
    public void PrintOwnedItems()
    {
        foreach (var item in ownedItems.Values)
        {
            Console.WriteLine($"{item.Type}:{item.Number}"); // 所有アイテムを表示
        }
    }
}

/// <summary>
/// 所有しているアイテムを表すクラス
/// </summary>
public class OwnedItem
{
    /// <summary>
    /// アイテムの種類
    /// </summary>
    public ItemType Type { get; } // アイテムのタイプ

    /// <summary>
    /// アイテムの数量
    /// </summary>
    public int Number { get; private set; } // アイテムの数

    /// <summary>
    /// OwnedItem クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="type">アイテムの種類</param>
    public OwnedItem(ItemType type)
    {
        Type = type;
    }

    /// <summary>
    /// アイテムの数量を増やします。
    /// </summary>
    /// <param name="number">増やす数量</param>
    public void IncreaseNumber(int number)
    {
        Number += number; // アイテムの数を増やす
    }
}

/// <summary>
/// アプリケーションのエントリーポイントを含むクラス
/// </summary>
public class Program
{
    /// <summary>
    /// アプリケーションのエントリーポイント
    /// </summary>
    public static void Main()
    {
        Inventory inventory = new Inventory();

        inventory.Add(ItemType.斧);
        inventory.Add(ItemType.剣);

        inventory.Add(ItemType.斧, 3);
        inventory.Add(ItemType.剣, 10);

        inventory.PrintOwnedItems(); // 所有アイテムを表示
    }
}

List版に戻して比較

/// <summary>
/// アイテムの種類を表す列挙型
/// </summary>
public enum ItemType
{
    斧, // 斧のアイテムタイプ
    剣, // 剣のアイテムタイプ
}

/// <summary>
/// アイテムの在庫を管理するクラス
/// </summary>
public class Inventory
{
    private List<OwnedItem> ownedItems = new List<OwnedItem>();

    /// <summary>
    /// アイテムを在庫に追加します。既存のアイテムは数量を増やします。
    /// </summary>
    /// <param name="type">アイテムの種類</param>
    /// <param name="number">アイテムの数量 (デフォルト値: 1)</param>
    public void Add(ItemType type, int number = 1)
    {
        // 既存のアイテムがあるか確認
        OwnedItem existingItem = ownedItems.FirstOrDefault(item => item.Type == type);

        if (existingItem != null)
        {
            existingItem.IncreaseNumber(number); // 既存のアイテムの数量を増やす
        }
        else
        {
            OwnedItem newItem = new OwnedItem(type, number);
            ownedItems.Add(newItem); // 新しいアイテムをリストに追加
        }
    }

    /// <summary>
    /// 所有しているアイテムの一覧をコンソールに出力します。
    /// </summary>
    public void PrintOwnedItems()
    {
        foreach (var item in ownedItems)
        {
            Console.WriteLine($"{item.Type}:{item.Number}"); // 所有アイテムを表示
        }
    }
}

/// <summary>
/// 所有しているアイテムを表すクラス
/// </summary>
public class OwnedItem
{
    /// <summary>
    /// アイテムの種類
    /// </summary>
    public ItemType Type { get; }

    /// <summary>
    /// アイテムの数量
    /// </summary>
    public int Number { get; private set; }

    /// <summary>
    /// OwnedItem クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="type">アイテムの種類</param>
    /// <param name="number">アイテムの数量 (デフォルト値: 0)</param>
    public OwnedItem(ItemType type, int number = 0)
    {
        Type = type;
        IncreaseNumber(number);
    }

    /// <summary>
    /// アイテムの数量を増やします。
    /// </summary>
    /// <param name="number">増やす数量</param>
    public void IncreaseNumber(int number)
    {
        Number += number; // アイテムの数を増やす
    }
}

/// <summary>
/// アプリケーションのエントリーポイントを含むクラス
/// </summary>
public class Program
{
    /// <summary>
    /// アプリケーションのエントリーポイント
    /// </summary>
    public static void Main()
    {
        Inventory inventory = new Inventory();

        inventory.Add(ItemType.斧);
        inventory.Add(ItemType.剣);

        inventory.Add(ItemType.斧, 3);
        inventory.Add(ItemType.剣, 10);

        inventory.PrintOwnedItems(); // 所有アイテムを表示
    }
}

さらに付け加える機能など

シングルトンにする

唯一のインスタンスとして(1つしか作れないようにする)使えるようにしてみましょう

保存機能の追加

PlayerPrefsクラス

string型のデータを外部記憶装置に保存読み出しできるようにしましょう

Jsonでの管理

持ち物のリストのインスタンスをJsonフォーマットに変換して外部記憶装置に保存読み出しできるようにしましょう
Saveメソッドを追加してみましょう

持ち物を使う機能の追加

Useメソッドを追加してみましょう

インナークラスの採用

ItemType列挙型をまたItemクラスのブロックに戻して、ownedItemクラスをInventoryクラスのインナークラスとしてみましょう

補足

リストに含まれているかをチェックするコード

OwnedItem existingItem = ownedItems.FirstOrDefault(item => item.Type == type);

このコードは、C#言語で書かれたコードの一部であり、リストまたはコレクション内で指定された条件に一致する最初の要素を見つけるためのLINQ(Language-Integrated Query)クエリを使用しています。以下にコードの要点を説明します:

  1. ownedItemsは、リストやコレクションのようなデータ構造を表す変数です。このリストにはOwnedItemという型の要素が含まれているものと仮定します。
  2. FirstOrDefaultメソッドは、LINQクエリを使用してリスト内で指定された条件に一致する最初の要素を取得するためのメソッドです。このメソッドは、条件に一致する要素が見つからない場合にはデフォルトの値を返します。
  3. item => item.Type == typeは、LINQのラムダ式です。これは、リスト内の各要素に対して条件をチェックし、Typeプロパティが指定されたtype変数の値と一致する要素を探します。
  4. existingItemは、条件に一致する最初の要素を格納するための変数です。条件に一致する要素が見つからない場合、existingItemにはデフォルトの値(nullまたはdefault(OwnedItem))が格納されます。

したがって、このコードは、ownedItemsリスト内の要素から、指定されたtypeに一致する最初の要素を検索し、その要素をexistingItem変数に格納します。この方法は、特定の型やプロパティに基づいて要素をフィルタリングおよび検索するために一般的に使用されます。

item => item.Type == type

このラムダ式は、引数がitemという名前の変数で、条件式がitem.Type == typeである匿名関数を表しています。分解して説明します:

  1. item:これはラムダ式の引数です。itemはコレクション内の各要素を代表します。
  2. =>:この記号はラムダ式のパラメータリストと本体を区別します。左側が引数リストで、右側が本体です。
  3. item.Type == type:これはラムダ式の本体です。item.Typeは、itemオブジェクトのTypeプロパティを表し、typeは外部の変数です。

このラムダ式は、コレクション内の各要素(item)に対して、その要素のTypeプロパティが外部のtype変数の値と一致するかどうかをチェックします。ラムダ式の評価結果は真偽値(trueまたはfalse)で、条件が一致した場合にtrueを返し、それ以外の場合にfalseを返します。このラムダ式はLINQクエリ内で使用され、条件に一致する最初の要素を見つけるために使用されます。

メソッドに置き換えると

ラムダ式をメソッドに置き換えることは可能です。これにより、コードがより読みやすくなり、同じ条件を複数の場所で再利用できます。次に、ラムダ式をメソッドに置き換えた例を示します

元のラムダ式

item => item.Type == type

これをメソッドに変換すると

private bool IsMatchingType(OwnedItem item, ItemType type)
{
    return item.Type == type;
}

新しいメソッド IsMatchingType は OwnedItem 型の item オブジェクトと ItemType 型の type パラメータを受け取り、条件を評価して真偽値を返します。これでラムダ式の代わりにメソッドを使用することができます。

LINQクエリ内でメソッドを使用する場合、次のようになります

OwnedItem existingItem = ownedItems.FirstOrDefault(item => IsMatchingType(item, type));

このように、IsMatchingType メソッドを呼び出すことで、コードがより読みやすくなり、条件の詳細をメソッド内でカプセル化できます。また、同じ条件を複数の場所で再利用する際にも便利です。

参考

PlantUMLクラス図アクセス修飾子説明

このPlantUMLのコードでは、MyClassというクラスを定義して、さまざまなアクセス修飾子を示しています。アクセス修飾子の記号とその意味は以下の通りです:

  • -: プライベート (Private) アクセス修飾子は、同じクラス内からのみアクセスできるフィールドやメソッドを示します。他のクラスからはアクセスできません。
  • #: プロテクテッド (Protected) アクセス修飾子は、同じクラス内およびそのサブクラスからのみアクセスできるフィールドやメソッドを示します。
  • ~: パッケージ (Package) アクセス修飾子は、同じパッケージ内からのみアクセスできるフィールドやメソッドを示します。パッケージは特定のプログラミング言語の概念で、Javaなどで使用されます。
  • +: パブリック (Public) アクセス修飾子は、どのクラスからでもアクセスできるフィールドやメソッドを示します。

アクセス修飾子は、クラス設計やソフトウェアアーキテクチャの設計において、情報隠蔽やセキュリティの観点から非常に重要です。 PlantUMLを使用することで、これらのアクセス修飾子を含むクラス図を効果的に表現できます。

@startuml
class MyClass {
  - privateField: int
  # protectedField: string
  ~ packageField: double
  + publicField: boolean

  - privateMethod()
  # protectedMethod()
  ~ packageMethod()
  + publicMethod()
}

MyClass --|> AnotherClass : Inheritance
@enduml

クラス図コード

@startuml

' クラスの定義
enum ItemType {
  斧
  剣
}

class OwnedItem {
  - Type: ItemType
  - Number: int
  + OwnedItem(type: ItemType, number: int = 0)
  + IncreaseNumber(number: int)
}

class Inventory {
  - ownedItems: List<OwnedItem>
  + Add(type: ItemType, number: int = 1)
  + PrintOwnedItems()
}

class Program {
  + Main()
}

' 関連
OwnedItem --o| ItemType : 包含
Inventory *-- OwnedItem : 所有
Program --> Inventory : 使用

@enduml

innerClassProject

Unity

Posted by hidepon