よく使うJavaライブラリで味わうデザインパターン - Adapterパターン


普段よく使うJavaライブラリにも、GoFのデザインパターンが隠されています。日々の作業が忙しく見逃しがちですが、たまにはじっくり一種の芸術ともいえる美しい設計を味わってみましょう。

今回の芸術

ソースファイル

共通
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...

Logger logger = LoggerFactory.getLogger(this.getClass());
...

クラスパスに追加するjarファイル1

パターン1 - Logbackを使う場合
- slf4j-api-VERSION.jar
- logback-classic-VERSION.jar
- logback-core-VERSION.jar
パターン2 - Log4jを使う場合
- slf4j-api-VERSION.jar
- slf4j-log4j12-VERSION.jar
- log4j-VERSION.jar
パターン3 - Java標準のロガーを使う場合
- slf4j-api-VERSION.jar
- slf4j-jdk14-VERSION.jar
- rt.jar(Javaのコアライブラリなので最初からある)

Javaのロガー(SLF4J)を生成するシーンです。ソースコードはそのままで、クラスパスに追加するjarファイルを取り替えるだけで、ロガーの実装を自由に選ぶことができます。インターフェイスと実装をとてもきれいに切り離していることに芸術性を感じます。

鑑賞のポイント

Javaのロギングライブラリはいろいろある2ため、プロジェクトごとに異なったものを利用していることも多いかと思います。他のプロジェクトのソースを自分のプロジェクトでも使おうとしたとき、利用されているロガーが異なると、ソースコードを修正しないといけなかったり、ロガーの設定ファイルを二重で管理しないといけなかったりと、美しくないことになってしまいます。

今回の鑑賞ポイントは、アダプタークラスを用意することで、実装クラス(ロガー)をすり替えても同じ使い方ができるようになっているところです。おまけに、実装クラスのすり替えはjarを置き換えるだけという、古典的ですが最もシンプルな方法が採用されています。設計の美しさはもとより、ライブラリを使う私たちに対しての設計者の粋な図らいに畏敬の念を抱きます。以下で一緒に鑑賞していきましょう。

Adapterパターンを使わない場合

冒頭のコードをAdapterパターンを使わないで書くと、以下のようになります。

パターン1 - Logbackを使う場合
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.Logger;
...

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger log = lc.getLogger(this.getClass());
パターン2 - Log4jを使う場合
import org.apache.log4j.Logger;
...

Logger logger = Logger.getLogger(this.getClass());
パターン3 - Java標準のロガーを使う場合
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メソッドの中身は以下のようになっています。

org.slf4j.LoggerFactory.java
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

このソースコードを追っていくと、次のような流れでLoggerFactoryLoggerのインスタンスが生成されることがわかります。各クラスがどのjarに含まれているかを【】で書いています。それを意識すると理解しやすいです。

Logbackの場合
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実装クラスを参照して使用することになります。

Logbackの場合
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パターンをうまく使えば,開発チーム内の対人関係が良くなる,という効用もある。低スキル者が滅茶苦茶なコードを書いた際,そのクラスのソースコード自体に横から手を出して他のメンバが書き変えようとすると,最初にコーディングした方のプログラマは「傷ついた」と感じるものだ。それを避けるために,ラッパークラスを一段かませて,ラッパークラス側を充実させて高品質にする。

GoFの23のデザインパターンを,Javaで活用するための一覧表 より

最後に

わざわざ美術館に行かなくても、たった1行のコードを眺めるだけで知的な愉しみを味わうことができるのは、プログラマーの醍醐味でしょう。

Adapterパターンの芸術性に共感してくださったエンジニアの方は、ぜひ当社(クオリサイトテクノロジーズ株式会社)の採用担当までご連絡ください!

参考URL

関連記事

インスタンスを作る

インターフェイスをシンプルにする

他のクラスに任せる

脚注


  1. 実際の開発シーンでは、jarファイルの追加には、Mavenなどのモジュール管理ツールを使います。Mavenの設定ファイル(pom.xml)への記述方法は、こちらのSLF4Jの公式リファレンスを参照してください。 

  2. javaのロガーが多すぎて訳が解らないので整理してみました