概要
Java8から導入された関数型インターフェイスや関数合成を使ってDecoratorパターンの実装をリファクタリングしてみます。
Decoratorパターン
Decoratorパターンを利用すると既存のオブジェクトに新しい機能や振る舞いを動的に追加することができます。Javaでよく引き合いに出されるのはInputStream/OutputStreamやReader/Writerなどのクラスです。
例えばファイル読み込み時に以下のようなコードを書くと
InputStream is0 = Files.newInputStream(pathIn);
InputStreamReader reader1 = new InputStreamReader(is0, csIn);
BufferedReader reader = new BufferedReader(reader1);
// readerを使った本処理・・・
ファイルの内容をバイト単位で扱うInputStreamに対して文字単位で扱う機能を付加し(InputStreamReader)、更にバッファリング機能を付加する(BufferedReader)ことができます。
上の例ではreaderを使ったファイルデータ処理が続きますが、データ処理の前にデータの整形や補正といった事前処理が必要になることはままあります。このような事前処理もDecoratorパターンでの実装が可能です。以下にその実装例をあげます(PreprocedReaderOld.javaの全文はGitHubにあります)。
/**
* 事前処理の実装例(Decoratorパターン版)
*/
public class PreprocedReaderOld extends PipedReader {
protected Reader in;
public PreprocedReaderOld(Reader in) throws IOException {
this.in = in;
doPreproc();
}
protected void doPreproc() throws IOException {
PipedWriter pipedOut = new PipedWriter();
this.connect(pipedOut);
BufferedReader reader = new BufferedReader(in);
BufferedWriter writer = new BufferedWriter(pipedOut);
try (reader; writer;) {
String line;
int lineNo = 1;
while ((line = reader.readLine()) != null) {
pipedOut.write(preprocess(lineNo, line));
pipedOut.write('\n');
lineNo++;
}
}
}
protected String preprocess(int lineNo, String line) {
return "!事前処理! " + line;
}
@Override
public void close() throws IOException {
in.close();
}
}
この事前処理実行クラスは、他のデコレータ同様以下のようにコンストラクタ呼び出しを追加して適用します。
InputStream is0 = Files.newInputStream(pathIn);
InputStreamReader reader1 = new InputStreamReader(is0, csIn);
Reader reader2 = new PreprocedReaderOld(reader1); // 事前処理を追加
BufferedReader reader = new BufferedReader(reader2);
// readerを使った本処理・・・
別の事前処理が更に必要になった場合、この例では事前処理実行クラスPreprocedReaderOld
を継承しかつpreprocess(int lineNo, String line)
メソッドをオーバーライドした事前処理実行クラスを新たに定義し、コンストラクタ呼び出しを追加していくことになります。
InputStream is0 = Files.newInputStream(pathIn);
InputStreamReader reader1 = new InputStreamReader(is0, csIn);
Reader reader2 = new PreprocedReaderOld(reader1); // 事前処理を追加
Reader reader3 = new PreprocedReaderOld2(reader2); // 事前処理その2を追加
Reader reader4 = new PreprocedReaderOld3(reader3); // 事前処理その3を追加
BufferedReader reader = new BufferedReader(reader4);
// readerを使った本処理・・・
以上、ファイルに対する事前処理のDecoratorパターンによる実装例でした。
関数型インターフェイスを使って書き直す
ここからは上の実装例を関数型インターフェイスを利用して書き直してみます。
まず、以下のような関数型インターフェイスPreprocess
を用意します。
/**
* 事前処理インターフェイス
*/
@FunctionalInterface
public static interface Preprocess {
/**
* 事前処理
*
* @param lineNo 対象行番号
* @param line 対象行文字列
* @return 事前処理結果
*/
public String apply(int lineNo, String line);
// ----- ----- //
/**
* 事前処理の合成
*
* @param next 合成する事前処理
* @return 合成後の事前処理
*/
default Preprocess compose(Preprocess next) {
return (int n, String v) -> next.apply(n, this.apply(n, v));
}
/**
* 単位元
*
* @return
*/
public static Preprocess identity() {
return (lineNo, line) -> line;
}
/**
* 複数の事前処理を合成するユーティリティ関数
*
* @param preprocs 合成対象の事前処理
* @return
*/
static Preprocess compose(final Preprocess... preprocs) {
return Stream.of(preprocs).reduce((preproc, next) -> preproc.compose(next)).orElse(identity());
}
}
事前処理実行クラスは以下のように実装します。前半で例示したDecoratorパターン版のPreprocedReaderOld
との違いは事前処理の扱いだけです。
public class PreprocedReader extends PipedReader {
private final Reader in;
/**
* (合成された)事前処理。
* 初期値は単位元。
*/
private Preprocess preprocs = Preprocess.identity();
public PreprocedReader(Reader in, Preprocess...preprocs) throws IOException {
this.in = in;
this.preprocs = Preprocess.compose(preprocs); // 指定された事前処理を合成のうえ保持する
doPreproc();
}
private void doPreproc() throws IOException {
PipedWriter pipedOut = new PipedWriter();
this.connect(pipedOut);
BufferedReader reader = new BufferedReader(in);
BufferedWriter writer = new BufferedWriter(pipedOut);
try (reader; writer;) {
String line;
int lineNo = 1;
while ((line = reader.readLine()) != null) {
pipedOut.write(preprocs.apply(lineNo, line)); // 事前処理の適用
pipedOut.write('\n');
}
}
}
@Override
public void close() throws IOException {
in.close();
}
}
Decoratorパターン版ではメソッドとして実装される事前処理(preprocess(int lineNo, String line)
)を呼び出す形でしたが、書き直し後のPreprocedReader
では関数として実装される事前処理(Preprocess
)をコンストラクタ引数として与え、かつ事前処理が複数ある場合はそれらを1つの事前処理に合成してから適用する形にしています。
個々の事前処理はPreprocess
をimplementsして実装します。以下の例では3種類の事前処理を実装しています。
/**
* 《事前処理》エスケープ処理を行う
*/
private static class PreprocEscape implements Preprocess {
@Override
public String apply(int lineNo, String line) {
return org.apache.commons.text.StringEscapeUtils.escapeJava(line);
}
}
public static final Preprocess ESCAPE = new PreprocEscape(); // 糖衣構文
/**
* 《事前処理》指定された列位置でファイルをトリムする
*/
private static class PreprocTrimCol implements Preprocess {
private final int from;
private final int to;
public PreprocTrimCol(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public String apply(int lineNo, String line) {
final int len = line.length();
if (len < to) {
return line.substring(from);
} else if (to <= from) {
return "";
} else {
return line.substring(from, to);
}
}
}
public static final Preprocess TRIM_COL(int from, int to) { // 糖衣構文
return new PreprocTrimCol(from, to);
}
/**
* 《事前処理》行操作は行わず、行内容を標準出力へ出力する
*/
private static class PreprocDumpStdout implements Preprocess {
@Override
public String apply(int lineNo, String line) {
System.out.println("[DUMP]"+line);
return line;
}
}
public static final Preprocess DUMP_STDOUT = new PreprocDumpStdout(); // 糖衣構文
こうして用意した事前処理は、以下のように適用します。
InputStream is0 = Files.newInputStream(pathIn);
InputStreamReader reader1 = new InputStreamReader(is0, csIn);
Reader reader2 = new PreprocedReader(reader1, TRIM_COL(0, 5), DUMP_STDOUT, ESCAPE); // 事前処理を追加
BufferedReader reader = new BufferedReader(reader2);
// readerを使った本処理・・・
Decoratorパターンでは複数のコンストラクタをネストさせる必要がありましたが、書き直し後の関数版では事前処理をコンストラクタ引数として列挙できます。引数として与えられた事前処理は左から右へ順番に実行されます。
このように複数の機能を動的に追加したいケースで、Decoratorパターンの代わりに上のような関数型インターフェイスを利用するとコードが簡潔になることが多いのでおすすめです。私は内部DSLの定義の際によく利用しています。
Quiz
引数として与えられた事前処理は左から右へ実行されますが、右から左へ実行させるにはPreprocess
のどこを修正すれば良いでしょう?
ソースコード:GitHubにあります