演習: インターフェイス
題材ごとに logger、discount、book、shipping のパッケージを分けて作ります。各クラスと Main を対応するパッケージに置き、先頭に package logger; のようにパッケージ宣言を書きます。
Directoryoop-practice/
Directorylogger/ 6-A, 6-B, 6-C, 6-F
- …
Directorydiscount/ 6-D, 6-E
- …
Directorybook/ 6-G, 6-H
- …
Directoryshipping/ 6-I
- …
6-A. Logger の定義と ConsoleLogger
Section titled “6-A. Logger の定義と ConsoleLogger”ここから 6-F までは logger パッケージで取り組みます。
ログを出力する Logger インターフェイスを定義し、それを実装する ConsoleLogger を作ってください。
Logger インターフェイスを次のとおり定義します。
log(String message): 戻り値なしのメソッドを宣言する
Logger を実装する ConsoleLogger を次のとおり定義します。
log(String message):[LOG] messageの形式でSystem.out.printlnに出力する
ConsoleLogger logger = new ConsoleLogger();logger.log("処理を開始しました");出力例:
[LOG] 処理を開始しましたヒント
インターフェイスは public interface Logger { void log(String message); } のように、メソッドの宣言だけを書きます。本文 interface キーワード を参照してください。
ConsoleLogger の宣言は public class ConsoleLogger implements Logger です。log メソッドに @Override を付けて実装します。
6-B. SilentLogger の追加
Section titled “6-B. SilentLogger の追加”Logger を実装する SilentLogger を追加してください。log メソッドは何も出力しません(本体を空にする)。テストなどでログ出力を止めたい場面で使います。
Main.java では、変数の型を Logger にして、ConsoleLogger と SilentLogger の両方を扱います。
Logger logger1 = new ConsoleLogger();Logger logger2 = new SilentLogger();
logger1.log("これは出力される");logger2.log("これは出力されない");System.out.println("プログラム終了");出力例:
[LOG] これは出力されるプログラム終了ヒント
SilentLogger の log は @Override を付けて、本体を {} のように空にします。
本文 インターフェイス型での宣言 のとおり、Logger 型の変数には Logger を実装したクラスのインスタンスをどれでも代入できます。同じ Logger 型でも、logger2.log(...) は実体の SilentLogger の実装が呼ばれるため、何も出力されません。
6-C. インターフェイスのコンパイルエラー
Section titled “6-C. インターフェイスのコンパイルエラー”次のコードはコンパイルエラーになります。エラーの理由を説明し、コンパイルが通るように修正してください。
Logger logger = new Logger();logger.log("動作確認");ヒント
本文 インターフェイス型での宣言 の警告を参照してください。インターフェイスはメソッドの宣言だけを持ち、処理を持ちません。そのため、インターフェイス自体のインスタンスは作れません。
new Logger() を、Logger を実装したクラス(ConsoleLogger など)の new に変えると、コンパイルが通ります。
6-D. DiscountPolicy の定義と複数実装
Section titled “6-D. DiscountPolicy の定義と複数実装”ここから 6-E までは discount パッケージで取り組みます。
商品価格に対する割引ルールを表す DiscountPolicy インターフェイスを定義し、2 つの実装を作ってください。
DiscountPolicy インターフェイスを次のとおり定義します。
apply(int price): 割引後の価格(int)を返すメソッドを宣言する
DiscountPolicy を実装するクラスを 2 つ、次のとおり定義します。
RegularDiscount: 割引なし。priceをそのまま返すMemberDiscount: 会員割引。priceの 10% を引いた価格を返す(price - price / 10)
DiscountPolicy regular = new RegularDiscount();DiscountPolicy member = new MemberDiscount();
System.out.println(regular.apply(1000));System.out.println(member.apply(1000));出力例:
1000900ヒント
Logger と同じく、DiscountPolicy インターフェイスにメソッドの宣言(int apply(int price);)を書き、各クラスが implements DiscountPolicy で実装します。違いは、apply が戻り値(int)を持つ点です。各実装は割引後の価格を return で返します。
6-E. 複数の割引の一括適用
Section titled “6-E. 複数の割引の一括適用”Main.java で複数の DiscountPolicy を List に入れ、同じ価格にそれぞれの割引を適用した結果を出力してください。
import java.util.ArrayList;import java.util.List;
List<DiscountPolicy> policies = new ArrayList<>();policies.add(new RegularDiscount());policies.add(new MemberDiscount());
int price = 2000;for (DiscountPolicy policy : policies) { System.out.println(policy.apply(price));}出力例:
20001800ヒント
DiscountPolicy 型の List には、DiscountPolicy を実装したクラスのインスタンスをどれでも入れられます。ループの中で policy の型は DiscountPolicy ですが、policy.apply(price) は各要素の実体に応じた割引を実行します。
6-F. Logger を差し替えられる OrderService
Section titled “6-F. Logger を差し替えられる OrderService”注文を処理する OrderService を定義してください。Logger(6-A で作ったもの)をコンストラクターで受け取り、注文時にログを出力します。
- フィールド:
logger(Logger型、private final) - コンストラクターで
Loggerを受け取る placeOrder(int amount):logger.log("注文を受け付けました: " + amount + "円")を呼ぶ
Main.java では、同じ OrderService のコードのまま、渡す Logger の実装を差し替えて挙動が変わることを確認します。
OrderService consoleService = new OrderService(new ConsoleLogger());OrderService silentService = new OrderService(new SilentLogger());
consoleService.placeOrder(1500);silentService.placeOrder(800);出力例:
[LOG] 注文を受け付けました: 1500円ヒント
フィールドの型を Logger インターフェイスにすると、ConsoleLogger でも SilentLogger でも受け取れます。本文 抽象への依存 と同じ構造です。
OrderService のコードは ConsoleLogger や SilentLogger という具体的なクラスを知りません。Main.java で渡す実装を変えるだけで、ログが出力されるかどうかが変わります。
6-G. BookFormatter のインターフェイス化
Section titled “6-G. BookFormatter のインターフェイス化”ここから 6-H までは book パッケージで取り組みます。
まず 演習 1-H で作った Book(getTitle、getPrice、getStock を持つもの)を book パッケージにコピーし、先頭に package book; を加えます。
演習 5-I で具象クラスとして作った BookFormatter を、インターフェイスに変更します。本文 GradeJudge のインターフェイス化 と同じリファクタリングです。
BookFormatter インターフェイスを次のとおり定義します。
format(Book book): 整形した文字列を返すメソッドを宣言する
BookFormatter を実装するクラスを 2 つ、次のとおり定義します。
SimpleFormatter:『書名』だけを返すDetailFormatter:『書名』(価格円、在庫 N 冊)を返す(5-I のBookFormatterと同じ形式)
Book book = new Book("こころ", 650, 3);
BookFormatter simple = new SimpleFormatter();BookFormatter detail = new DetailFormatter();
System.out.println(simple.format(book));System.out.println(detail.format(book));出力例:
『こころ』『こころ』(650円、在庫 3 冊)ヒント
5-I では BookFormatter を具象クラスとして書きましたが、整形の仕方が複数あるため、BookFormatter をインターフェイスにして実装を分けます。format の宣言だけを BookFormatter に残し、SimpleFormatter と DetailFormatter がそれぞれ implements BookFormatter で実装します。
6-H. BookService での整形の差し替え
Section titled “6-H. BookService での整形の差し替え”演習 5-J で作った BookService を、6-G の BookFormatter インターフェイスを受け取る形にしてください。コードは 5-J とほぼ同じですが、formatter フィールドの型がインターフェイスになります。
- フィールド:
formatter(BookFormatter型、private final)、books(List<Book>型、private final) - コンストラクターで
BookFormatterとList<Book>を受け取る showAll(): すべての本をformatter.format(book)で整形して出力する
Main.java では、同じ books に対して、渡す BookFormatter の実装を変えて出力を切り替えます。
import java.util.ArrayList;import java.util.List;
List<Book> books = new ArrayList<>();books.add(new Book("こころ", 650, 3));books.add(new Book("人間失格", 590, 0));
BookService simpleService = new BookService(new SimpleFormatter(), books);BookService detailService = new BookService(new DetailFormatter(), books);
System.out.println("--- 簡易表示 ---");simpleService.showAll();
System.out.println("--- 詳細表示 ---");detailService.showAll();出力例:
--- 簡易表示 ---『こころ』『人間失格』--- 詳細表示 ---『こころ』(650円、在庫 3 冊)『人間失格』(590円、在庫 0 冊)ヒント
BookService のフィールドの型が BookFormatter インターフェイスなので、SimpleFormatter でも DetailFormatter でも受け取れます。BookService のコードは変えずに、Main.java で渡す実装を変えるだけで、本の表示形式が切り替わります。これが 抽象への依存 の利点です。
6-I. ShippingFee インターフェイスの設計
Section titled “6-I. ShippingFee インターフェイスの設計”shipping パッケージで取り組みます。
配送料を計算する仕組みを、自分でインターフェイスとして設計してください。配送方法によって料金の計算方法が異なるとき、インターフェイスで差し替えられるようにします。
次の要件を満たすインターフェイスと実装を定義します。
ShippingFeeインターフェイス:calculate(int orderAmount)を宣言する。注文金額を受け取り、配送料(int)を返すStandardShipping: 配送料は一律500円FreeShipping: 注文金額が3000円以上なら配送料0円、未満なら500円
ShippingFee standard = new StandardShipping();ShippingFee free = new FreeShipping();
System.out.println(standard.calculate(5000));System.out.println(free.calculate(5000));System.out.println(free.calculate(2000));出力例:
5000500ヒント
ShippingFee インターフェイスには int calculate(int orderAmount); の宣言だけを書き、StandardShipping と FreeShipping がそれぞれ implements ShippingFee で実装します。
StandardShipping は常に 500 を返すだけ、FreeShipping は if で注文金額を判定して配送料を返します。配送方法ごとに計算が違っても、calculate という共通の契約でまとめられる点が、インターフェイスで設計する利点です。