Lombok ライブラリ
概要
Javaでは、クラスにフィールドを追加することによって、
既存のロジックに影響が出たり、新しく追加する必要があるメソッドが存在する。
また、これらはフィールドが増えるごとにコードの大部分を占めるようになるにも関わらず、
実装は極めて機械的であり、そのロジックはクラスにとって本質的でないことが多い。
このようなコードをボイラープレートコードという。
今回は、アノテーション記述によって、ボイラープレートコードを自動生成する
Javaライブラリの Lombok を調査してみた。
導入
インストール
個人的に分かりやすかったのは、以下のサイト。
【Java】Lombokで冗長コードを削減しよう | キャスレーコンサルティング 技術ブログ
なお、lombok.jar を Eclipse へのインストールを実行した後は、
eclipse.ini に以下2行が入っているか確認した方が良い。
=============================
-javaagent:lombok.jar
-Xbootclasspath/a:lombok.jar
=============================
私が起動したときは、-Xbootclasspath/a:lombok.jar が記述されて
いなかったため、動かなかった。(Eclipseバージョン: Mars.1 Release (4.5.1))
導入効果
可読性向上
ボイラープレートコードが減ることによって、コードの記述量がはっきりと減る。
同時に、本質的なコードが大部分を占めるようになるため、コードが分かりやすくなる。
導入例
Before
public class Book { private String id; private String title; private String author; private int page; public Book(String id, String title, String author, int page) { this.id = id; this.title = title; this.author = author; this.page = page; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } }
After
import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Book { private String id; private String title; private String author; private int page; }
Use Case
public class Main { public static void main(String[] args) { Book book1 = new Book("10", "EFFECTIVE JAVA 第2版", "Joshua Bloch", 327); Book book2 = new Book("10", "EFFECTIVE JAVA 第2版", "Joshua Bloch", 327); Book book3 = new Book("15", "星の王子さま", "サン=テグジュペリ", 245); Book book4 = new Book("15", "星の王子さま", "サン=テグジュペリ", 246); // toStringメソッド System.out.println(book1); // equalsメソッド System.out.println("book1.equals(book2): " + book1.equals(book2)); System.out.println("book1.equals(book3): " + book1.equals(book3)); System.out.println("book3.equals(book4): " + book3.equals(book4)); // hashCodeメソッド System.out.println("book1 hash:" + book1.hashCode() + ", book2 hash: " + book2.hashCode()); System.out.println("book1 hash:" + book1.hashCode() + ", book3 hash: " + book3.hashCode()); System.out.println("book3 hash:" + book3.hashCode() + ", book4 hash: " + book4.hashCode()); } }
Console Output
Book(id=10, title=EFFECTIVE JAVA 第2版, author=Joshua Bloch, page=327) book1.equals(book2): true book1.equals(book3): false book3.equals(book4): false book1 hash:712838778, book2 hash: 712838778 book1 hash:712838778, book3 hash: 1107997353 book3 hash:1107997353, book4 hash: 1107997354
Lombok ライブラリ
この他、どのような記述が可能か本家(Lombok)で調査。
(記事執筆時のバージョン :v1.16.6)
規模が大きくなったため、別ページに分割記述。
Lombok ライブラリ - クラス関連 - Status Code 303 - See Other
Lombok ライブラリ - フィールド関連 - Status Code 303 - See Other
Lombok ライブラリ - その他 - Status Code 303 - See Other
対象パッケージ
- lombok
- lombok.experimental
- lombok.extern.*
今後更新予定
- lombok.exprimental
Java でのマイナンバーのチェックデジット作成メソッド
概要
http://www.soumu.go.jp/main_content/000327387.pdf
上記第五条に定義があり、そこでは、個人番号を構成する11桁の番号
およびその検査用数字(チェックデジット)で構成されるらしい。
そこで、11桁の番号からチェックデジットを計算するメソッドを作成してみる。
詳細内容
今回作成したチェックデジットを取得するメソッド(getCheckDigitメソッド)。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Main { public static void main(String[] args) throws IOException { // 使用例. // 標準入力からマイナンバーを拾って、そのチェックデジット値を標準出力に表示. String str; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); while ((str = br.readLine()) != null) { System.out.println(getCheckDigit(str)); } } /** * マイナンバー(チェックデジットを除く)からチェックデジットを取得する. * * @param myNumber * マイナンバー(正規表現 {@code "^[0-9]{11}$" } にマッチングする文字列) * @return チェックデジット({@code 0 <= x && x < 10} を満たす整数 x) * @throws NullPointerException * myNumber が null だった場合. * @throws IllegalArgumentException * myNumber が null ではないが,11桁の番号で構成されていない場合. */ public static int getCheckDigit(String myNumber) { Matcher m = Pattern.compile("^([0-9]{11})$").matcher(myNumber); if (m.find()) { long value = Long.parseLong(m.group(1)); int sum = 0; for (int i = 1; i <= 11; i++) { sum += (value % 10) * (i < 7 ? i + 1 : i - 5); value /= 10; } int rem = sum % 11; return rem <= 2 ? 0 : 11 - rem; } throw new IllegalArgumentException("My number must be 11-digit number."); } }
説明するほどではないと思うが、一応、メソッド説明。
まずは、入力値の正当性を評価する。
Matcher m = Pattern.compile("^([0-9]{11})$").matcher(myNumber); if (m.find()) { (中身は省略) } throw new IllegalArgumentException("My number must be 11-digit number.");
もし、Matcherが番号11桁を認識しなければ(m.find()で false を返すとき)、IllegalArgumentException が発生して終了する。
また、入力値(myNumber)が null だったら、matcherメソッドで NullPointerException が発生する。
これで、if文の内部の処理が動作するときは、myNumberが番号11桁であることが保証される。
なお、入力値に数値以外が入っているか、数字の桁が足りないかを判断したいときは、
Matcher m = Pattern.compile("^([0-9]+)$").matcher(myNumber); if (m.find()) { if(myNumber.length() == 11) { (チェックデジットの作成処理) }else { // 数値が11桁でないときの処理 } } // 数値以外の文字が含まれていたときの処理
のように変更すれば良い。今回は両方を一つの例外で扱っている。
次に、チェックデジットを算出する。第五条では、
====================================================
算式 をで除した余り
ただし、をで除した余り の場合は、 とする。
算式の符号
個人番号を構成する検査用数字以外の11桁の番号の最下位の桁を桁目としたときの 桁目の数字
のとき 、のとき、
====================================================
とあるので、これを実装する。
int sum = 0; for (int i = 1; i <= 11; i++) { sum += (value % 10) * (i < 7 ? i + 1 : i - 5); value /= 10; }
この部分で、を作成。
なお、 に該当するのが、value % 10 (最下位桁の数値を取得)。
に該当するのが、i < 7 ? i + 1 : i - 5 (7桁目未満はその桁数+1、それ以外はその桁数から-5)。
そして、次の処理時にの最下位桁を次の桁に設定するために、valueを10で割ってから次の処理に動いている。
あとは、さきほど計算した値の11で除した余りが1以下であるなら0を返し、それ以外は、
をで除した余りを下記 return 文で返すようにしている。
int rem = sum % 11; return rem <= 1 ? 0 : 11 - rem;
Java の switch 構文落とし穴
概要
switch 構文は、ある変数がある値のときに処理を分岐させる命令であり、
数多くの言語でサポートされているが、言語により微妙に仕様が異なっている。
言語共通の話題も数多く含まれているが、
今回は、Java を前提に switch 構文について記述する。
詳細説明
switch 構文の記述方法
int value = 0; // valueの値により処理を分岐する switch (value) { case 0: // 処理1 break; case 10: // 処理2 break; default: // 処理3 }
上記 switch 構文は、次の if-else 構文と等価。
int value = 0; // valueの値により処理を分岐する if (value == 0) { // 処理1 }else if(value == 10) { // 処理2 }else { // 処理3 }
使い分け基準
一般的に、処理の分岐条件がある変数の値に依存する場合は switch 構文を使う。
理由
if-else 構文とは違い、分岐条件が値の一致しかできないため、
ソースコードを見る人にその意図が明確に伝わるから。
フォールスルーを極力使わない
フォールスルーとは
switch 構文における case 文の break を省略した記述。
switch 構文にある break を記述することは強制ではない。これらは次の case 文の処理を実行させないためにある。
もし、次の case 文の処理を続けて実行したいときは、break を省略できる。
しかし、フォールスルーは使い方により、バグの温床になる可能性がありオススメしない。
フォールスルーが利用される場合
基本的には、複数の値に対して同じ処理がある場合。
int value = 0; switch (value) { case 0: case 10: // 処理1 break; default: // 処理2 }
上記 switch 構文は、次の if-else 構文と等価であり、直感的で読みやすい。
int value = 0; if (value == 0 || value == 10) { // 処理1 }else { // 処理2 }
危険なフォールスルー
しかし、次のように処理を記述するとフォールスルーは途端に複雑になる。
int value = 0; switch (value) { case 0: // 処理1 case 10: // 処理2 break; default: // 処理3 }
これは、次の if-else 構文と同等。
int value = 0; if (value == 0 || value == 10) { if(value == 0) { // 処理1 } // 処理2 }else { // 処理3 }
これは、value が0でも10でも処理2が実行されることを意味する。
例えば、処理1と処理2がある同じ変数の初期化を行っているとすれば、
value が 0 でも10でも、最終的には同じ値が変数に格納される。
さらに、もう一個増やしてみよう。
int value = 0; switch (value) { case 0: // 処理1 case 10: // 処理2 case 20: // 処理3 break; default: // 処理4 }
これは、次の if-else 構文と同等。
int value = 0; if (value == 0 || value == 10 || value == 20) { if(value == 0 || value == 10) { if(value == 0) { // 処理1 } // 処理2 } // 処理3 }else { // 処理4 }
まとめ:フォールスルーが危険な理由
- 処理が複雑になりやすい
- 記述ミスで break を忘れた場合、バグの温床となりうる
なお、C#.NET では、上記のようなフォールスローは言語仕様で記述できないようになっている。
分岐用変数に極力オブジェクト変数を使わない
さきほどの例では、switch 変数の分岐用変数は int 型であったが、他にも次の変数が指定できる。
- int以下の整数基本データ(int/char/short/byte)
- 列挙型 (Java SE 1,5 から)
- String型 (JavaSE 1.7 から)
- int以下の整数基本データのラッパークラス(Integer/Char/Short/Byte)
もし、分岐条件の変数にint以下の整数基本データ「以外」を指定するなら、注意すべき事項がある。
1. null は分岐条件の値には指定できない
String value = getValue(); // 値をメソッドから取得 switch (value) { case "A": // 処理1 break; case null: //コンパイルエラー( null は指定できない) // 処理2 break; default: // 処理3 }
null の場合の処理分岐は、switch 文ではできない。
2. Java 内部実装による例外発生の可能性
さきほどのコードで、getValueメソッドの戻り値が null だったら NullPointerException が発生する。
極端な話、次のコードでも例外が発生する。
String value = null; switch(value){ default: }
原因
Java が case 文の処理を選択する際に内部的に String 型の equlas メソッドを呼び出しているため。
同様にラッパーオブジェクト、列挙型も内部的なメソッド呼び出しのために同様の例外発生が起こる可能性がある。
対策
オブジェクト型を条件分岐の値として使いたい場合は、以下を行う事。
- 条件分岐の値がメソッド処理結果だった場合、 メソッド仕様として null を返さない事を保証
- null チェックを switch 構文前に行う
switch 構文を用いるより、列挙型を用いた State パターンを実装する
State パターンとは
GoF(Gang of Four)デザインパターンの一つ。状態をクラスとして表すパターン。
例えば、あるサイトでの会員登録の画面を例にすると、
会員情報入力/入力項目確認/会員登録という段階があったとき、「次へ」のボタンを押すとき各段階において処理が異なる。
各段階を表現した状態クラスを導入することで、状態と処理が対応した分かりやすい構造ができる。
さきほどの例でいえば、valueが0のとき、valueが10のときは状態になり、
各処理がそのとき実行される処理となる。
State パターンへの書き換え
例えば次の switch 構文は、
int value = getValue(); // 状態を表す int 型を返す // valueの値により処理を分岐する switch (value) { case 0 : // 処理1 break; case 1 : // 処理2 break; default: // 処理3 }
クラス定義とロジック部分の2か所に分けて以下のように記述される。
ロジック部
State value = getValue(); // 処理に応じた State型を返すようにする。 value.doSomething(); // case 文にあった、どれかの処理が実行される
Stateパターンクラス定義
public static enum State { A { public void doSomething() { //処理1 } }, B { public void doSomething() { //処理2 } }, UNKNOWN { public void doSomething() { //処理3 } }; public abstract void doSomething(); }
Stateパターン利点1. 保守しやすい
状態が今後増えたとしても、Stateパターンクラスのインスタンスを追加するだけで対応可能。
ロジック部の変更は一切不要。
public static enum State { A { public void doSomething() { //処理1 } }, B { public void doSomething() { //処理2 } }, C { //追加した分岐条件 public void doSomething() { //処理 } }, UNKNOWN { public void doSomething() { //処理3 } }; public abstract void doSomething(); }
Stateパターン利点2. 各状態における処理の整合性が取れる
もし、ある状態における2つの処理が対応しなければならない場合があっても整合性がとりやすい。
switch 構文は、ある状態における処理(case 文)が欠落してもコンパイルエラーにならず動作する。
これらを起こさないために実装をクラス構造で強制することができる。
State value = getValue(); // State型を返すようにする。 value.doSomething(); //ここに何か処理があっても良い value.tearDown(); // 後片付け処理
public static enum State { A { public void doSomething() { //処理1 } public void tearDown() { // 状態Aのときの後片付け } }, B { public void doSomething() { //処理2 } public void tearDown() { // 状態Bのときの後片付け } }, UNKNOWN { public void doSomething() { //処理3 } public void tearDown() { // 状態UNKNOWNの後片付け } }; public abstract void doSomething(); public abstract void tearDown(); }
例えば、もし以下の対応関係が取れていなかったとしよう。
- ファイルからデータ取得 → ファイルストリームを閉じる
- データベースからデータ取得 → コネクションのクローズ
これらが対応していなくとも、おそらくそのプログラムは動作するが、いつか問題が発生するだろう。
それは非常に再現が難しく、原因も特定が難しい。さらに、このコード外で発生するかもしれないし、
プログラムの規模が大きくなるにつれて、その原因を解決することは難しくなるだろう。
Stateパターン利点3. コード管理がしやすい
- 状態における処理が一か所にまとまっているため、記述ミスが起こりにくい。
- 複数人の開発においては、ロジック部分とその詳細(処理)を分けて作成できる。
- abstract 宣言により各動作実装が保証されているため、実装の欠落があり得ない。
Stateパターンと比較した switch 構文の欠点
- 状態による分岐処理を複数箇所に記述すると、その都度全状態の case 文を必要とする
- 対応する処理が実装されなかった場合、整合性の取れない動作をする可能性がある。
さらに、これらはコンパイルエラーではなく、実行時例外として別の処理で現れる可能性がある。 - コードが読み辛くなりやすい
結論
- switch 構文は、複雑な処理は記述するにはあまり向いていない。
- 危険なフォールスルーを記述した場合、可読性を大幅に低下させ、バグの温床となりえる。
- オブジェクト型を switch 構文の分岐条件に使う場合は、その変数が null でないことを保証すること。
- もし、現在 switch 構文を使用している箇所が、今後以下のようになるならStateパターン適用を考慮する。
- 状態が増える
- 状態による分岐を複数箇所に記述する
- 各状態に対する整合性のあった処理が必要
参考図書
リファクタリング―プログラムの体質改善テクニック (Object Technology Series)
- 作者: マーチンファウラー,Martin Fowler,児玉公信,平澤章,友野晶夫,梅沢真史
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000/05
- メディア: 単行本
- 購入: 94人 クリック: 3,091回
- この商品を含むブログ (312件) を見る
EFFECTIVE JAVA 第2版 (The Java Series)
- 作者: Joshua Bloch,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (10件) を見る
プロパティファイルの使用方法
概要
プログラミングでは、処理内容とデータをファイル的に分断したい場合がある。
例えば、プログラム中にデータを書き込んでいた場合。
- データの修正漏れや記述ミスにより、特定箇所のみ動かなくなるバグの原因となり得る。
- ステージング環境と本番環境を切り替えるとき、プログラムに手を入れる必要がある。
- 開発してる人に重要な情報(パスワードなど)を公開してしまう。
このような事態を防ぐには、それらを別ファイルに用意し、
プログラムはそのデータを参照するような構造を用意する必要がある。
これらの実装方法について記述する。
事前準備
データを保存するファイルの作成
Javaでは、***.properties (***には名前) ファイルで作成する。
.propertitesファイルの文字コードは ISO 8859_1 なので、
日本語を使用する場合は、エディタをダウンロードした方が望ましい。
私が個人的に使ってるのは、プロパティ・エディター。
インストール手順は以下の通り。(2015/12/27現在)
以下はウィンドウの指示に従ってください。
なお、インストール後 .properties ファイルをプロパティ・エディターで開けない場合、
[ウィンドウ(W)]->[設定(P)] で .properties ファイルに対するエディタの関連付けを設定します。
詳細説明
実装は至ってシンプルで、.propertiesファイルとそれを扱う専用クラスを1つ作成すればOK。
なお、.propertiesファイルはパスが通っているところに設置してください。
プロパティファイル(conf.properties)
# メール題名のつもり ←コメント subject = サンプル # メール本文のつもり body = ああああ
プロパティファイルを扱うクラス(Property.java)
import java.util.ResourceBundle; public class Property { // conf.properties データ private ResourceBundle bundle = ResourceBundle.getBundle("conf"); // キーに対応する値を取得する。 public String getString(String key) { return bundle.getString(key); } // キーに対応する数値を取得する。 public int getInt(String key) { return Integer.parseInt(bundle.getString(key)); } }
プロパティファイルから値を使う場合。
Property property = new Property(); System.out.println("題名 : " + property.getString("subject")); System.out.println("本文 : " + property.getString("body"));
出力結果
題名 : サンプル 本文 : ああああ
static メソッドによるインスタンス生成
概要
static メソッドとは、オブジェクト指向言語において、インスタンスを生成せずとも
処理を実行できるクラスに関連づけられたメソッドである。
使用例としては、便利な機能を提供する Util クラスを作成する場合に用いられ、
Java ライブラリでは、java.lang.Math、java.util.Collections クラスなどがそれにあたる。
これ以外にも、インスタンス生成をコントロールする場合、実装の内部情報を隠す際にも用いられる。
この場合についての説明を記述する。
詳細説明
比較用クラス
トランプのクラスを例として説明する。まず、次のCardクラスを2通りの方法で作成する。
一般的な構造をしたCardクラス
// カードクラス public class Card { // スーツ public String suit; // 番号 A->1, 2->2, ..., 10->10, J->11, Q->12, K->13とする public int num; // コンストラクタ public Card(String suit, int num) { this.suit = suit; this.num = num; } }
クライアントがCardクラスを利用する場合のコード記述例。
Card clobAce = new Card("clob", 1);
staticメソッドでCardクラスを取得するよう変更
// カードクラス public class Card { // スーツ private String suit; // 番号 A->1, 2->2, ..., 10->10, J->11, Q->12, K->13とする private int num; // ここを変更! プライベートコンストラクタ private Card(String suit, int num) { this.suit = suit; this.num = num; } // ここを追加! インスタンス生成用メソッド public static Card getCard(String suit, int num) { return new Card(suit, num); } }
クライアントでは次の呼び出し方法となる。
Card clobAce = Card.getCard("clob", 1);
static メソッド利点1. インスタンスを毎回作成しなくて良い
もし作成するインスタンスが次のようだったとしよう。
- 作成にコスト(生成時間/メモリ)がかかる
- 同じインスタンスを多数回作成
- 一度作成すればそれらを使い回せる
このような場合、既存インスタンスを使い回したいと思うだろう。
しかし、コンストラクタは必ずインスタンスを作成してしまうため実現できない。
staticメソッドであれば、メソッド内でその制御を行うことにより実現できる。
// インスタンスをプールする private static Map<String, Card> pool = new HashMap<String, Card>(); // インスタンス生成用関数 public static Card getCard(String suit, int num) { String id = suit + num; if (!pool.containsKey(id)) { pool.put(id, new Card(suit, num)); } return pool.get(id); }
これによって、以下のパフォーマンス改善が見込まれる。
- メモリ領域の節約
- 処理速度向上(インスタンス作成回数減少/ガーベジコレクション減少)
static メソッド利点2. 適切な名前が使える
このCardクラスは、修正されることになった。それは ジョーカーの追加だ。
では、staticメソッドを用いない場合のジョーカーを作成するとしよう。
今回は suitが null でかつ num が 0 ならジョーカーだという仕様にしたとする。
こんな仕様を理解できるのは、説明書きを読むか、実際のコードを見ないと分からない。
しかし、クライアントはジョーカーを扱う場合、以下のように書かねばならない。
// これが ジョーカーだ! ← 分かるかー!! Card joker = new Card(null, 0);
これをstaticメソッドを使って書き直す場合。Cardクラス定義に以下を追加、
// ジョーカー作成用メソッド public static Card getJoker() { return new Card(null, 0); }
クライアントは次のように変更する。
// これが ジョーカーだ!
Card joker = Card.getJoker();
この実装により、クライアントのコードは非常に分かりやすくなる。
適切なメソッド名が使えるのは、staticメソッドを使ううえで重要な利点だ。
static メソッド利点3. インスタンスの内部情報を隠す
さきほどのコードでは、ジョーカーの内部情報(suitとnum)をクライアントが直接記述している。
今後、ジョーカーの仕様を変更すれば、これらを利用しているクライアントのプログラムも変更しなければならない。
つまり、クライアントに影響がでないようにしたいなら、今後その仕様を変更しないになる。
それは今後修正されるであろう、パフォーマンスの改善もできなくなることを意味する。
それに比べ、static メソッドによる実装は内部情報を隠しているため、
今後ジョーカーの内部情報を変更しても、クライアントのプログラムには影響を及ぼさない。
static メソッド利点4. 生成するインスタンスを変更
絵柄カード(J, Q, K)は、他のカードとは異なる性質を有する場合がある。
例えば、ブラックジャックでは、他の数カードと異なり、Aは1か11、J,Q,K は10になる。
カードによって挙動が異なる場合は、Cardクラスを継承した
FaceCardクラス(コードでは省略)を新たに作成すべきだろう。
そして、static メソッドを用いた場合は、CardクラスだけではなくFaceCardクラスのインスタンスを返しても良い。
// インスタンス生成用メソッド public static Card getCard(String suit, int num) { if (10 < num && num < 14) { return new FaceCard(suit, num); } return new Card(suit, num); }
このようなことは、コンストラクタを用いたインスタンスの生成では不可能だ。
なぜなら、コンストラクタは実装クラスが何かを公開しているから。
これがstaticメソッドを用いた場合、クライアントは、Cardクラスのファミリーの何かが返ってくると認識する。
つまり、ファミリー(Cardクラスを継承している)なら、今後何を返されても構わないのだ。
結論
プログラムは設計段階では、今後変更されるかもしれない箇所が予想できる場合がある。
それは、機能追加やパフォーマンス/セキュリティ上の問題解決かもしれないが、
いずれにせよ、それを改善するための方法は、大体は以下を行う事である。
これらの変更をクライアントに今後意識させたくなければ、
static メソッドを用いたインスタンス生成を行うことを考慮した方が良い。
そうすることで、今後の追加やメンテナンスに対して多大なコストをかけずに変更できる。
あとがき
基本的に下記書物の最初の項に具体例を入れてまとめただけです(^^;
参考図書
EFFECTIVE JAVA 第2版 (The Java Series)
- 作者: Joshua Bloch,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (10件) を見る
Facade - GoF デザインパターン
概要
Facade(ファサードと読むらしい)パターンとは、GoF (Gang of Four)デザインパターンの一つであり、
このパターンが意図することは、サブシステムの簡素なインタフェース提供である。
それ故、Facadeパターン はそれ単体では、新しい機能を提供しない。
複雑なシステムは、簡単なことでもどう実現すればいいか分かりにくくなる。だから簡素化したインタフェースを提供する。
ただし、簡素化することによって機能を追加しやすくはできる。
詳細説明
ユーザは How ではなく What が大事
「JPEGなどに用いられる不可逆な画像圧縮は、離散コサイン変換で色情報を縦横の
周波数成分に変換し、周波数成分の小さい値を省くことによって実現されている」
上記一文は画像圧縮の実現方法だ。大体の人はこれら画像圧縮の詳細な実現方法は
意識していないにも関わらず、これらを日々利用している。
この他にも、HTTP や SSL/TLS などをユーザは日頃実現方法を意識することは
ないにも関わらず、ユーザたちはそれらを用いて、自身のしたいことを平然と行っている。
(ネットでの買い物や、電車の乗換案内、ツイートしたり)
これは使用する機器において、特定機能のツールとして提供されているからだ。
これにより、ユーザは「何ができるか(What)」だけ興味を持ち、
その実現方法(How)は意識しないで済む。
また、使用するシステム自体はとても複雑になったとしても、それらのサブセットを
機能として提供しているからこそ、ユーザはあたかも自分の手足のように動かせる。
このため、ユーザはそれらを使った「自身の仕事をどうするか」だけに注力できる。
複雑なシステムから、意味のある処理の塊を機能として提供することで、ユーザはその詳細を
意識しないで済む。これがまさに Facade パターンの意図することになる。
危険なシステム
あるプログラマが、現在稼働中システムの修正を上司から命じられた。
ある機能にバグがあり、原因処理の切り分けまではほぼ見当がついている。
そのコードを眺めてみると、自分の全く知らないライブラリが数多のクラスで使用されており、
処理の粒度が細かいため、全体の流れが何をしているか分からない。
慎重になったプログラマは、インターネットで調査する。
文献はあるにはあったが、全てが外国語。その中にライブラリのドキュメントを見つけた。
それらはやたら大きなドキュメントだったが、幸い、問題の箇所は検討がついている。
プログラマは四苦八苦しながらも日本語訳をして解決を試みる。
─そして、努力甲斐があり、最後に素晴らしい回答にたどり着いた。
そのライブラリのライフサイクルは既に終了しており、
「現在稼働中の環境での動作保証はされない」だった。
このため、ライブラリを取っ替えようかと考えたが、多数のクラスに
処理が記述されているため変更による影響の度合いは計り知れない。
また、その処理を取り替えることができたとして、正しい動作を確認するには、
全てのクラスをテストする必要があり、それはどう考えても間に合わない。
結局、プログラマは、現状で問題のある部分だけを部分的に修復をし、
現在のシステムはもはや保守できるものではないことを上司に報告した。
この話題から得られる教訓
さて、この話題には保守運用の視点から見ると、問題点が数多くある。
ライブラリの機能を利用する人への配慮がない
もし、ライブラリの機能をあるコードが直接1ステップでも利用したのであれば、
このコードに関与した全員がライブラリに対する知識を持たねばならない。
まず、これが非効率だと思わないだろうか?
また、ライブラリを多数用いた複雑なシステムは見通しが良くないため、様々なリスクをはらんでしまう。
- 解析・影響判定に時間を費やす
- 修正時におもわぬ影響を発見、大きな手戻り
もし、Facade パターンを利用すれば、ライブラリの機能は間接的に利用されるため、
ライブラリによる影響をあるコード(Facade クラス)だけに閉じ込めることができる。
また、Facade パターンを用いることで、追加の利点が生じる。
Facade が提供する機能を利用することで、利用側はこの機能は何を(What)するかだけを意識することになる。
つまり、詳細な実装内容(How)が利用者側に現れないため、コードが読みやすくなる。
ライブラリ変更に対する配慮がない
ライブラリを利用する側は、今後ライブラリに追加して欲しい機能が出てくるかもしれない。
しかし、外部が開発・提供するライブラリは、外部の都合に影響されやすく、その機能を今後追加しないかもしれない。
もっと悪いときには、開発もしくは保守自体がストップすることもある。
古くなったライブラリはセキュリティ的に危険なこともある。また、後々それらが判明することもある。
このような場合を考慮して、ライブラリを取り替える準備をする必要があるはずだ。
Facade パターンを利用すれば、ライブラリを適用している箇所をブラックボックス化できるため、
ライブラリの取っ替えが容易にでき、その変更による影響も特定範囲内になる。
Facade パターン適用例
ライブラリの簡素化
テンプレートによる処理とデザインの切り分け - Status Code 303 - See Other
Velocity ライブラリから一部の機能をラッピングして作成しているため、
OutputBuilderクラスのユーザは、Velocityライブラリについて知らなくても良い。
SSHJ による暗号化通信 - Status Code 303 - See Other
SSHJ のFacade クラス。これらを用いることで、ユーザは、SSH を利用する際、
接続→認証→コマンド実行(ファイルダウンロード/アップロード) を行う手順だけを意識すれば良い。
また、もし SSHJ を今後取り替えたい場合は、SSHClientFacade クラス内を実装し直すだけで良い。
(他の例は、あとで適宜更新)
Facade パターン導入効果
既存コードの積極的再利用
Facadeクラスがライブラリのまとめ役の役割なので、裏側では多数のライブラリが
ひしめきあっていたとしても クライアントがそれを気にする必要がない。
つまり、裏側では、より優れた機能があったならライブラリを取り替えても構わない。
可読性向上
Facade クラスは既存機能を単純化しただけなので、そのメソッド名は「何をする」を示すはず。
もし、プログラムが抽象化された処理で記述できるようになれば、プログラムは格段に読みやすくなる。
修正コストの削減
全体的に保守にかかる費用が削減される。各プロセス別に記述すると。
- コード解析
- 何をしているか分かりやすくなるため早くなる
- 修正に伴う影響判定
- 処理がまとまっているため影響は小さくなる
- コード修正
- メソッド内にしか影響がないため容易になる
- テスト
- 特定箇所に絞ってテストできるため容易になる
依存関係の簡素化
ある処理の塊を必要なクラスに直接記述することは、その処理の影響をそのクラスは受け入れることを意味する。
つまり、その処理自体にバグがあったなら、全てのクラスを修正する必要がある。
もし、Facade クラスを作成して作成すれば、その処理の影響範囲はその中だけに納まる。
それは、特定機能がバグの原因が突き止めやすく、テストもそのメソッドだけ行えばいいことになる。
結論
今回の Facadeクラスでは、クラスの関係が以下のようになる。
- (適用前) クライアント───────<< use >>──────→ サブシステム(ライブラリ)
- (適用後) クライアント─<< use >>→ Facadeクラス ─<< use >>→サブシステム(ライブラリ)
プログラムの保守性を向上するには、関係クラス間に中間層(上では Facade クラス)を設け利用する考えがある。
これによって、クライアントが利用する機能を中間層でコントロールできるからだ。
注意点として、やりすぎは禁物である。この操作の結果、クラスは元の解決策より増加する。
クラス数が大きくなりだすと、どこに何の機能があるか分かりにくくなり、保守もしにくくなる。
あくまでバランスが大事だということ。
もし、上記に挙げた効果が現在のシステムに非常に良い効果をもたらすと期待できるなら
利用すべきであり、そうでなければやってはいけない。
そして、厄介なことに、この明確な基準はおそらくプログラマの力量である。
参考文献
デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)
- 作者: アラン・シャロウェイ,ジェームズ・R・トロット,村上雅章
- 出版社/メーカー: ピアソン・エデュケーション
- 発売日: 2005/09/16
- メディア: 大型本
- 購入: 51人 クリック: 615回
- この商品を含むブログ (125件) を見る
更新履歴
2016/01/21 具体例を一部導入して公開。
2016/02/06 例の追加。一部文言修正。