C#でのオブジェクト指向学習サンプル8(レジ、賞味期限切れの処理を各種類ごとでも実行したい)

2023年8月4日

賞味期限切れの時の処理は賞味期限切れのグループで共通化されているけど、個別の種類(お肉や魚)のクラスでも実行できるようにしたい

前回までのサンプル

ベースとなる基本レジ

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

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

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

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

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

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

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

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

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

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

抽象クラスを使った継承ポリモーフィズムの機能を使って、制約も持てるようにしました

レジ、賞味期限があるグループは限定の表示をしたい

インターフェースを使ったポリモーフィズムの機能を使って、制約も持てるようにしました
抽象クラスとの違いは、is-aの関係を保たなくても良いことと、多重継承(派生クラスで2つの基底クラスを持てない)をクリアすること

新たに実現したい機能

現在、商品は、お肉グループと本グループに分類されています
is-a の関係が成り立たない(賞味期限がある)ケースでインターフェースを使ったグループ分けをしたが、各種類のクラス(肉)にも特定のメソッドの実装を強制させたい

C#のバージョン8から、それまでダメだったメソッドの実装(ブロック内にコードが記述できる)ができるようになりました

実行結果

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

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

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

賞味期限切れの場合、お肉クラスの実装されているメソッドも実行されます

クラス図

イラスト

全体システム

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

商品一覧には、商品が複数登録されています
今回はお肉を賞味期限があるグループに所属させています
このグループに所属すると賞味期限を必ず実装しなければなりません

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

前回の振る舞いのイラスト

今回の振る舞いのイラスト

UML図

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

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

属性振る舞いの2つです

クラスの形

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

クラス図のサンプル

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

前回のクラス図

今回のクラス図

機能の追加とともに、インターフェースを示す矢印線を波線に修正しています

シーケンス図

図中の表現

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

前回のシーケンス図

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

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

今回のシーケンス図

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

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

その1 全体のパターン
その2 お肉商品に絞った別のパターン

別の視点になります
記述方法も変更されています
インターフェースは省略されています

コード

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

PurchasedItem.cs(購入済み商品)

変更点はありません

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

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

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

属性

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

振る舞い

ありません

class PurchasedItem
{
    public string barCode;

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

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

変更点はありません

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

属性

バーコード商品名価格の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();
}

IPerishable.cs(賞味期限がある)

抽象メソッドを追加します
インターフェースを実装するクラスに強制的に実装させます
どのようなメソッド内容にするかは実装するクラスに任せられています

インターフェース内で、

これは、ポリモーフィズムとして機能します

// 賞味期限切れの時の処理
void ExpirationDateProcessing();

属性

抽象プロパティ

このインターフェースを実装したクラスに賞味期限プロパティの実装を強制します
public修飾子は付けませんが、実装したクラスではpublic修飾子を矯正されます

振る舞い

デフォルトメソッド(CheckExpiryDateメソッド)

賞味期限が切れているかのチェックメソッド
このインターフェースを実装したクラスでアクセスすることができます

抽象メソッド(ExpirationDateProcessingメソッド)

期限切れの処理を実装側のクラスに求めます
実装したクラスでは強制的です

/// <summary>
/// 賞味期限がある
/// </summary>
interface IPerishable
{
    DateTime ExpiryDate { get; set; }

    void CheckExpiryDate()
    {
        if (ExpiryDate < DateTime.Now)
        {
            Console.WriteLine("賞味期限が切れています");

            // 賞味期限処理を実装しているクラスのコードが呼ばれます
            ExpirationDateProcessing();
        }
    }

    // 賞味期限切れの時の処理の抽象メソッド(実体は、実装しているクラスのメソッド)
    void ExpirationDateProcessing();
}

MeatItem.cs(お肉商品)

インターフェースを実装して抽象メソッドを作成します

赤いラインが表示されエラーになりますので、インテリセンスの機能で実装してみましょう

IPerishableインターフェースで実装を強制されたExpirationDateProcessingメソッドをここに記述します

public void ExpirationDateProcessing()
{
    Console.WriteLine("お肉は生鮮担当が廃棄します");
}

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

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

IPerishableインターフェース(賞味期限がある)を実装します

属性

賞味期限のみです
これは、IPerishableインターフェースで実装を強制するように宣言されています

振る舞い

抽象クラスからの実装メソッド(Showメソッド)

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

インターフェースからの実装メソッド(ExpirationDateProcessingメソッド)

賞味期限が切れた時に呼ばれるメソッドです
賞味期限が切れていない時は実行されません

class MeatItem : Item, IPerishable
{
    public DateTime ExpiryDate { get; set; }

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

    public void ExpirationDateProcessing()
    {
        Console.WriteLine("お肉は生鮮担当が廃棄します");
    }
}

まとめ

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

インターフェースでプロパティが定義されているため、実装クラスでのExpiryDateプロパティは強制になります

インターフェースで抽象メソッドが定義されているため、実装クラスでExpirationDateProcessingメソッドの実装は強制になります

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(レジスター)

変更はありません

レジクラスになります

属性

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

振る舞い

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

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

    public Reg()
    {
        // コンストラクタで商品マスターに商品を初期登録しています
        Item item1 = new MeatItem { name = "鶏肉", barCode = "1234", Price = 100, ExpiryDate = new DateTime(2023, 5, 10) };
        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();

            // Ipershableインターフェースを実装していれば キャストしてperishableItem変数に代入
            if (findItem is IPerishable perishableItem)
            {
                perishableItem.CheckExpiryDate();
            }

            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記法から図を作成するネットのサービス(無料)

シーケンス図

その1

@startuml
hide footbox
title 購入処理シーケンス図

participant Customer as C
participant PurchasedItem as PI
participant Reg as R
participant Item as I
participant BookItem as BI
participant MeatItem as MI
participant IPerishable as IP

C->R: RegSystem.Run()
activate R
R->C: new Reg()
activate C

loop 購入処理
    C->C: Buy()
    activate C

    loop 商品選択
        C->C: バーコード入力
    end

    C->C: バーコード入力(空)
    deactivate C

    C->R: PrintReceipt(Customer customer)
    activate R
    R->I: new MeatItem()
    activate I
    I->MI: MeatItemの設定
    activate MI
    MI->IP: CheckExpiryDate()
    activate IP
    IP->MI: ExpirationDateProcessing()
    deactivate IP
    MI->R: Show()
    deactivate MI
    R->C: 商品情報表示
    deactivate I

    R->I: new BookItem()
    activate I
    I->BI: BookItemの設定
    activate BI
    BI->R: Show()
    deactivate BI
    R->C: 商品情報表示
    deactivate I

    R->C: 合計金額表示
    deactivate R
end
@enduml

その2

@startuml

actor Customer

Customer -> Reg : Buy()
Reg -> PurchasedItem : PurchasedItem(barCode)
Reg --> Customer : add item to items

activate Reg

loop
  Customer -> PurchasedItem : PurchasedItem(barCode)
  PurchasedItem -> Item : constructor(barCode)
  note over Item : Item creation
  Item -> MeatItem : constructor(name, barCode, Price, ExpiryDate)
  note over MeatItem : MeatItem creation
end

Customer --> Reg : Done (empty input)

Reg -> Reg : PrintReceipt(Customer)
activate Reg

Reg -> Customer : Print receipt header
Reg -> Customer : Get items
Customer --> Reg : items

loop for each item in items
  Reg -> Item : Show()
  Item --> Reg : ShowBaseInfo()
  note over Item : ShowBaseInfo call
  Reg -> Item : ExpiryDate
  alt item is IPerishable
    Item -> MeatItem : ExpirationDateProcessing()
    note over MeatItem : ExpirationDateProcessing call
  end
end

Reg -> Customer : Print total price
deactivate Reg

@enduml

その3

@startuml

actor Customer

Customer -> Reg : Buy()
Reg -> PurchasedItem : PurchasedItem(barCode)
Reg --> Customer : add item to items

activate Reg

loop
  Customer -> PurchasedItem : PurchasedItem(barCode)
  PurchasedItem -> Item : constructor(barCode)
  note over Item : Item creation
  Item -> MeatItem : constructor(name, barCode, Price, ExpiryDate)
  note over MeatItem : MeatItem creation
end

Customer --> Reg : Done (empty input)

Reg -> Reg : PrintReceipt(Customer)
activate Reg

Reg -> Customer : Print receipt header
Reg -> Customer : Get items
Customer --> Reg : items

loop for each item in items
  Reg -> Item : Show()
  Item --> Reg : ShowBaseInfo()
  note over Item : ShowBaseInfo call
  Reg -> Item : ExpiryDate
  alt item is IPerishable
    Item -> MeatItem : ExpirationDateProcessing()
    note over MeatItem : ExpirationDateProcessing call
  end
end

Reg -> Customer : Print total price
deactivate Reg

@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 <<get>> <<set>>
    + <<override>> Show() : void
    + ExpirationDateProcessing() : void
}

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

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

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

interface IPerishable {
    +ExpiryDate : DateTime <<get>> <<set>>
    +CheckExpiryDate() : void
    +{abstract}ExpirationDateProcessing() : void
}

class RegSystem {
    +Run()
}

IPerishable <|.. MeatItem : < implementation

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