Product クラスで税込価格

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

Product クラスで税込価格 とは

商品を表す Product クラスを設計し、税込価格を返すメソッドをクラスの中に閉じ込める練習を通じて、オブジェクト指向の最小単位を体に馴染ませる。

クラスを「データと振る舞いの入れ物」として設計する

これまでのレッスンで、変数・条件分岐・繰り返し・メソッドと、Java の基本部品をひととおり触ってきました。ここからはいよいよ Java の真骨頂である クラス (class) とオブジェクト の世界に入っていきます。

クラスを一言で説明すると、「データ と、そのデータを扱う 振る舞い をひとまとめにした入れ物」です。これまで書いてきた Solution.bmi(weight, height) のような static メソッドは、データを毎回引数で受け取る作りでした。一方クラスでは、Product という箱の中に商品の値段や名前を 持たせた状態 で持ち回せます。そして、その箱に対して「税込みでいくら?」と聞くと、箱自身が答えてくれる。これがオブジェクト指向の感覚です。

よくある比喩として、class設計図object (インスタンス) は 設計図から組み立てた実物 に例えられます。Product という設計図から、apple (税抜き 100 円) や book (税抜き 3980 円) という実物を new Product(...) で何個でも作れる、というイメージです。

このレッスンでは、ECサイトのバックエンドでも保健所の登録システムでも必ず登場する 商品 を題材に、Product クラスを自分の手で設計します。題材は身近ですが、ここで学ぶ「ビジネスルールをメソッドに閉じ込める」という考え方は、業務システム開発の根幹に直結する大事なトピックです。

今回作る Product クラスの全体像

先に完成形のイメージを共有します。Product クラスは、商品の 税抜き価格 を保持し、聞かれたら 税込み価格 を返します。下記のような骨格になる予定です。

Java

public class Product { private int price; // 税抜き価格 (フィールド) private static final double TAX_RATE = 0.10; // 消費税率 public Product(int price) { // コンストラクタ this.price = price; } public int getPriceWithTax() { // インスタンスメソッド return (int) (price * (1 + TAX_RATE)); } }

たった 10 行強ですが、フィールド コンストラクタ インスタンスメソッド 定数 という、クラスを構成する 4 つの主要パーツが揃っています。1 つずつ意味を確認していきましょう。

フィールド (field) はクラスが持つ「変数」

private int price; の行が、Productフィールド です。フィールドはクラスの中に住んでいる変数で、その インスタンス (実物の商品) ごとに 別々の値 を覚えていてくれます。

  • apple という Product インスタンスなら price = 100
  • book という Product インスタンスなら price = 3980

このように、同じ Product クラスから作ったオブジェクトでも、それぞれが自分の price を持ちます。これが static メソッド (クラス全体で 1 つ) との一番大きな違いです。

private というキーワードは、「このフィールドはクラスの外から直接いじらせない」という宣言です。apple.price = -100 のようにマイナス値を勝手にセットされて壊されるのを防げます。外から触りたければ、後述する メソッド経由で 触ってもらう、というのがオブジェクト指向の流儀です。これを カプセル化 (encapsulation) と呼びます。

初心者向け教材だと public int price; と書いて済ませている例もありますが、現場では基本 private にして、必要に応じて getPrice() のような getter を用意します。private は「キッチンに勝手に入らないでください、注文はホールで受けます」という張り紙のような役割です。

コンストラクタ (constructor) はインスタンスの「組み立て手順」

public Product(int price) { this.price = price; } の行は、Productコンストラクタ です。コンストラクタはクラスと同じ名前で書く特別なメソッドで、new Product(100) のように呼ばれたときに動きます。

中身でやっているのは「引数で受け取った price を、フィールドの price に代入する」ことだけです。this.pricethis は「今まさに組み立て中の自分自身のオブジェクト」を指す予約語で、引数の priceフィールドの price が同名なときの区別に使います。

Java

Product apple = new Product(100); // price = 100 のインスタンスができる Product book = new Product(3980); // price = 3980 のインスタンスができる

new は「新しい実物を組み立てる」という意味のキーワードで、Java では new クラス名(...) の形で必ず登場します。

インスタンスメソッド vs static メソッド

さて、ここからが今回のメイン課題です。getPriceWithTax() という インスタンスメソッド の話をしましょう。前のレッスンで何度も書いてきた static メソッドとの違いを、はっきり押さえます。

Java

// static メソッド (これまでのスタイル) - 毎回 price を引数で渡す public static int priceWithTax(int price) { return (int) (price * 1.10); } // インスタンスメソッド (今回のスタイル) - 自分の price を使う public int getPriceWithTax() { return (int) (this.price * 1.10); }

見た目のいちばんの違いは、static というキーワードが 無い ことと、引数が 無い ことです。インスタンスメソッドは、apple.getPriceWithTax() のように オブジェクトに対して呼び出す ので、わざわざ price を引数で渡さなくても、自分自身のフィールド this.price を読み出して使えます。

言い換えると、static メソッドは「電卓」、インスタンスメソッドは「自分の状態を知っている賢い AI スピーカー」のようなものです。電卓は数字を毎回入力する必要がありますが、AI スピーカーは「今日の俺の体重は?」と聞けば、もう知っているので即答してくれる。クラスを使う最大のうま味の 1 つがこれです。

ビジネスロジックを クラスの中に集約 する利点も大きいです。Product クラスを開けば、商品にまつわる計算 (getPriceWithTax getDiscountedPrice isInStock など) が一覧で見えます。コードが大きくなったときに、「税込み価格の計算式、どこに書いたっけ?」と grep する手間が一気に減るのです。

Product クラスの設計図

ここまでの構造を図で整理しておきます。下記の図は、Product クラスがどんなフィールドとメソッドを持っていて、new Product(...) で 2 つのインスタンスを作るとどう見えるか、を表しています。

diagram (will load when visible)

設計図 1 枚 から 実物が何個でも 作れる、というのがクラスの強みです。1 個ごとに違う price を持ちつつ、getPriceWithTax() の計算式 (税込みロジック) は 設計図の 1 ヶ所 にだけ書かれている。ここに修正が入った場合、applebook もまとめて新しい計算式で動き始めます。

テストしやすい設計の話

もう一歩踏み込んで、なぜ「ビジネスロジックを Product クラスに置く」のがテストしやすい設計なのか、を整理します。

たとえば、税込み価格の計算を、商品画面の表示コードに直接ベタ書きしたとします。下記のようなイメージです。

Java

// アンチパターン: 画面のコードに計算式が紛れている String label = "税込価格: " + (int) (product.price * 1.10) + " 円"; System.out.println(label);

一見動くのですが、ここには 2 つの問題が潜んでいます。1 つ目は、画面の表示税込み計算 が混ざっているので、計算だけを自動テストにかけにくいこと。2 つ目は、税率が変わった (10%8% 軽減税率、12% への引き上げなど) ときに、画面コードを全部 grep して直す羽目になることです。

下記のように、計算を Product.getPriceWithTax() に閉じ込めておくと、両方の悩みが解決します。

Java

Product apple = new Product(100); int total = apple.getPriceWithTax(); // 計算はクラスの中で完結 assertEquals(110, total); // テストは値だけを検証

値を返すメソッド画面表示を担当するメソッド は分ける」「同じ入力なら必ず同じ出力」という設計指針は、単一責任の原則 純粋関数 という名前で、業務システム・Web アプリ・ゲームを問わず通用するベストプラクティスです。Product.getPriceWithTax() は完全にこの形に沿っているので、テストもデバッグも極めてやりやすくなります。

よくある間違い

初めてクラス + 税込み計算に挑むと、ほぼ全員がいくつかの罠を踏みます。代表的な 3 つを紹介します。

1 つ目は、税率を 1.10 のようにベタ書きしてしまう ことです。下記のような書き方は動きはしますが、税率が変わった瞬間に「1.10 を grep して全部書き換える」作業が必要になります。

Java

// 微妙な例: 1.10 がコードに直接書かれている (マジックナンバー) return (int) (price * 1.10);

対策として、private static final double TAX_RATE = 0.10; のような 定数 にしておくと、後で TAX_RATE を変更するだけで全箇所が連動します。定数は static final で書くのが Java の慣例です。static で「クラス共通」、final で「変更不可」を意味します。

2 つ目は、整数除算で結果が 0 になる ミスです。たとえば下記のような書き方は、Java の整数演算ルールでとんでもない結果になります。

Java

// NG: int 同士の計算で、税率が 0 になってしまう return price * (1 + 10 / 100); // 10 / 100 は int 同士なので 0

10 / 100int / int なので結果は 0、つまり税抜き価格がそのまま返ってきてしまいます。Java で小数を扱いたいときは、必ずどちらかを double にして 10 / 100.0 のように書きます。今回はあらかじめ TAX_RATE = 0.10 として double で定義しておくのが安全です。

3 つ目は、フィールドを null のまま使ってしまう 罠です。今回の priceint なので null にはなりませんが、String name のような参照型のフィールドを持たせると、コンストラクタで初期化し忘れたときに null のままになり、name.length() のようなメソッドを呼んだ瞬間に NullPointerException が飛びます。Java で最も有名な例外と言っても過言ではないこのエラー、初学者から熟練者まで誰もが一度は踏みます。コンストラクタでフィールドを必ず初期化する、という習慣を最初から付けておきましょう。

余談として、getPriceWithTax() の戻り値を int にするか double にするか、というのも実は設計判断のポイントです。今回は「税込み価格は最終的に消費者に提示する円単位の整数」と決めて、計算結果を (int) でキャストして切り捨てています。実務では BigDecimal を使うのが正解ですが、まずは int で「キャストで切り捨て」の感覚を掴むのがおすすめです。

やってみよう

それでは課題に挑戦しましょう。下記の手順で進めると詰まりにくいです。

  1. 右側のエディタで starter_code を開く
  2. Solution.productTotal(int price) の中で、Product インスタンスを new Product(price) で作り、getPriceWithTax() の結果を return する
  3. 実行ボタンを押して、すべてのテストが緑になることを確認する
  4. Product クラス側の getPriceWithTax() も、price * 1.10(int) キャストで切り捨てる形で実装する

うまくいったら、下記のような書き換えを試して、クラス設計の感覚を広げてみましょう。

  • TAX_RATE0.08 に変更して、100 円が 108 円になることを確認する
  • ProductString name フィールドとコンストラクタ引数を増やして、getLabel() メソッドで "apple: 110 円" のような表示を返してみる
  • getPriceWithTax()static メソッド版 priceWithTax(int price) にも書き直して、呼び方の違いを比べる
  • 税率を 1.10 と直接書いた版と、TAX_RATE 定数を使った版を比べて、「税率を 12% に変えるときの修正範囲」を体感する

この Product クラスはたった 10 数行ですが、ここで身につけた 「データと振る舞いをクラスに閉じ込める」 という感覚は、ここから先の Order クラス、User クラス、ShoppingCart クラス、と発展していく業務システム開発の出発点になります。new で実物を作り、メソッドに仕事を依頼する、という会話のスタイルにじっくり慣れていきましょう。

次のレッスン

次は 銀行口座クラス で、銀行口座クラス を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. Solution クラスに public static int productTotal(int price) を実装する
  2. Product クラスを定義して getPriceWithTax() で税込み価格を返す設計にする (税率 10% は TAX_RATE のような static final な定数で表現するのが望ましい)
  3. 戻り値は int 型。小数は (int) キャストで切り捨てる

入出力例

test-cases.txt

productTotal(100)110 productTotal(99)108 productTotal(3980)4378 productTotal(0)0

ヒント

main.java
main.java
学習モード

メモ

Product クラスで税込価格

⌘S で保存