C#でのオブジェクト指向学習サンプル6(レジ、商品情報は、必ずどの分類かに限定してほしいし、必ず限定の表示をしてほしい)

お肉や本まで品揃えしているスーパーで、どの商品の情報もいずれかの種類に分類されるようにしてほしい

前回までのサンプル

ベースとなる基本レジ

シンプルで、これはこれでいいと思いますが、さらに現実のレジに近づけてみましょう

レジ、マイナスの商品は買えないようにする

C#のプロパティの機能を採用して、堅牢なコードに変更します
これは、オブジェクト指向の3大要素と呼ばれるカプセル化にあたります

レジ、購入商品数を最初に入力しなくてもよくする

Listクラスの機能を採用して、要素数が実行中に変更可能な配列のように変更します
これにより最初に買い物する商品数の入力が不要になりました

レジ、購入はバーコード番号入力だけにする

商品一覧をレジクラスに持たせることで、お客はバーコードだけをレジに通すと精算ができるようにしました

レジ、商品の種類によってレシート表示を変えて欲しい

継承、ポリモーフィズムの機能を使って、まとめて管理しやすく、また変更もしやすいようにしましたね

新たに実現したい機能

商品情報は、必ずどの分類かに限定してほしいし、必ず限定の表示をしてほしい

分類かどうかについて

ここでいう分類かどうかは次のように考えてください

「商品」というのは分類ではないのです(抽象的なので特定されないと考えます)
「お肉」や「本」は分類です(具体なので「商品」よりは特定できます)

変更点の概要

まず、「商品」では登録できないようにしましょう(つまり商品のクラスからインスタンスを作成できないようにする)
次に、分類では、必ず「Showメソッド」を実装しなけらばならないようにしましょう

これらの機能はこれまでの知識でも頑張ればできますが、楽に実現するために、新しくC#オブジェクト指向の抽象クラスを使った継承とポリモーフィズムの仕組みを使ってみます

実行結果

前回の商品の種類による実行結果を全く同じです(なので、結果もコピペしています)

お客(山田さん)が買い物かごに買いたい商品を入れていきます
必要なものを入れたらレジで精算(レシート表示)します

最初に商品のバーコードを入力してもらっています
全ての買い物を終わったら単にエンターキーを押して買い物を終えたことにします

クラス図

イラスト

変更点はありません

全体システム

商品クラスについての追加・更新

商品一覧には、商品が複数登録されています
今回は商品をお肉と本に分けてそれぞれの特徴についてはそれぞれの固有の属性として持つようにします

レシートにどのように表示するかを表したものです
種類別で表示を変更(呼び出されるメソッドが選択される)様子を示しています

UML図

C#の初学であれば、次のポイントに注意して確認してください

クラスはブロック内を2つの部分に分けて分析してみます

属性振る舞いの2つです

クラスの形

2つの部分他の一般的な呼ばれ方プログラム言語での呼び方
属性データ部分、名詞部分、情報、性質の情報、パラメータ(雰囲気で)、特徴、などプロパティ、フィールド、など
静的型付け言語(JavaやC#など)の場合、
型名と名前で宣言されています
振る舞い操作、動作、動きの部分、アクション、動詞部分、などメソッド、関数、プロシジャーなどとも呼ばれています
ブロック内の手続型のコードを記述します
(手続型は、上から下に順次実行が基本。条件分岐や繰り返し処理など組み合わせます)

クラス図のサンプル

サンプルクラスの読み方は、
「購入済み商品クラスで、属性としてバーコードを持っている、振る舞いとしてレシートを表示する」と考えます
コンストラクタとは、newキーワードで新しくレジ(1号機、2号機のあるスーパーを想定)を導入した当時、1回だけ実行される振る舞いと考えます

前回のクラス図

レジクラスは商品マスター(商品一覧)を同様に保持しています
Item型のオブジェクトを複数持っているというのは変わらないのでですが、実際には、お肉商品と本商品のクラスからオブジェクトが作られています

C#的に表現すると、基底クラスの宣言された変数には、派生クラスのオブジェクトの代入が可能といいます

今回のクラス図

今回の変更箇所を取り込んだ最終形を示しています
抽象クラス(抽象的)なので、これからインスタンスを作成できない機能?があります
抽象メソッド(抽象的)なので、ここにメソッドのブロックは書けない機能?があります
代わりに派生クラスに絶対記述するように強制します

シーケンス図

変更点はありません

図中の表現

loopループ(繰り返し)処理を表します
alt(Alternative)分岐処理を表します

今回のシーケンス図

基本の形は変えていますので注意してください

クラス名の英単語化等、日本語だけの表記を更新しています

コード

メソッドは、ブロック内を全て理解することを目的にせず、メソッド名から何の動作をさせようとしているのかを分析対象とします

PurchasedItem.cs(購入済み商品)

変更点はありません

お客が購入する商品のクラスになります(購入済み商品)
スーパーでの商品に貼ってあるラベルのイメージです

買い物かご(items)に登録する1つ1つの購入済み商品のクラスになります

買い物かごは、お客が保持しています(持っています)
実際購入する場合、バーコードをスキャナで読ませればOKなので、そのような状況に合わせてみました

属性

バーコードのみ
コンストラクタで、バーコードが代入できるようにしています

振る舞い

ありません

class PurchasedItem
{
    public string barCode;

    public PurchasedItem(string barCode)
    {
        this.barCode = barCode;
    }
}

Item.cs(商品マスターの登録用の商品)

商品マスター(商品一覧)に登録する1つ1つの商品のクラスになります
商品マスターのデータの構造を定義しています
商品マスターは、レジが保持しています(持っています)

第1段階 商品そのものからインスタンスを作れないようにする

変更点は、クラスにabstract修飾子の追加になります

abstractとは英単語で「抽象」です
この追加により、このクラスではインスタンスを作成できないことを明示できます

試しにRegクラスでItemクラスからインスタンスを作成してみますが、インテリセンスでエラー表示され実行できません

属性

バーコード商品名価格の3つで考えます

振る舞い

商品名と価格を表示するのみになります

// Itemクラス(設計図)
/// <summary>
/// スーパーの商品クラス
/// </summary>
abstract class Item
{
    // (フィールド)
    // バーコード
    public string barCode;
    // 商品名
    public string name;

    // 価格
    // このクラス外からはアクセスできないフィールド(private修飾子)
    private int price;

    // (プロパティ)public修飾子なのでクラス外からアクセスできる
    public int Price
    {
        get
        {
            // 読み込まれた場合の処理
            return price;
        }
        set
        {
            // 書き込まれた場合の処理
            if (value < 0)
            {
                price = 0;
            }
            else
            {
                price = value;
            }
        }
    }

    // ------------------------------------

    // (メソッド、関数、プロシジャー、ファンクション)
    // 商品名と価格を表示する
    public virtual void Show()
    {
        Console.WriteLine($"{name}の価格は{price}円です");
    }
}

第2段階 必ず限定の表示をする

変更点は、Showメソッドに強制力を持たせるためにabstract修飾子の追加になります

メソッドにabstract修飾子を追加すると、派生クラスに強制的にメソッドを実装させることができるようになります

なお、abstract修飾子を追加すると、ブロックにコードを記述する必要がないため(派生クラスが常に呼び出される)、コードを残しておくとエラーになります

今回は、商品クラスに商品名と価格を表示させる振る舞いを残したいので、新しくShowBaseInfoメソッドを作成して、そこに商品の基本情報表示用の文を移動します

変更手順

属性

バーコード商品名価格の3つで考えます

振る舞い

商品名と価格を表示するのと派生クラスに種類ごとの表示を強制する抽象メソッドになります

// Itemクラス(設計図)
/// <summary>
/// スーパーの商品クラス
/// </summary>
abstract class Item
{
    // (フィールド)
    // バーコード
    public string barCode;
    // 商品名
    public string name;

    // 価格
    // このクラス外からはアクセスできないフィールド(private修飾子)
    private int price;

    // (プロパティ)public修飾子なのでクラス外からアクセスできる
    public int Price
    {
        get
        {
            // 読み込まれた場合の処理
            return price;
        }
        set
        {
            // 書き込まれた場合の処理
            if (value < 0)
            {
                price = 0;
            }
            else
            {
                price = value;
            }
        }
    }

    // ------------------------------------

    // (メソッド、関数、プロシジャー、ファンクション)
    // 商品名と価格を表示する
    public void ShowBaseInfo()
    {
        Console.WriteLine($"{name}の価格は{price}円です");
    }

    // 派生クラスにShowメソッドの実装を強制する
    public abstract void Show();
}

MeatItem.cs(お肉商品)

商品の基本情報(商品名と価格)の表示のメソッドを商品クラス側で分けましたので、分けたメソッドを呼ぶようにします

Itemクラスを継承した、お肉商品クラスになります
Itemクラスのメンバーを全て網羅していて使うことができます
それにプラスして、このクラスのメンバーも使いことができます

メソッドにoverride修飾子追加されています
これは、派生クラスでメソッドを置き換えることができることを示しています

属性

賞味期限のみです

振る舞い

賞味期限を表示します

商品名価格を表示したのち、賞味期限も表示します

class MeatItem : Item
{
    public string expiryDate;

    public override void Show()
    {
        // 基底クラスのメソッドを呼び出す
        ShowBaseInfo();
        Console.WriteLine( $"賞味期限 {expiryDate}");
    }
}

基底クラスで抽象メソッドが定義されているため、派生クラスでのShowメソッドの実装は強制になります

次のようにShowメソッドを削除(ビデオ中ではコメント)した時の様子を確認してみましょう

BookItem.cs(本商品)

商品の基本情報(商品名と価格)の表示のメソッドを商品クラス側で分けましたので、分けたメソッドを呼ぶようにします

Itemクラスを継承した、本商品クラスになります
Itemクラスのメンバーを全て網羅していて使うことができます
それにプラスして、このクラスのメンバーも使いことができます

メソッドにoverride修飾子が追加されています
これは、派生クラスでメソッドを置き換えることができることを示しています

属性

著作者のみです

振る舞い

商品名価格を表示したのち、著作者も表示します

class BookItem : Item
{
    public string auther;

    public override void Show()
    {
        // 基底クラスのメソッドを呼び出す
        ShowBaseInfo();
        Console.WriteLine($"著者 {auther}");
    }
}

Customer.cs(お客)

変更点はありません

お客クラスになります
購入済み商品を複数持っていることを表しています

属性

お客様名複数の購入済み商品の2つで考えます

振る舞い

インスタンス作成時、名前を登録する購入するの2つになります

class Customer
{
    public string name;

    // カートの商品
    public List<PurchasedItem> items;

    public Customer(string name)
    {
        this.name = name;
    }

    public void Buy()
    {
        // PurchasedItemクラス(購入済み商品)からインスタンスを作成する(オブジェクトを作成)
        // =>インスタンス化ともいいます

        items = new List<PurchasedItem>();

        while (true)
        {
            Console.Write("バーコード: ");
            string barCode = Console.ReadLine();

            if (barCode == "")
            {
                break;
            }

            items.Add(new PurchasedItem(barCode));
        }

    }

}

Reg.cs(レジスター)

変更点はありませんが、コード

 findItem.Show();

は、Itemクラスで強制的に実装されていることが約束されたメソッドなので「存在しない」ことによるエラーは発生しないです

レジクラスになります

属性

商品マスター(商品一覧)を持ちます

振る舞い

レシートを表示するのみになります

class Reg
{
    // 商品マスター(商品一覧)
    List<Item> items = new List<Item>();

    public Reg()
    {
        // コンストラクタで商品マスターに商品を初期登録しています
        Item item1 = new MeatItem { name = "鶏肉", barCode = "1234", Price = 100,expiryDate = "20231010" };
        Item item2 = new BookItem { name = "三四郎", barCode = "2345", Price = 200,auther = "夏目漱石" };

        items.Add(item1);
        items.Add(item2);
    }

    // レシートを表示する
    public void PrintReceipt(Customer customer)
    {
        //customer.items[0].Price = -100;
        Console.WriteLine($"{customer.name}さんのレシート");
        Console.WriteLine("----------------------------");

        // 合計金額を入れる変数を用意する
        int totalPrice = 0;

        // レシートの表示
        foreach (var item in customer.items)
        {
            // LINQを使って、お客が持っている購入済み商品のバーコードと商品マスター(一覧)のバーコードの一致を探し、
            // 結果をfindItem変数に代入しています
            var findItem = items.FirstOrDefault(itemMaster => item.barCode == itemMaster.barCode);

            // 商品名を表示する
            findItem.Show();

            totalPrice += findItem.Price;
        }


        Console.WriteLine("----------------------------");

        // 合計を表示する
        Console.WriteLine($"合計金額は{totalPrice}円です");
    }

}

RegSystem.cs(レジのアプリをコントロール)

変更点はありません(他の人の場合もコメントで追加はしています)

レジのシステム全体をコントロールするクラスになります

属性

ありません

振る舞い

Runメソッドからコードが始まります

ここでは、お客様の山田さんの作成買い物をするレジの作成レシートを表示するを行なっています

class RegSystem
{
    public void Run()
    {

        // レジオブジェクトを作成
        Reg reg1 = new Reg();

        // 山田さんを作成
        Customer customer1 = new Customer("山田");
        customer1.Buy();
        reg1.PrintReceipt(customer1);

        /*
        Customer customer2 = new Customer("森本");
        Customer customer3 = new Customer("太田");

        // 森本さんが先に購入
        customer2.Buy();
        // 森本さんがそのまま清算
        reg1.PrintReceipt(customer2);
        customer3.Buy();

        reg1.PrintReceipt(customer3);
        */
    }
}

Program.cs(エントリーポイント。このクラスのMainメソッドからプログラムは開始されます)

変更点はありません

C#でプログラムが実行される最初のメソッドになります

属性

ありません

振る舞い

トップレベルステートメントとしてコードを記述していますので、Programクラスに記述されているMainメソッドと同じブロックになります

ここでは、レジシステムの作成レジの実行を行なっています
このコードが実行されると、RegSystemクラスのRunメソッドが実行されます

RegSystem regSystem = new RegSystem();
regSystem.Run();

おまけ

PlantUML記法から図を作成するネットのサービス(無料)

シーケンス図

@startuml

actor Customer
participant "RegSystem" as RegSys
participant "Reg" as Reg
participant "Customer" as Cust
participant "MeatItem" as Meat
participant "BookItem" as Book
participant "Item" as Itm
database "items" as Items

Customer -> RegSys: Run()
RegSys -> Reg: new Reg()
RegSys --> Cust: new Customer("山田")
Cust -> Cust: Buy()
loop 購入商品入力
    Cust -> Cust: 商品バーコード入力
    Cust -> Cust: Enterキー入力
end
Cust -> Reg: PrintReceipt(customer1)
Reg -> Items: 商品一覧取得
loop 購入商品のループ
    Reg -> Items: 商品マスター検索
    Items --> Reg: 商品情報
    Reg -> Itm: Show()
    alt 商品がBookItemの場合
        Itm -> Book: Show()
    else 商品がMeatItemの場合
        Itm -> Meat: Show()
    end
    Reg --> Cust: 商品情報表示
end
Reg --> Cust: 合計金額表示

@enduml

クラス図

@startuml

class Program{
    -Main()
}

class Customer {
    +name: string
    +items: List<PurchasedItem>
    +Customer(name: string)
    +Buy()
}

abstract class Item {
    + barCode : string
    + name : string
    - price : int
    + Price : int <<get>> <<set>>
    + ShowBaseInfo() : void
    + {abstract} Show() : void
}

class MeatItem {
    + expiryDate : string
    + <<override>> Show() : void
}

class BookItem {
    + auther : string
    + <<override>> Show() : void
}

class PurchasedItem {
    +barCode: string
    +PurchasedItem(barCode: string)
}

class Reg {
    -items: List<Item>
    +Reg()
    +PrintReceipt(customer: Customer)
}

class RegSystem {
    +Run()
}

Customer --> PurchasedItem : has 1..* >
Reg --> Item : has 1..* >
Program --> RegSystem : creates >
RegSystem --> Customer : create >
RegSystem --> Reg : creates >
RegSystem --> Customer : uses >
RegSystem --> Reg : uses >
Item <|-- BookItem : < inheritance
Item <|-- MeatItem : < inheritance
@enduml