クラスおよびオブジェクトを使用したオブジェクト指向プログラミングについて確認します

このチュートリアルでは、コンソール アプリケーションを構築し、C# 言語に含まれるオブジェクト指向の基本的な機能について確認します。

前提条件

アプリケーションを作成する

ターミナル ウィンドウで、「Classes」という名前のディレクトリを作成します。 ここにアプリケーションを構築します。 このディレクトリに移動し、コンソール ウィンドウで「dotnet new console」と入力します。 このコマンドにより、アプリケーションが作成されます。 Program.cs を開きます。 内容は次のようになります。

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

このチュートリアルでは、銀行口座を表す新しい型を作成します。 通常、開発者は各クラスを別々のテキスト ファイルで定義します。 この方法なら、プログラムのサイズが大きくなっても管理が容易です。 classes ディレクトリに「BankAccount.cs」という名前の新しいファイルを作成します。

このファイルには、"銀行口座" の定義が含まれます。 オブジェクト指向プログラミングを使用して "クラス" の形式で型を作成することにより、コードを整理します。 これらのクラスには、特定のエンティティを表すコードが含まれています。 BankAccount クラスは銀行口座を表します。 コードでは、メソッドとプロパティを使用した特定の操作を実装します。 このチュートリアルでは、銀行口座は次の動作をサポートします。

  1. 銀行口座を一意に特定する 10 桁の数字をサポートしています。
  2. 口座の名前、または所有者の名前を格納する文字列をサポートしています。
  3. 残高を取得できます。
  4. 預金を許可します。
  5. 引き出しを許可します。
  6. 初期残高は正の値である必要があります。
  7. 引き出しによって残高が負の値になることはありません。

銀行口座の型を定義する

動作を定義するクラスの基本を作成することから開始できます。 File:New コマンドを使用して、新しいファイルを作成します。 BankAccount.cs という名前を付けます。 BankAccount.cs ファイルに次のコードを追加します。

namespace Classes;

public class BankAccount
{
    public string Number { get; }
    public string Owner { get; set; }
    public decimal Balance { get; }

    public void MakeDeposit(decimal amount, DateTime date, string note)
    {
    }

    public void MakeWithdrawal(decimal amount, DateTime date, string note)
    {
    }
}

先に進む前に、構築したものを確認してみましょう。 namespace 宣言は、コードを論理的に整理する方法を提供します。 このチュートリアルで取り扱うコードは比較的小さいため、1 つの名前空間にすべてのコードを配置します。

public class BankAccount は、これから作成するクラスまたは型を定義します。 クラス宣言のあとにある {} の内側はすべて、クラスの状態と動作を定義しています。 BankAccount クラスには、5 つの "メンバー" があります。 最初の 3 つは "プロパティ" です。 プロパティはデータ要素であり、検証やその他の規則を適用するコードを持つことができます。 最後の 2 つは "メソッド" です。 メソッドは 1 つの機能を実行するコード ブロックです。 各メンバーの名前を確認すると、開発者がそのクラスの作用を把握するための十分な情報が得られます。

新しいアカウントを開く

実装する最初の機能は、銀行口座を開く機能です。 顧客が口座を開く場合、初期残高や口座の (1 名または複数名の) 所有者の情報を入力する必要があります。

BankAccount 型の新しいオブジェクトを作成することは、それらの値を割り当てる "BankAccount" を定義することを意味します。 "コンストラクター" はクラスと同じ名前を持つメンバーです。 これは、そのクラス型のオブジェクトを初期化するために使用されます。 BankAccount 型に次のコンストラクターを追加します。 MakeDeposit クラス宣言の上に次のコードを配置します。

public BankAccount(string name, decimal initialBalance)
{
    this.Owner = name;
    this.Balance = initialBalance;
}

上のコードでは、this 修飾子を含めることで構築されていオブジェクトのプロパティを示しています。 通常、この修飾子はオプションであり、省略されます。 次の記述も可能です。

public BankAccount(string name, decimal initialBalance)
{
    Owner = name;
    Balance = initialBalance;
}

this 修飾子は、ローカル変数またはパラメーターがそのフィールドまたはプロパティと同じ名前を持つ場合にのみ必要です。 この記事の残りの部分では、必要な場合を除き this 修飾子を省略します。

new を使用してオブジェクトを作成すると、コンストラクターが呼び出されます。 Console.WriteLine("Hello World!");Console.WriteLine("Hello World!"); の行を次のコードで置き換えます (<name> を自分の名前に置き換えます)。

using Classes;

var account = new BankAccount("<name>", 1000);
Console.WriteLine($"Account {account.Number} was created for {account.Owner} with {account.Balance} initial balance.");

これまでに構築したものを実行してみましょう。 Visual Studio を使用している場合は、 [デバッグ] メニューから [デバッグなしで開始] を選択します。 コマンド ラインを使用している場合は、プロジェクトを作成したディレクトリで dotnet run を入力します。

口座番号が空であることに気付かれましたか? 次にこの問題を解決します。 口座番号はオブジェクトが作成されるときに割り当てられる必要があります。 しかし、それを作成する責任を呼び出し元に負わせるべきではありません。 BankAccount クラスのコードは、新しい口座番号の割り当て方を知っている必要があります。 簡単な方法は、10 桁の数字で始めることです。 そして、新しい口座番号が作成されるごとに値を 1 増加します。 最後に、オブジェクトが作成されるときに現在の口座番号を格納します。

BankAccountクラスにメンバーの宣言を追加します。 BankAccount クラスの先頭の左中かっこ { の後に、次のコードを配置します。

private static int s_accountNumberSeed = 1234567890;

accountNumberSeed はデータ メンバーです。 これは private であり、BankAccount クラス内のコードのみがこれにアクセスできます。 この方法により、プライベートな実装 (口座番号の生成方法) から (口座番号を持つなどの) パブリックな責任を分離できます。 static でもあるため、すべての BankAccount オブジェクトによって共有されます。 静的でない変数の値は BankAccount オブジェクトのインスタンスごとに一意です。 accountNumberSeedprivate static フィールドであるため、C# の名前付け規則に従って s_ プレフィックスが付けられます。 sstatic フィールドを示し、_private フィールドを示します。 次の 2 行をコンストラクターに追加して、口座番号を割り当てます。 それらは this.Balance = initialBalance という行の後に配置します。

Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;

dotnet run」と入力して結果を表示します。

預金と引き出しを作成する

銀行口座クラスは、預金と引き出しを許可して正しく動作するようにする必要があります。 口座のすべてのトランザクションを記録する履歴を作成して、預金と引き出しを実装しましょう。 すべてのトランザクションを追跡すると、単純にトランザクションごとに残高を更新する方法に比べていくつかのメリットがあります。 履歴は、すべてのトランザクションを監査して毎日の残高を管理するために使用できます。 必要に応じてすべてのトランザクションの履歴から残高を計算することにより、1 つのトランザクションの中で修正されたすべてのエラーが正しく残高に反映されて次の計算に使用されます。

トランザクションを表す新しい型を作成するところから始めましょう。 トランザクションは責任を何も持たない単純型です。 いくつかのプロパティが必要になります。 「Transaction.cs」という名前の新しいファイルを作成します。 これに次のコードを追加します。

namespace Classes;

public class Transaction
{
    public decimal Amount { get; }
    public DateTime Date { get; }
    public string Notes { get; }

    public Transaction(decimal amount, DateTime date, string note)
    {
        Amount = amount;
        Date = date;
        Notes = note;
    }
}

BankAccount クラスに Transaction オブジェクトの List<T> を追加しましょう。 BankAccount.cs ファイルのコンストラクターの後に次の宣言を追加します。

private List<Transaction> _allTransactions = new List<Transaction>();

それでは、Balance を正しく計算しましょう。 現在の残高は、すべての取引の値を合計することによって確認できます。 現在、このコードで取得できるのは口座の初期残高のみであるため、Balance プロパティを更新する必要があります。 public decimal Balance { get; }public decimal Balance { get; } の行を、次のコードに置き換えます。

public decimal Balance
{
    get
    {
        decimal balance = 0;
        foreach (var item in _allTransactions)
        {
            balance += item.Amount;
        }

        return balance;
    }
}

この例は、"プロパティ" の重要な側面を示しています。 これで、別のプログラマーが値を要求したときに残高が計算されるようになりました。 この計算処理は、すべてのトランザクションを列挙して、その合計値を現在の残高として提供します。

次に MakeDeposit メソッドと MakeWithdrawal メソッドを実装します。 これらのメソッドは、初期残高が正の値でなければならず、引き出し後の残高が負の値になってはいけない、という最後の 2 つの規則を適用します。

これらの規則により、"例外" の概念が導入されています。 メソッドが作業を正常に完了できないことを示す標準的な方法は、例外をスローすることです。 例外の型とそれに関連付けられたメッセージがエラーを説明します。 MakeDeposit メソッドは、預金額が 0 以下になる場合に例外をスローします。 MakeWithdrawal メソッドは、引き出し額が 0 以下になる場合、または引き出しを適用した結果、残高が負の値になる場合に例外をスローします。 _allTransactions リストの宣言の後に、次のコードを追加します。

public void MakeDeposit(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of deposit must be positive");
    }
    var deposit = new Transaction(amount, date, note);
    _allTransactions.Add(deposit);
}

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < 0)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

throw ステートメントからは例外がスローされます。 現在のブロックの実行が終了し、コントロールによってコール スタックで最初に一致した catch ブロックに転送されます。 あとで catch ブロックを追加してこのコードをテストします。

残高を直接更新するのではなく、最初のトランザクションを追加するようにするため、コンストラクターを 1 か所変更する必要があります。 既に MakeDeposit メソッドは記述したので、このメソッドをコンストラクターから呼び出します。 完成したコンストラクターは次のようになります。

public BankAccount(string name, decimal initialBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

DateTime.Now は、現在の日付と時刻を返すプロパティです。 新しい BankAccount を作成するコードの後で、Main メソッドにいくつかの預金と引き出しを追加することで、このコードをテストします。

account.MakeWithdrawal(500, DateTime.Now, "Rent payment");
Console.WriteLine(account.Balance);
account.MakeDeposit(100, DateTime.Now, "Friend paid me back");
Console.WriteLine(account.Balance);

次に、残高が負の値になっている口座を作成してみることで、エラー条件のキャッチをテストします。 追加したコードの後に、次のコードを追加します。

// Test that the initial balances must be positive.
BankAccount invalidAccount;
try
{
    invalidAccount = new BankAccount("invalid", -55);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine("Exception caught creating account with negative balance");
    Console.WriteLine(e.ToString());
    return;
}

try-catch ステートメントを使用して、例外をスローする可能性のあるコード ブロックをマークし、想定したエラーをキャッチします。 同じ方法で、残高が負の値になっている場合に例外をスローするコードをテストします。 Main メソッドの invalidAccount の宣言の前に次のコードを追加します。

// Test for a negative balance.
try
{
    account.MakeWithdrawal(750, DateTime.Now, "Attempt to overdraw");
}
catch (InvalidOperationException e)
{
    Console.WriteLine("Exception caught trying to overdraw");
    Console.WriteLine(e.ToString());
}

ファイルを保存し、「dotnet run」と入力して試します。

課題 - すべてのトランザクションをログに記録する

このチュートリアルを完了すると、トランザクション履歴の string を作成する GetAccountHistory メソッドを記述できるようになります。 このメソッドを BankAccount 型に追加します。

public string GetAccountHistory()
{
    var report = new System.Text.StringBuilder();

    decimal balance = 0;
    report.AppendLine("Date\t\tAmount\tBalance\tNote");
    foreach (var item in _allTransactions)
    {
        balance += item.Amount;
        report.AppendLine($"{item.Date.ToShortDateString()}\t{item.Amount}\t{balance}\t{item.Notes}");
    }

    return report.ToString();
}

履歴では、StringBuilder クラスを使って、各トランザクションを 1 行で表す文を含んだ文字列をフォーマットします。 文字列をフォーマットするコードについては、このチュートリアルで先述しました。 新しい文字の 1 つは \t です。 これはタブを挿入して出力をフォーマットします。

次の行を追加して、Program.cs でテストします。

Console.WriteLine(account.GetAccountHistory());

プログラムを実行して結果を確認します。

次の手順

うまくいかない場合は、このチュートリアルのソースを GitHub リポジトリで確認できます。

オブジェクト指向プログラミングのチュートリアルに進むことができます。

次の記事でこれらの概念の詳細を学習できます。