普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。
今回の芸術
ソースファイル
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
Logger logger = LoggerFactory.getLogger(this.getClass());
...
クラスパスに追加するjarファイル1
- slf4j-api-VERSION.jar
- logback-classic-VERSION.jar
- logback-core-VERSION.jar
- slf4j-api-VERSION.jar
- slf4j-log4j12-VERSION.jar
- log4j-VERSION.jar
- slf4j-api-VERSION.jar
- slf4j-jdk14-VERSION.jar
- rt.jar(Javaのコアライブラリなので最初からある)
Javaのロガー(SLF4J)を生成するシーンです。ソースコードはそのままで、クラスパスに追加するjarファイルを取り替えるだけで、ロガーの実装を自由に選ぶことができます。インターフェイスと実装をとてもきれいに切り離していることに芸術性を感じます。
鑑賞のポイント
Javaのロギングライブラリはいろいろある2ため、プロジェクトごとに異なったものを利用していることも多いかと思います。他のプロジェクトのソースを自分のプロジェクトでも使おうとしたとき、利用されているロガーが異なると、ソースコードを修正しないといけなかったり、ロガーの設定ファイルを二重で管理しないといけなかったりと、美しくないことになってしまいます。
今回の鑑賞ポイントは、アダプタークラスを用意することで、実装クラス(ロガー)をすり替えても同じ使い方ができるようになっているところです。おまけに、実装クラスのすり替えはjarを置き換えるだけという、古典的ですが最もシンプルな方法が採用されています。設計の美しさはもとより、ライブラリを使う私たちに対しての設計者の粋な図らいに畏敬の念を抱きます。以下で一緒に鑑賞していきましょう。
Adapterパターンを使わない場合
冒頭のコードをAdapterパターンを使わないで書くと、以下のようになります。
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.Logger;
...
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger log = lc.getLogger(this.getClass());
import org.apache.log4j.Logger;
...
Logger logger = Logger.getLogger(this.getClass());
import java.util.logging.Logger;
...
Logger logger = Logger.getLogger(this.getClass());
ロガーごとにインスタンス取得時の記述と、import文の記述が異なっています。また、インスタンス化するLogger
クラスが異なるため、それぞれで使えるメソッドも異なります。この状態でロギングライブラリを変更すると、たくさんのソースコードを置換することになってしまいます。さらに、もし特定のロギングライブラリにしか存在しないメソッドを使っていた場合は、置換さえできません。
Adapterパターンを使った場合
冒頭のコードではAdapterパターンが使われています。もう一度見てみましょう。
まずは、クラスパスに追加するjarファイルです。どのロギングライブラリにも3つのjarファイルがありますが、役割は同じで、
- SLF4Jのインターフェイス(SLF4Jが配布しているjarファイル)
- SLF4Jのインターフェイスの実装。内部でそれぞれのロギングライブラリを呼ぶ。アダプターの役割。
- それぞれのロギングライブラリの実装
となっています。
ロギングライブラリの種類とjarの役割を表にすると以下のようになります。
Logback | Log4j | Java標準のロガー | |
---|---|---|---|
共通IF | slf4j-api-VERSION.jar | 左に同じ | 左に同じ |
IF実装(アダプター) | logback-classic-VERSION.jar | slf4j-log4j12-VERSION.jar | slf4j-jdk14-VERSION.jar |
ロギングライブラリ実装 | logback-core-VERSION.jar | log4j-VERSION.jar | rt.jar |
※IFはインターフェイスの略
そして、ロガーインスタンスは、どのロギングライブラリを使っていても、一律で次のように生成します。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
Logger logger = LoggerFactory.getLogger(this.getClass());
...
このgetLogger
メソッドの中身は以下のようになっています。
package org.slf4j;
...
// 共通IFのjarにあるクラス
public final class LoggerFactory {
...
public static Logger getLogger(String name) {
// org.slf4j.ILoggerFactoryのインスタンスを生成
// 実装クラスはIF実装(アダプター)のjar内にあるクラス(下の表を参照)
ILoggerFactory iLoggerFactory = getILoggerFactory();
// org.slf4j.Loggerのインスタンスを生成
// 実装クラスはIF実装(アダプター)のjar内にあるクラス(下の表を参照)
return iLoggerFactory.getLogger(name);
}
...
}
ILoggerFactory
、およびLogger
の実装クラスは以下のようになっています。いずれのクラスもそれぞれのロギングライブラリのIF実装(アダプター)jarに含まれます。
Logback | Log4j | Java標準のロガー | |
---|---|---|---|
ILoggerFactoryの実装クラス | ch.qos.logback.classic.LoggerContext | org.slf4j.impl.Log4jLoggerFactory | org.slf4j.jul.JDK14LoggerFactory |
org.slf4j.Loggerの実装クラス | ch.qos.logback.classic.Logger | org.slf4j.impl.Log4jLoggerAdapter | org.slf4j.jul.JDK14LoggerAdapter |
このソースコードを追っていくと、次のような流れでLoggerFactory
とLogger
のインスタンスが生成されることがわかります。各クラスがどのjarに含まれているかを【】で書いています。それを意識すると理解しやすいです。
org.slf4j.LoggerFactory【共通IFのjar】
↓
↓ org/slf4j/impl/StaticLoggerBinder.class ファイルを
↓ 探してインスタンス化
↓
org.slf4j.impl.StaticLoggerBinder【IF実装(アダプター)のjar】
↓
↓ org.slf4j.ILoggerFactoryインターフェイス
↓ を実装しているLoggerContextをインスタンス化
↓
ch.qos.logback.classic.LoggerContext【IF実装(アダプター)のjar】
↓
↓ org.slf4j.Loggerインターフェイス
↓ を実装しているch.qos.logback.classic.Loggerをインスタンス化
↓
ch.qos.logback.classic.Logger【IF実装(アダプター)のjar】
最終的に生成されたLogger
は、ロギングライブラリの実装jarに含まれるクラスではなく、IF実装のjarに含まれるクラスであることがポイントです。このクラスは、ロギングライブラリの参照をメンバ変数に保持していて、ログ出力のときにはそれを呼び出します。
以上のようにすることで、SLF4Jを使う人は、ロギングライブラリのインスタンスを直接参照するのではなく、そのアダプターに相当するIF実装クラスを参照して使用することになります。
SLF4Jを使う人
↓
↓ 使う
↓
org.slf4j.Logger【共通IFのjar】
この実体はch.qos.logback.classic.Logger【IF実装(アダプター)のjar】
↓
↓ 内部で使っている
↓
ch.qos.logback.core.XXXAppender【ロギングライブラリ実装のjar】
Adapterパターンのまとめ
Adapterパターンは、Wrapper(ラッパー)パターンと呼ばれることもあります。そのままでは使いにくいクラスを使いやすいように適合させるために、新しいクラスでラップするのです。ちなみに、そのままでは使いにくいクラスというのは、似たようなクラス間でメソッドに統一感がなかったり、ソースコードが汚かったりするクラスのことです。
どうりでSLF4Jのインターフェイスはきれいなはずです。それは、きれいにすることを目的に、Adapterパターンで整形された結果なのです。
今回の例にはAdapterパターンの魅力が最大限引き出されているものを選びましたが、ここまで徹底的にやらなくても(IF、IF実装、ロギングライブラリ実装でjarファイルを分けるまでやらなくても)、既存のままでは使いにくいクラスのラッパークラスを作るだけで、Adapterパターンを使っていることになります。
Adapterパターンへの専門家のコメント
多くの専門家からも、Adapterパターンを評価するコメントが寄せられています。
結城浩さん
プログラムの世界でも、すでに提供されているものがそのままつかえないときに、必要な形に変換してから利用することがよくあります。「すでに提供されているもの」と「必要なもの」の間の「ずれ」を埋めるようなデザインパターン、これがAdapterパターンです。
『Java言語で学ぶデザインパターン入門』 より
lang_and_engineさん
Adapterパターンをうまく使えば,開発チーム内の対人関係が良くなる,という効用もある。低スキル者が滅茶苦茶なコードを書いた際,そのクラスのソースコード自体に横から手を出して他のメンバが書き変えようとすると,最初にコーディングした方のプログラマは「傷ついた」と感じるものだ。それを避けるために,ラッパークラスを一段かませて,ラッパークラス側を充実させて高品質にする。
最後に
わざわざ美術館に行かなくても、たった1行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。
Adapterパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!
参考URL
関連記事
インスタンスを作る
- よく使うJavaライブラリで味わうデザインパターン - Factoryパターン
- よく使うJavaライブラリで味わうデザインパターン - Builderパターン
- よく使うJavaライブラリで味わうデザインパターン - Abstract Factoryパターン
インターフェイスをシンプルにする
- よく使うJavaライブラリで味わうデザインパターン - Facadeパターン
- よく使うJavaライブラリで味わうデザインパターン - Adapterパターン
他のクラスに任せる
脚注
-
実際の開発シーンでは、jarファイルの追加には、Mavenなどのモジュール管理ツールを使います。Mavenの設定ファイル(pom.xml)への記述方法は、こちらのSLF4Jの公式リファレンスを参照してください。 ↩