C# はオブジェクト指向プログラミング言語です。 オブジェクト指向プログラミングの 4 つの基本原則は次のとおりです。
- アブストラクション エンティティの関連する属性と相互作用をクラスとしてモデル化し、システムの抽象表現を定義します。
- カプセル化 オブジェクトの内部状態と機能を非表示にし、関数のパブリック セットを介してのみアクセスを許可します。
- 継承 既存の抽象化に基づいて新しい抽象化を作成する機能。
- 多形 複数の抽象化にわたってさまざまな方法で継承されたプロパティまたはメソッドを実装する機能。
前のチュートリアルであるクラスの概要では、抽象化とカプセル化の両方を見ました。
BankAccount
クラスは、銀行口座の概念の抽象化を提供しました。
BankAccount
クラスを使用したコードに影響を与えることなく、その実装を変更できます。
BankAccount
クラスとTransaction
クラスの両方で、コードでこれらの概念を記述するために必要なコンポーネントのカプセル化が提供されます。
このチュートリアルでは、 継承 と ポリモーフィズム を利用して新しい機能を追加するように、そのアプリケーションを拡張します。 また、前のチュートリアルで学習したBankAccount
とカプセル化の手法を利用して、 クラスに機能を追加します。
さまざまな種類のアカウントを作成する
このプログラムをビルドすると、機能を追加する要求が表示されます。 銀行口座の種類が 1 つしかない場合に最適です。 時間の経過と同時に、ニーズの変更、および関連するアカウントの種類が要求されます。
- 毎月末に利息が発生する利子収益勘定。
- 残高が負になることもありますが、残高がある場合は毎月利息が発生する信用枠。
- 1 回のデポジットで始まり、支払いのみ可能な前払いギフト カード アカウント。 毎月初めに1回補充できます。
これらの異なるアカウントはすべて、前のチュートリアルで定義 BankAccount
クラスに似ています。 そのコードをコピーし、クラスの名前を変更して、変更することができます。 この手法は短期的には機能しますが、時間の経過に伴ってより多くの作業が行われます。 すべての変更は、影響を受けるすべてのクラスにコピーされます。
代わりに、前のチュートリアルで作成した BankAccount
クラスからメソッドとデータを継承する新しい銀行口座の種類を作成できます。 これらの新しいクラスは、各型に必要な特定の動作を使用して、 BankAccount
クラスを拡張できます。
public class InterestEarningAccount : BankAccount
{
}
public class LineOfCreditAccount : BankAccount
{
}
public class GiftCardAccount : BankAccount
{
}
これらの各クラスは、共有基底クラス (BankAccount
) から共有動作を継承します。
各派生クラスの新機能と異なる機能の実装を記述します。 これらの派生クラスには、 BankAccount
クラスで定義されているすべての動作が既に含まれます。
新しい各クラスを別のソース ファイルに作成することをお勧めします。 Visual Studio では、プロジェクトを右クリックし、[クラスの追加] を選択して新しいファイルに新しいクラスを追加できます。 Visual Studio Code で[ファイル]、[新規作成]の順に選択して、新しいソース ファイルを作成します。 どちらのツールでも、クラス ( InterestEarningAccount.cs、 LineOfCreditAccount.cs、GiftCardAccount.cs) に一致するファイルに名前を 付けます。
前のサンプルに示すようにクラスを作成すると、どの派生クラスもコンパイルされないことがわかります。 コンストラクターは、オブジェクトの初期化を担当します。 派生クラスコンストラクターは、派生クラスを初期化し、派生クラスに含まれる基底クラス オブジェクトを初期化する方法の手順を提供する必要があります。 通常、適切な初期化は余分なコードなしで行われます。
BankAccount
クラスは、次のシグネチャを持つ 1 つのパブリック コンストラクターを宣言します。
public BankAccount(string name, decimal initialBalance)
コンストラクターを自分で定義しても、コンパイラは既定のコンストラクターを生成しません。 つまり、各派生クラスは、このコンストラクターを明示的に呼び出す必要があります。 基底クラスのコンストラクターに引数を渡すことができるコンストラクターを宣言します。 次のコードは、 InterestEarningAccount
のコンストラクターを示しています。
public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
この新しいコンストラクターのパラメーターは、基底クラス コンストラクターのパラメーターの型と名前と一致します。 基底クラスコンストラクターの呼び出しを示すには、 : base()
構文を使用します。 一部のクラスでは複数のコンストラクターが定義されており、この構文を使用すると、呼び出す基底クラスコンストラクターを選択できます。 コンストラクターを更新したら、派生クラスごとにコードを開発できます。 新しいクラスの要件は、次のように記述できます。
- 利子収益アカウント:
- 月末の残高の 2% が振り込まれます。
- 与信枠:
- 負の残高を持つことができますが、与信限度額よりも絶対値を大きくすることはできません。
- 月末残高が 0 ではない毎月の利息料金が発生します。
- 与信限度額を超える出金ごとに手数料が発生します。
- ギフト カード アカウント:
- 毎月1回、月の最終日に指定した量で補充することができます。
これらの 3 つのアカウントの種類すべてに、毎月の終わりに発生するアクションがあることがわかります。 ただし、アカウントの種類ごとに異なるタスクが実行されます。 ポリ モーフィズム を使用して、このコードを実装します。
virtual
クラスに 1 つのBankAccount
メソッドを作成します。
public virtual void PerformMonthEndTransactions() { }
上記のコードは、 virtual
キーワードを使用して、派生クラスが別の実装を提供する可能性がある基底クラスのメソッドを宣言する方法を示しています。
virtual
メソッドは、派生クラスが再実装を選択できるメソッドです。 派生クラスでは、 override
キーワードを使用して新しい実装を定義します。 通常、これを "基底クラスの実装をオーバーライドする" と参照します。
virtual
キーワードは、派生クラスが動作をオーバーライドすることを指定します。 派生クラスが動作をオーバーライドする必要がある abstract
メソッドを宣言することもできます。 基底クラスは、 abstract
メソッドの実装を提供しません。 次に、作成した 2 つの新しいクラスの実装を定義する必要があります。
InterestEarningAccount
から始めます。
public override void PerformMonthEndTransactions()
{
if (Balance > 500m)
{
decimal interest = Balance * 0.02m;
MakeDeposit(interest, DateTime.Now, "apply monthly interest");
}
}
LineOfCreditAccount
に次のコードを追加します。 このコードは、口座から引き出された正の利率を計算するために残高を否定します。
public override void PerformMonthEndTransactions()
{
if (Balance < 0)
{
// Negate the balance to get a positive interest charge:
decimal interest = -Balance * 0.07m;
MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
}
}
GiftCardAccount
クラスでは、月末の機能を実装するために 2 つの変更が必要です。 まず、月ごとに追加する省略可能な量を含むようにコンストラクターを変更します。
private readonly decimal _monthlyDeposit = 0m;
public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
=> _monthlyDeposit = monthlyDeposit;
コンストラクターは、 monthlyDeposit
値の既定値を提供するため、呼び出し元は毎月のデポジットなしで 0
を省略できます。 次に、コンストラクターで 0 以外の値に設定されている場合は、 PerformMonthEndTransactions
メソッドをオーバーライドして月単位のデポジットを追加します。
public override void PerformMonthEndTransactions()
{
if (_monthlyDeposit != 0)
{
MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
}
}
オーバーライドにより、コンストラクタに設定された月々の入金設定が適用されます。
Main
メソッドに次のコードを追加して、GiftCardAccount
とInterestEarningAccount
に対してこれらの変更をテストします。
var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money");
Console.WriteLine(giftCard.GetAccountHistory());
var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());
結果を確認します。 次に、 LineOfCreditAccount
の同様のテスト コードのセットを追加します。
var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
上記のコードを追加してプログラムを実行すると、次のようなエラーが表示されます。
Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount')
at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42
at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31
at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9
at OOProgramming.Program.Main(String[] args) in Program.cs:line 29
注
実際の出力には、プロジェクトを含むフォルダーへの完全なパスが含まれます。 簡潔にするために、フォルダー名は省略されました。 また、コード形式によっては、行番号が若干異なる場合があります。
BankAccount
は初期残高が 0 より大きい必要があると想定しているため、このコードは失敗します。
BankAccount
クラスに組み込まれているもう 1 つの前提は、残高が負にならねないということです。 代わりに、残高を超えてしまう引き出しは拒否されます。 これらの前提条件の両方を変更する必要があります。 信用枠は0から始まり、通常は残高がマイナスになります。 また、顧客があまりにも多くのお金を借りると、料金が発生します。 トランザクションは受け入れられ、コストが高くなります。 最初のルールは、最小残高を指定する BankAccount
コンストラクターに省略可能な引数を追加することで実装できます。 既定値は 0
です。 2 番目の規則には、派生クラスが既定のアルゴリズムを変更できるようにするメカニズムが必要です。 ある意味では、基底クラスは、オーバードラフトがある場合に何が起こるかを派生型に "要求" します。 既定の動作では、例外をスローすることによってトランザクションを拒否します。
最初に、省略可能な minimumBalance
パラメーターを含む 2 つ目のコンストラクターを追加します。 この新しいコンストラクターは、既存のコンストラクターによって実行されるすべてのアクションを実行します。 また、最小残高プロパティも設定します。 既存のコンストラクターの本文をコピーすることもできますが、それにより将来、2ヶ所で変更を行う必要が生じます。 代わりに、 コンストラクター チェーンを 使用して、1 つのコンストラクターで別のコンストラクターを呼び出すことができます。 次のコードは、2 つのコンストラクターと新しい追加フィールドを示しています。
private readonly decimal _minimumBalance;
public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) { }
public BankAccount(string name, decimal initialBalance, decimal minimumBalance)
{
Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;
Owner = name;
_minimumBalance = minimumBalance;
if (initialBalance > 0)
MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}
上記のコードは、2 つの新しい手法を示しています。 最初に、 minimumBalance
フィールドは readonly
としてマークされます。 つまり、オブジェクトの構築後に値を変更することはできません。
BankAccount
が作成されると、minimumBalance
は変更できません。 次に、2 つのパラメーターを受け取るコンストラクターは、 : this(name, initialBalance, 0) { }
を実装として使用します。
: this()
式は、3 つのパラメーターを持つもう 1 つのコンストラクターを呼び出します。 この手法を使用すると、クライアント コードで多数のコンストラクターのいずれかを選択できる場合でも、オブジェクトを初期化するための実装を 1 つ作成できます。
この実装では、初期残高がMakeDeposit
より大きい場合にのみ、0
が呼び出されます。 これにより、預金はプラスでなければならないというルールが維持され、クレジット アカウントは 0
残高で開くことができます。
BankAccount
クラスに最小残高の読み取り専用フィールドが追加されたので、最終的な変更は、ハード コードの0
を minimumBalance
メソッドでMakeWithdrawal
に変更することです。
if (Balance - amount < _minimumBalance)
BankAccount
クラスを拡張した後、次のコードに示すように、LineOfCreditAccount
コンストラクターを変更して新しい基本コンストラクターを呼び出すことができます。
public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}
LineOfCreditAccount
コンストラクターは、creditLimit
パラメーターの意味と一致するように、minimumBalance
パラメーターの符号を変更します。
過剰な引き出しに対する異なる規則
最後に追加する機能を使用すると、 LineOfCreditAccount
はトランザクションを拒否するのではなく、与信限度額を超える料金を請求できます。
1 つの手法は、必要な動作を実装する仮想関数を定義することです。
BankAccount
クラスは、MakeWithdrawal
メソッドを 2 つのメソッドにリファクタリングします。 新しいメソッドは、引き出しが最小値を下回る残高を取るときに、指定されたアクションを実行します。 既存の MakeWithdrawal
メソッドには、次のコードがあります。
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 < _minimumBalance)
{
throw new InvalidOperationException("Not sufficient funds for this withdrawal");
}
var withdrawal = new Transaction(-amount, date, note);
_allTransactions.Add(withdrawal);
}
以下のコードに置き換えます。
public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
}
Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance);
Transaction? withdrawal = new(-amount, date, note);
_allTransactions.Add(withdrawal);
if (overdraftTransaction != null)
_allTransactions.Add(overdraftTransaction);
}
protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
{
if (isOverdrawn)
{
throw new InvalidOperationException("Not sufficient funds for this withdrawal");
}
else
{
return default;
}
}
追加されたメソッドは protected
。つまり、派生クラスからのみ呼び出すことができます。 この宣言により、他のクライアントがメソッドを呼び出すのを防ぐことができます。 また、派生クラスが動作を変更できるように、 virtual
です。 戻り値の型は Transaction?
です。
?
注釈は、メソッドがnull
を返す可能性があることを示します。 引き出し制限を超えたときに料金を請求するには、 LineOfCreditAccount
に次の実装を追加します。
protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
isOverdrawn
? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
: default;
オーバーライドは、口座が残高不足のときに手数料のトランザクションを返します。 引き出しが制限を超えない場合、メソッドは null
トランザクションを返します。 これは、料金がないことを示します。 これらの変更をテストするには、Main
クラスの Program
メソッドに次のコードを追加します。
var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
プログラムを実行し、結果を確認します。
概要
スタックした場合は、 GitHub リポジトリでこのチュートリアルのソースを確認できます。
このチュートリアルでは、Object-Oriented プログラミングで使用される手法の多くを示しました。
- 異なるアカウントの種類ごとにクラスを定義するときに 、抽象化 を使用しました。 これらのクラスは、その種類のアカウントの動作を記述しました。
- カプセル 化 は、各クラスに
private
多くの詳細を保持するときに使用しました。 - コードを保存するために、 クラスで既に作成されている実装を利用するときに
BankAccount
を使用しました。 - 派生クラスがオーバーライドできるメソッドを作成するときに
virtual
を使用して、そのアカウントの種類に固有の動作を作成しました。
.NET