C#でのオブジェクト指向学習サンプル10(レジ:複数のレジで商品一覧の共通化、お客の状況やレシート表示をわかりやすくしてほしい)

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

前回までのサンプル

ベースとなる基本レジ

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

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

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

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

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

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

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

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

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

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

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

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

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

レジ、賞味期限切れの処理を各種類ごとでも実行したい

インターフェースを使ったポリモーフィズムの機能を使って、メソッドの制約も持てるようにしました

コードの整理:リファクタリング

ここで、一度コードを整理しています
整理することでコードが統一され、今後の更新もやりやすくなることを目的に進めます

実行結果

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

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

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

入店の表示、誰が購入したか、レジ番号の表示を追加しています

クラス図

イラスト

全体システム

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

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

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

振る舞いのイラスト

レジごとの商品一覧を共通で使うイメージ

実際のレジシステムでは、一般的に商品マスターはレジごとで持っています
ハードウェアが個別で存在するからです
このサンプルではバーチャル空間なのでこのように考えることができます

商品が一覧に存在しない場合、ネットワークを通じてマスターを検索する機能を有している実際のレジもあります

また、クレジットカードなどは(限度額等の条件もありますが)都度ネットで検索する機能を備えています

複数のレジを導入する(インスタンス作成)と商品一覧もそれぞれ持つことになる

商品一覧をレジクラスで共通で使うようにする
レジクラスの商品一覧メンバー(items)にstaticキーワードを追加

UML図

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

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

属性振る舞いの2つです

クラスの形

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

クラス図のサンプル

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

has 1.*

集約を表しています1対多の構成で「持っている」を表します

inheritance

継承を表します

implementation

実装を表します

クラス図

PlantUML図ではスタティックには下線(_)を付けます

シーケンス図

図中の表現

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

シーケンス図

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

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

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

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

コード

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

PurchasedItem.cs(購入済み商品)

変更はありません

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

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

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

属性

バーコード

振る舞い

コンストラクタ

商品を購入した際、バーコードが代入できるイメージになります

class PurchasedItem
{
    public string BarCode { get; }

    public PurchasedItem(string barCode)
    {
        BarCode = barCode;
    }
}

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

変更はありません

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

属性

商品別に登録されているバーコード商品名価格の3つで考えます
価格については、0より小さい値を代入できないように

振る舞い

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

abstract class Item
{
    public string BarCode { get; set; }
    public string Name { get; set; }
    private int price;

    public int Price
    {
        get => price;
        set => price = value < 0 ? 0 : value;
    }

    public void ShowBaseInfo()
    {
        Console.WriteLine($"{Name}の価格は{Price}円です");
    }

    public abstract void Show();
}

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

変更はありません

属性

抽象プロパティ

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

振る舞い

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

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

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

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

interface IPerishable
{
    DateTime ExpiryDate { get; set; }

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

    void ExpirationDateProcessing();
}

MeatItem.cs(お肉商品)

変更はありません

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

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

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

属性

賞味期限のみです
これは、IPerishableインターフェースで実装を強制するように抽象プロパティで宣言されています
ないとエラーになります

振る舞い

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

基底クラス(Itemクラス)で宣言されている抽象メソッド(Showメソッド)の強制実装になります
商品名価格を表示します(基底クラスItemクラスのメソッドを呼び出します)
ないとエラーになります
賞味期限も表示します

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

賞味期限が切れた時に呼ばれるメソッドです
IPerishableインターフェース(賞味期限がある)で宣言されている抽象メソッドの強制実装になります
ないとエラーになります
賞味期限が切れていない時は実行されません

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修飾子が追加されています
これは、派生クラスでメソッドを置き換えることができることを示しています

属性

著作者のみです

振る舞い

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

基底クラス(Itemクラス)で宣言されている抽象メソッド(Showメソッド)の強制実装になります
商品名価格を表示します(基底クラスItemクラスのメソッドを呼び出します)
ないとエラーになります
著者も表示します

class BookItem : Item
{
    public string Author { get; set; }

    public override void Show()
    {
        ShowBaseInfo();
        Console.WriteLine($"著者 {Author}");
    }
}

Customer.cs(お客)

お客の状況やレシートの表示を改善します
入店時(お客オブジェクトの作成時)に入店した旨表示します
購入時、購入者の名前が表示されるようにしました

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

属性

お客様名複数の購入済み商品の2つで考えます
コンストラクタで名前は登録されます

振る舞い

コンストラクタ

インスタンス作成時、名前を登録します
入店のイメージでお客の名前で入店したことを表示します

購入する(Buyメソッド)

最初に買い物かごをからにします(Itemsリストのクリア)
エンターキーを入力するまで引き続き購入処理を続けられます

class Customer
{
    public string Name { get; }
    public List<PurchasedItem> Items { get; }

    public Customer(string name)
    {
        Name = name;
        Items = new List<PurchasedItem>();
        Console.WriteLine($"{Name}さんが入店しました");
    }

    public void Buy()
    {
        Items.Clear();

        Console.WriteLine($"{Name}さんが買い物をします");
        while (true)
        {
            Console.Write("バーコード: ");
            string barCode = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(barCode))
            {
                break;
            }

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

Reg.cs(レジスター)

商品一覧を複数のレジで共通にします(メモリの削減の効果があります)
何番レジで精算したかも表示します

行間が詰まって読みづらいので、改行コード(¥n)を区切りで追加しています

レジクラスになります

属性

レジの名前商品マスター(商品一覧)を持ちます

振る舞い

コンストラクタ

商品の登録処理メソッドを呼び出しています

レシートを表示する(PrintReceipt)メソッド

商品マスター(商品一覧)から買い物かごから1つずつ抜き出した商品のバーコードと照らし合わせて見つかったら商品情報の表示をします
商品が見つからなければ、無視して次の商品の検索に進みます
また、賞味期限がある商品の場合、賞味期限切れか調べて結果を表示します

class Reg
{
    public string Name { get; }

    public Reg(string name)
    {
        Name = name;
    }

    private static List<Item> items = new List<Item>();

    static Reg()
    {
        RegisterItems();
    }

    private static void RegisterItems()
    {
        items.Add(new MeatItem { Name = "鶏肉", BarCode = "1234", Price = 100, ExpiryDate = new DateTime(2023, 5, 10) });
        items.Add(new BookItem { Name = "三四郎", BarCode = "2345", Price = 200, Author = "夏目漱石" });
    }

    public void PrintReceipt(Customer customer)
    {
        Console.WriteLine($"\n{customer.Name}さんのレシート(レジ:{Name})");
        Console.WriteLine("----------------------------");

        int totalPrice = 0;

        foreach (var purchasedItem in customer.Items)
        {
            var findItem = items.FirstOrDefault(item => item.BarCode == purchasedItem.BarCode);

            if (findItem == null)
            {
                continue;
            }

            findItem.Show();

            if (findItem is IPerishable perishableItem)
            {
                perishableItem.CheckExpiryDate();

            }

            totalPrice += findItem.Price;
        }

        Console.WriteLine("----------------------------");
        Console.WriteLine($"合計金額は{totalPrice}円です\n");
    }
}

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

レジに名前をつけることが可能になったのでインスタンスを作成時にコンストラクタの引数で渡すことにした

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

属性

ありません

振る舞い

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

ここでは、お客様の山田さんと森本さんの入店それぞれが買い物をするレジで清算する(レシートを表示する)を行なっています

class RegSystem
{
    public void Run()
    {
        Reg reg1 = new Reg("レジ1");
        Reg reg2 = new Reg("レジ2");

        Customer customer1 = new Customer("山田");
        Customer customer2 = new Customer("森本");

        customer1.Buy();

        customer2.Buy();

        reg2.PrintReceipt(customer2);

        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 <<get;>>
    +items: List<PurchasedItem> <<get;>>
    +Customer(name: string)
    +Buy()
}

abstract class Item {
    + BarCode : string <<tet; set;>>
    + Name : string <<get;set;>>
    - 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 <<get;set;>>
    + <<override>> Show() : void
}

class PurchasedItem {
    +BarCode: string <<get;>>
    +PurchasedItem(barCode: string)
}

class Reg {
    +Name {get;}
    -{static}items: List<Item>
    -{static}Reg(name: string)
    -{static}RegisterItems() : void
    +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

PlantUMLの仕様