Skip to content
Playground

演習: インターフェイス

題材ごとに loggerdiscountbookshipping のパッケージを分けて作ります。各クラスと 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-F までは logger パッケージで取り組みます。

ログを出力する Logger インターフェイスを定義し、それを実装する ConsoleLogger を作ってください。

Logger インターフェイスを次のとおり定義します。

  • log(String message): 戻り値なしのメソッドを宣言する

Logger を実装する ConsoleLogger を次のとおり定義します。

  • log(String message): [LOG] message の形式で System.out.println に出力する
Main.java
ConsoleLogger logger = new ConsoleLogger();
logger.log("処理を開始しました");

出力例:

[LOG] 処理を開始しました
ヒント

インターフェイスは public interface Logger { void log(String message); } のように、メソッドの宣言だけを書きます。本文 interface キーワード を参照してください。

ConsoleLogger の宣言は public class ConsoleLogger implements Logger です。log メソッドに @Override を付けて実装します。

Logger を実装する SilentLogger を追加してください。log メソッドは何も出力しません(本体を空にする)。テストなどでログ出力を止めたい場面で使います。

Main.java では、変数の型を Logger にして、ConsoleLoggerSilentLogger の両方を扱います。

Main.java
Logger logger1 = new ConsoleLogger();
Logger logger2 = new SilentLogger();
logger1.log("これは出力される");
logger2.log("これは出力されない");
System.out.println("プログラム終了");

出力例:

[LOG] これは出力される
プログラム終了
ヒント

SilentLoggerlog@Override を付けて、本体を {} のように空にします。

本文 インターフェイス型での宣言 のとおり、Logger 型の変数には Logger を実装したクラスのインスタンスをどれでも代入できます。同じ Logger 型でも、logger2.log(...) は実体の SilentLogger の実装が呼ばれるため、何も出力されません。

6-C. インターフェイスのコンパイルエラー

Section titled “6-C. インターフェイスのコンパイルエラー”

次のコードはコンパイルエラーになります。エラーの理由を説明し、コンパイルが通るように修正してください。

Main.java
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
Main.java
DiscountPolicy regular = new RegularDiscount();
DiscountPolicy member = new MemberDiscount();
System.out.println(regular.apply(1000));
System.out.println(member.apply(1000));

出力例:

1000
900
ヒント

Logger と同じく、DiscountPolicy インターフェイスにメソッドの宣言(int apply(int price);)を書き、各クラスが implements DiscountPolicy で実装します。違いは、apply が戻り値(int)を持つ点です。各実装は割引後の価格を return で返します。

Main.java で複数の DiscountPolicyList に入れ、同じ価格にそれぞれの割引を適用した結果を出力してください。

Main.java
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));
}

出力例:

2000
1800
ヒント

DiscountPolicy 型の List には、DiscountPolicy を実装したクラスのインスタンスをどれでも入れられます。ループの中で policy の型は DiscountPolicy ですが、policy.apply(price) は各要素の実体に応じた割引を実行します。

6-F. Logger を差し替えられる OrderService

Section titled “6-F. Logger を差し替えられる OrderService”

注文を処理する OrderService を定義してください。Logger(6-A で作ったもの)をコンストラクターで受け取り、注文時にログを出力します。

  • フィールド: loggerLogger 型、private final
  • コンストラクターで Logger を受け取る
  • placeOrder(int amount): logger.log("注文を受け付けました: " + amount + "円") を呼ぶ

Main.java では、同じ OrderService のコードのまま、渡す Logger の実装を差し替えて挙動が変わることを確認します。

Main.java
OrderService consoleService = new OrderService(new ConsoleLogger());
OrderService silentService = new OrderService(new SilentLogger());
consoleService.placeOrder(1500);
silentService.placeOrder(800);

出力例:

[LOG] 注文を受け付けました: 1500円
ヒント

フィールドの型を Logger インターフェイスにすると、ConsoleLogger でも SilentLogger でも受け取れます。本文 抽象への依存 と同じ構造です。

OrderService のコードは ConsoleLoggerSilentLogger という具体的なクラスを知りません。Main.java で渡す実装を変えるだけで、ログが出力されるかどうかが変わります。

6-G. BookFormatter のインターフェイス化

Section titled “6-G. BookFormatter のインターフェイス化”

ここから 6-H までは book パッケージで取り組みます。

まず 演習 1-H で作った BookgetTitlegetPricegetStock を持つもの)を book パッケージにコピーし、先頭に package book; を加えます。

演習 5-I で具象クラスとして作った BookFormatter を、インターフェイスに変更します。本文 GradeJudge のインターフェイス化 と同じリファクタリングです。

BookFormatter インターフェイスを次のとおり定義します。

  • format(Book book): 整形した文字列を返すメソッドを宣言する

BookFormatter を実装するクラスを 2 つ、次のとおり定義します。

  • SimpleFormatter: 『書名』 だけを返す
  • DetailFormatter: 『書名』(価格円、在庫 N 冊) を返す(5-I の BookFormatter と同じ形式)
Main.java
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 に残し、SimpleFormatterDetailFormatter がそれぞれ implements BookFormatter で実装します。

6-H. BookService での整形の差し替え

Section titled “6-H. BookService での整形の差し替え”

演習 5-J で作った BookService を、6-G の BookFormatter インターフェイスを受け取る形にしてください。コードは 5-J とほぼ同じですが、formatter フィールドの型がインターフェイスになります。

  • フィールド: formatterBookFormatter 型、private final)、booksList<Book> 型、private final
  • コンストラクターで BookFormatterList<Book> を受け取る
  • showAll(): すべての本を formatter.format(book) で整形して出力する

Main.java では、同じ books に対して、渡す BookFormatter の実装を変えて出力を切り替えます。

Main.java
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
Main.java
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));

出力例:

500
0
500
ヒント

ShippingFee インターフェイスには int calculate(int orderAmount); の宣言だけを書き、StandardShippingFreeShipping がそれぞれ implements ShippingFee で実装します。

StandardShipping は常に 500 を返すだけ、FreeShippingif で注文金額を判定して配送料を返します。配送方法ごとに計算が違っても、calculate という共通の契約でまとめられる点が、インターフェイスで設計する利点です。