銀行口座クラス
銀行口座クラス とは
残高という状態を持つ Account クラスを設計し、deposit / withdraw / getBalance を通して「データとふるまいをひとまとめにする」というオブジェクト指向のうま味と、validation 。
状態を持つオブジェクトを「銀行口座」で体感する
これまでのレッスンで、public static メソッドを並べてクラスを書く形にだいぶ慣れてきました。add(int a, int b)、bmi(double w, double h) のような「引数を入れたら値が返ってくる純粋な関数」をたくさん書いてきたはずです。これらは便利ですが、よく見ると 状態を持っていない という共通点があります。同じ引数を渡せばいつでも同じ結果が返ってくる、ある意味そっけない部品でした。
このレッスンでは一歩進んで、「状態を持つオブジェクト」 を作ります。題材はみんなが知っている 銀行口座 です。口座には 残高 という、時間とともに変化する内側のデータがあり、入金 や 引き出し を通して値が変わっていきます。同じ口座に同じ 1000円 を入金しても、昨日と今日では残高が違う、という当たり前の事実をそのままコードで表現するのが今回のゴールです。
銀行口座は、
オブジェクト指向プログラミング (Object Oriented Programming, OOP)の教科書にほぼ確実に出てくる定番例です。残高 (balance)というデータと、入金 (deposit)引き出し (withdraw)というふるまいを 1 つにまとめると、自然に「カプセル化」「validation の置き場所」「責任分担」というプロの設計テーマが姿を現すからです。
Account クラスを設計する
まずはどんな部品を持つかを決めます。今回作る Account クラスは下記の 5 つを持ちます。
- フィールド
private int balance— 口座の残高 (外からは直接いじれない) - コンストラクタ
Account(int initial)— 初期残高を受け取って口座を作る - メソッド
void deposit(int amount)—amountが正の数のときだけ残高に足す - メソッド
void withdraw(int amount)—amountが残高以下のときだけ残高から引く - メソッド
int getBalance()— 現在の残高を返す
注目してほしいのが private です。balance を public にしてしまうと、account.balance = -1000000; のような不正な書き換えがどこからでもできてしまい、validation が一気に意味を失います。フィールドを private で隠して、deposit withdraw という決められた入り口経由でしか触れないようにする、これがいわゆる カプセル化 (encapsulation) です。
カプセル化を一言で言うと、「データに勝手にアクセスさせない」「決められた操作経由でだけ触らせる」という発想です。銀行で考えれば自然で、客が金庫の中を直接いじることはなく、必ず行員 (= メソッド) を通して入金や引き出しをします。コードでも同じ作法を取ることで、validation の漏れやバグをぐっと減らせます。
コードに落とす
設計が決まれば、書くのは難しくありません。下記が完成形です。
Java
class Account {
private int balance;
public Account(int initial) {
this.balance = initial;
}
public void deposit(int amount) {
if (amount > 0) {
this.balance += amount;
}
}
public void withdraw(int amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
}
}
public int getBalance() {
return this.balance;
}
}ポイントを下記の順で押さえます。
class Account { ... }で新しい型を作っている。intやStringと同じレベルの「型」を自分で定義したことになるprivate int balance;のprivateで、Accountの外からはbalanceを直接参照できないように封印したpublic Account(int initial)は コンストラクタ。new Account(1000)のように呼ばれて、新しい口座を準備する役目this.balanceのthisは、「いま操作している口座インスタンスのbalance」を指す。引数名と被ったときの曖昧さもthisで解消できるdepositはamount > 0のときだけ加算する。負の入金や0円入金は黙って無視するルールwithdrawはamount > 0かつamount <= balanceのときだけ減算する。残高不足の引き出しも黙って無視する
呼び出し側はとてもシンプルです。下記のように new で口座を生成し、メソッドを呼んでいくだけです。
Java
Account acc = new Account(1000);
acc.deposit(500); // balance: 1500
acc.withdraw(300); // balance: 1200
int b = acc.getBalance(); // 1200Account は型なので、int と同じ感覚で変数 acc に入れて持ち運べます。これが「データとふるまいをまとめた部品 = オブジェクト」のうま味で、acc 1 つを引数として渡すだけで、別のメソッドや別の画面が、同じ口座の同じ残高にアクセスできるようになります。
deposit と withdraw のフロー
deposit と withdraw の内側でどう判定しているかを、下記の図で目に焼き付けておきます。
deposit も withdraw も、まず 条件チェック (validation) を通り、OK のときだけ残高を書き換えます。NG のときは黙って無視するのが今回の仕様です。例外を投げる選択肢もありますが、入門段階では if でガードして「不正な操作は無視」が読みやすいので、まずこちらを採用します。
「ありえない操作を呼ばれたとき、どう振る舞うか」は API 設計のいちばん難しいテーマです。実務では
IllegalArgumentExceptionを投げて呼び出し側に気付かせるのが王道ですが、UIの入力欄など「軽い不正値ならスルーした方が UX がいい」ケースでは、今回のようにifで黙って弾く選択肢も普通にあります。
同じ口座を共有することの意味
Account のような状態を持つオブジェクトを扱うときに、ぜひ理解しておきたいのが インスタンスの同一性 です。下記のコードを見てください。
Java
Account a = new Account(1000);
Account b = a; // 同じ口座を b 経由でも触れるようにする
b.deposit(500);
System.out.println(a.getBalance()); // 1500a と b は、変数こそ別ですが、指している 口座インスタンスは同じ です。b.deposit(500) を呼ぶと a.getBalance() の結果も増えます。これは新人がいちばん戸惑うところで、「片方を操作したらもう片方も変わった」とびっくりするポイントです。int のような 値型 とは違って、Account のような 参照型 は変数に「実体への矢印」を入れている、と覚えてください。
別々の口座を作りたいなら、必ず new Account(...) を 2 回呼ぶ必要があります。下記のとおりです。
Java
Account a = new Account(1000);
Account b = new Account(1000); // 別の口座を作る
b.deposit(500);
System.out.println(a.getBalance()); // 1000 のまま「同じインスタンスを別の変数で共有しているのか」「別々のインスタンスなのか」をいつでも意識する習慣は、
JavaだけでなくPythonJavaScriptKotlinなどほぼ全部のオブジェクト指向言語で必要になります。最初に銀行口座でつまずいておくと、将来の設計議論でぐっと楽になります。
よくある間違い
銀行口座クラスを書くときに、初学者がほぼ全員ハマる落とし穴を 4 つ並べます。実装に入る前に頭に入れておきましょう。
1 つ目は、validation を書き忘れる ミスです。deposit(amount) { this.balance += amount; } のように無条件で足してしまうと、deposit(-1000) を許してしまい、残高を不正にマイナスにできてしまいます。if (amount > 0) の 1 行で守れる単純な話ですが、書き忘れた瞬間に金融バグになります。withdraw 側も if (amount <= balance) で守らないと、残高 100 の口座から 200 引き出して -100 になります。
2 つ目は、balance を public で公開してしまう ミスです。public int balance; と書いてしまうと、account.balance = 9999999; のような不正な書き換えが外からできてしまい、deposit withdraw の validation が無意味になります。private で封印して、getBalance() で値を返すだけにする、というのが最低限の作法です。
3 つ目は、negative な値を素直に減算してしまう ミスです。deposit(-100) を許して balance += -100; をやると、「マイナス入金」という形で残高を減らす裏技ができてしまいます。withdraw の validation を回避するための抜け穴になるので、deposit 側でも amount > 0 を必ずチェックします。
4 つ目は、同じインスタンスを複数で共有してしまう ミスです。さきほどの Account b = a; の例ですが、これに気付かずに「夫の口座」「妻の口座」を Account couple = new Account(1000); の 1 つだけ作って共用してしまうと、片方の deposit がもう片方の残高にも反映されます。本当に共用したいならそれでよいのですが、別々の口座を扱うなら必ず new を 2 回呼びましょう。
もう 1 つ覚えておきたいのが、
コンストラクタで 初期残高にも validation を入れるか という設計判断です。今回の課題ではテストの都合上initialをそのまま使いますが、現実のサービスならif (initial < 0) initial = 0;のように初期値も負にならないよう守ったり、throw new IllegalArgumentException(...)で呼び出し側に気付かせたりします。
やってみよう
課題は、Account クラスを使って一連の操作を行う bankFlow メソッドを書くことです。Solution.bankFlow(int initial, int deposit, int withdraw) を呼ばれたら、下記の手順で最終残高を返してください。
new Account(initial)で口座を作るacc.deposit(deposit)を呼ぶ (depositが負なら内部で無視される)acc.withdraw(withdraw)を呼ぶ (残高を超えるなら内部で無視される)acc.getBalance()の戻り値を返す
下記の流れで進めると詰まりにくいです。
Solutionクラスの中にstatic class Account { ... }か、Solutionと同じファイルにclass Account { ... }を書く- まず
Accountのフィールドprivate int balanceを書く - コンストラクタ
public Account(int initial)でthis.balance = initial;する depositwithdrawgetBalanceを順に実装し、それぞれifの条件を確認する- 最後に
bankFlowの中でnew Account(initial)→deposit→withdraw→return getBalance()の順に呼ぶ
うまく緑になったら、下記の改造で Account の設計感覚を広げてみましょう。
depositwithdrawをboolean戻り値にして、成功 / 失敗を呼び出し側に返すように改造するtransfer(Account to, int amount)メソッドを足して、別の口座にお金を移す処理を書いてみるprivate final int initialBalance;を追加して、最初の残高をいつでも参照できるようにするAccount a = new Account(1000); Account b = a;を試して、本当に同じ残高が動くかを観察する
今回作る Account は、たかが 4 メソッドの小さなクラスですが、ここに詰め込まれている カプセル化 validation の置き場所 状態の共有 参照型と値型の違い といった話題は、Spring での User Order Product のドメインクラス設計でもそのまま生き続けます。「データに勝手に触らせない、決められた口を通して触らせる」を合言葉に、次のレッスンでも一緒に組み立てていきましょう。
次のレッスン
次は toString のオーバーライド で、toString のオーバーライド を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- Bank Account の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Bank Account とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- Solution の中に static class Account を定義し、private int balance フィールドと public Account(int initial) コンストラクタを持たせる
- Account に deposit(int amount) / withdraw(int amount) / getBalance() を実装し、deposit は amount > 0 のときだけ、withdraw は amount > 0 かつ amount <= balance のときだけ残高を更新する
- Solution.bankFlow(int initial, int deposit, int withdraw) の中で new Account(initial) → deposit → withdraw → getBalance() の順に呼んで最終残高を int で返す
入出力例
test-cases.txt
bankFlow(1000, 500, 300) → 1200
bankFlow(1000, -100, 200) → 800
bankFlow(100, 0, 200) → 100
bankFlow(0, 500, 500) → 0
bankFlow(500, -50, 1000) → 500
bankFlow(200, 300, 500) → 0