C#でのオブジェクト指向学習サンプル9(コードの整理:リファクタリング)

コードをここで一旦整理します
実際にプログラムを書くときはこのように記述しながら修正していきますので、その追体験をしてください

前回までのサンプル

ベースとなる基本レジ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

リファクタリングとは

リファクタリングの目標は、コードの可読性を向上させ、重複を削減することです

リファクタリング(Refactoring)は、ソフトウェア開発において、既存のコードを再設計し、その構造や設計を改善するプロセスです。このプロセスでは、コードの外部の振る舞い(つまり、ユーザーに見える動作)を変更せずに、コードの内部の構造を改善します。

リファクタリングの目的は、以下のような点にあります:

  1. 可読性の向上:コードの理解しやすさを向上させ、後で修正や拡張を行う際に開発者がスムーズに対応できるようにします。
  2. 保守性の向上:コードの品質を高めることで、保守性が向上します。バグの修正や新機能の追加が容易になります。
  3. パフォーマンスの最適化:リファクタリングは、コードの不要な部分を除去し、効率的なアルゴリズムやデータ構造を使用することで、パフォーマンスの向上を図ることもあります。
  4. バグの発見:リファクタリング中にバグが見つかることがあります。この過程で、コードの品質が向上し、潜在的な問題を早期に発見することができます。

リファクタリングの原則は、ソフトウェア開発者であるマーティン・ファウラーによって初めて明確に提唱されました。リファクタリングは、テストがあることが望ましい場合が多いため、自動化されたテストスイートを使用して実施することが推奨されています。テストを通過することが保証されたコードが、リファクタリング後にも正しく動作することを確認するためです。

リファクタリングは、ソフトウェアの進化と改善に不可欠なプラクティスであり、コードの品質を保ちながら、システム全体のメンテナンス性と拡張性を高めるのに役立ちます。

実行結果

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

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

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

クラス図

イラスト

全体システム

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

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

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

振る舞いのイラスト

UML図

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

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

属性振る舞いの2つです

クラスの形

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

クラス図のサンプル

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

has 1.*

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

inheritance

継承を表します

implementation

実装を表します

クラス図

シーケンス図

図中の表現

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

シーケンス図

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

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

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

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

コード

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

PurchasedItem.cs(購入済み商品)

属性をフィールドからプロパティ更新しています(それに伴い、Pascal記法に変更)

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

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

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

属性

バーコード

振る舞い

コンストラクタ

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

class PurchasedItem
{
    public string BarCode { get; }

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

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

コメントがなくても理解できるメンバーについてのコメントを削除
属性をフィールドからプロパティ更新しています(それに伴い、Pascal記法に変更)
if文を三項演算子に変更

商品マスター(商品一覧)に登録する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(本商品)

属性をフィールドからプロパティ更新しています(それに伴い、Pascal記法に変更)
コメントを削除

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(お客)

属性をフィールドからプロパティ更新しています(それに伴い、Pascal記法に変更)
コメントを削除

Itemsリストの初期化をコンストラクタで実行しています
Itemsリストの要素のクリア(買い物かごを0にする)は、Buyメソッドで実行しています
買い物終わりをエンターキーを条件としていますが、stringクラスのNullか空文字""か空白文字" “ではないかの条件メソッドを使うようにしています

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

属性

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

振る舞い

コンストラクタ

インスタンス作成時、名前を登録します

購入する(Buyメソッド)

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

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

    public Customer(string name)
    {
        Name = name;
        Items = new HashSet<PurchasedItem>();
    }

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

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

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

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

Reg.cs(レジスター)

コメントを削除
商品登録処理をメソッドに切り出しています

レジクラスになります

属性

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

振る舞い

コンストラクタ

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

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

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

class Reg
{
    private List<Item> items = new List<Item>();

    public Reg()
    {
        RegisterItems();
    }

    private 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($"{customer.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}円です");
    }
}

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 <<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 {
    -items: List<Item>
    +Reg()
    +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