銀行口座クラス

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

銀行口座クラス とは

残高という状態を持つ 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 です。balancepublic にしてしまうと、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 { ... } で新しい型を作っている。intString と同じレベルの「型」を自分で定義したことになる
  • private int balance;private で、Account の外からは balance を直接参照できないように封印した
  • public Account(int initial)コンストラクタnew Account(1000) のように呼ばれて、新しい口座を準備する役目
  • this.balancethis は、「いま操作している口座インスタンスの balance」を指す。引数名と被ったときの曖昧さも this で解消できる
  • depositamount > 0 のときだけ加算する。負の入金や 0 円入金は黙って無視するルール
  • withdrawamount > 0 かつ amount <= balance のときだけ減算する。残高不足の引き出しも黙って無視する

呼び出し側はとてもシンプルです。下記のように new で口座を生成し、メソッドを呼んでいくだけです。

Java

Account acc = new Account(1000); acc.deposit(500); // balance: 1500 acc.withdraw(300); // balance: 1200 int b = acc.getBalance(); // 1200

Account は型なので、int と同じ感覚で変数 acc に入れて持ち運べます。これが「データとふるまいをまとめた部品 = オブジェクト」のうま味で、acc 1 つを引数として渡すだけで、別のメソッドや別の画面が、同じ口座の同じ残高にアクセスできるようになります。

deposit と withdraw のフロー

depositwithdraw の内側でどう判定しているかを、下記の図で目に焼き付けておきます。

diagram (will load when visible)

depositwithdraw も、まず 条件チェック (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()); // 1500

ab は、変数こそ別ですが、指している 口座インスタンスは同じ です。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 だけでなく Python JavaScript Kotlin などほぼ全部のオブジェクト指向言語で必要になります。最初に銀行口座でつまずいておくと、将来の設計議論でぐっと楽になります。

よくある間違い

銀行口座クラスを書くときに、初学者がほぼ全員ハマる落とし穴を 4 つ並べます。実装に入る前に頭に入れておきましょう。

1 つ目は、validation を書き忘れる ミスです。deposit(amount) { this.balance += amount; } のように無条件で足してしまうと、deposit(-1000) を許してしまい、残高を不正にマイナスにできてしまいます。if (amount > 0) の 1 行で守れる単純な話ですが、書き忘れた瞬間に金融バグになります。withdraw 側も if (amount <= balance) で守らないと、残高 100 の口座から 200 引き出して -100 になります。

2 つ目は、balancepublic で公開してしまう ミスです。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) を呼ばれたら、下記の手順で最終残高を返してください。

  1. new Account(initial) で口座を作る
  2. acc.deposit(deposit) を呼ぶ (deposit が負なら内部で無視される)
  3. acc.withdraw(withdraw) を呼ぶ (残高を超えるなら内部で無視される)
  4. acc.getBalance() の戻り値を返す

下記の流れで進めると詰まりにくいです。

  • Solution クラスの中に static class Account { ... } か、Solution と同じファイルに class Account { ... } を書く
  • まず Account のフィールド private int balance を書く
  • コンストラクタ public Account(int initial)this.balance = initial; する
  • deposit withdraw getBalance を順に実装し、それぞれ if の条件を確認する
  • 最後に bankFlow の中で new Account(initial)depositwithdrawreturn getBalance() の順に呼ぶ

うまく緑になったら、下記の改造で Account の設計感覚を広げてみましょう。

  • deposit withdrawboolean 戻り値にして、成功 / 失敗 を呼び出し側に返すように改造する
  • 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 つができることを確認しましょう。

  1. Bank Account の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Bank Account とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution の中に static class Account を定義し、private int balance フィールドと public Account(int initial) コンストラクタを持たせる
  2. Account に deposit(int amount) / withdraw(int amount) / getBalance() を実装し、deposit は amount > 0 のときだけ、withdraw は amount > 0 かつ amount <= balance のときだけ残高を更新する
  3. 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

ヒント

main.java
main.java
学習モード

メモ

銀行口座クラス

⌘S で保存