Edited at

Decorator Pattern

More than 1 year has passed since last update.


自己紹介


  • 氏名: 政倉 智 (まさくら とも)

  • 所属: codeArts 株式会社

  • 所属: html5j 鹿児島

  • 趣味: バイク



アジェンダ

今日は、デザインパターンの話ってより

デザインパターンを使うと

こういうことができるよということを

覚えていただければ嬉しいです

デザインパターンの形とかより

メリットを理解していきましょう



サンプルコード


GitHub のリポジトリを検索するコンソールアプリ

https://github.com/masakura/decorator-sample


動かしてみる

$ ./gradlew run -q

GitHub Repository の検索キーワードを入力してください:
aratana
mwtestlab/aratana => https://github.com/mwtestlab/aratana
yamakei7323/aratanaIntern => https://github.com/yamakei7323/aratanaIntern
tao1027/aratana-test => https://github.com/tao1027/aratana-test
High-Hill/aratana-intern => https://github.com/High-Hill/aratana-intern
takashi1029/aratana_intern => https://github.com/takashi1029/aratana_intern
papuaaaaaaa/yakitori => https://github.com/papuaaaaaaa/yakitori
AratanaLab/aratanalab.github.com => https://github.com/AratanaLab/aratanalab.github.com
abehazuki/knowlege => https://github.com/abehazuki/knowlege
GitHub Repository の検索キーワードを入力してください:


こんな感じ


処理の流れはこんな感じ


GitHub API の呼び出しはこんな感じ

public class GitHubApi {

private final WebTarget target;

public GitHubApi() {
Client client = ClientBuilder.newClient();
target = client.target("https://api.github.com/search/repositories");
}

public RepositoriesResult searchRepositories(String keyword) {
return target
.queryParam("q", keyword)
.request(MediaType.APPLICATION_JSON_TYPE)
.get(RepositoriesResult.class);
}
}

GitHub API を呼び出して結果を返してるだけ!


入力を受け付けてるのはこんな感じ

public class Ui {

private final GitHubApi api;

public Ui() {
api = new GitHubApi();
}

public void searchRepositories() {
while (true) {
String input = getInput();

if (Objects.equals(input, "")) break;

RepositoriesResult result = api.searchRepositories(input);

display(result);
}
}

private String getInput() {
InputStreamReader is = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(is);

System.out.println("GitHub Repository の検索キーワードを入力してください: ");

try {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

private void display(RepositoriesResult result) {
for (Repository repo : result.getItems()) {
System.out.println(repo.getFull_name() + " => " + repo.getHtml_url());
}
}
}



キャッシュを組み込む


本題

ちょっと遅いので、キャッシュしたくなりました!

皆さんどうします?


ここ、いじりたくなりませんか?

public class GitHubApi {

private final WebTarget target;

public GitHubApi() {
Client client = ClientBuilder.newClient();
target = client.target("https://api.github.com/search/repositories");
}

public RepositoriesResult searchRepositories(String keyword) {
// ここをいじりたくなりませんか?
return target
.queryParam("q", keyword)
.request(MediaType.APPLICATION_JSON_TYPE)
.get(RepositoriesResult.class);
}
}


個人的におすすめしません!

こういった修正を何度も繰り返すと混沌としてきます


ちょっとずつ解説します


GitHubApi クラスを修正すると...


Ui クラスを修正すると...


コピペすると...


どれがいいんだろうね...

将来どうなるかを考えてもっとも適切なものを...

選ばずに


なるべく書き換えない方向で!


こうじゃなくて!


こうでもなくて!


こうです!


こんな感じ


ただ、Ui クラスに影響が出る



一行の修正だけで済む

public class Ui {

private final GitHubApi api;

public Ui() {
// api = new GitHubApi();
api = new CacheGitHubApi(new GitHubApi());
}

// ...snip
}



abstract クラスを使ってインターフェイスと実装を分離

するか


interface を使ってインターフェイスと実装を分離


abstract class と interface どっちがいい?


  • なるべく interface で!

  • ただ、interface だと問題が出ることがある


    • Java は interface のデフォルト実装で解決

    • .NET Framework は拡張メソッドで解決




ハンズオンではこのあたりまでやります


メリット


  • 使う側でぱちぱちできる

  • すぐに元に戻せる

  • 単体テストがやりやすい

  • 後回しにできる


使う側でぱちぱちできる

// キャッシュなし

api = new RawGitHubApi();

// キャッシュ付き
api = new CacheGitHubApi(new RawGitHubApi());

// API コールしたときにログを出力する
api = new CacheGitHubApi(new LogGitHubApi(new RawGitHubApi()));


すぐに元に戻せる

CacheGitHubApi クラスがバグってもそこまで怖くない!

// api = new RawGitHubApi();

api = new CacheGitHubApi(new RawGitHubApi());


単体テストがやりやすい

手で、キャッシュされたかどうかのテストを行うのはすごく困難

MockGitHubApi mock = new MockGitHubApi(); // テスト用モック

GitHubApi target = new CacheGitHubApi(mock);

// キャッシュがないので API がコールされた
target.searchRepositories("abc");
assertEquals(mock.callCount(), 1);

// キャッシュヒットしたので API がコールされない
target.searchRepositories("abc");
assertEquals(mock.callCount(), 1);

// 別のキーワードで検索はキャッシュヒットしない
target.searchRepositories("def");
assertEquals(mock.callCount(), 2);


後回しにできる


  • API をどうキャッシュするかはかなり難しい問題

  • 今回、キャッシュなしからキャッシュありに書き換えた

  • ということは、プログラムを書く前に決める必要がなくなる

  • よりアプリの価値に近いところに集中しておいて、後から調整できる


decorator パターンの使い道


  • キャッシュ・バッファリング

  • トランザクション

  • ログ


    • 例: 監査のために、一時期のみメールを飛ばす

    • 設計時に織り込めなかったものでも対処しやすい



  • その場しのぎ


    • 例: 特定パターンで計算が狂う

    • 根本的なバグを洗い出す前のその場しのぎに



  • 他にもたくさんありそうですね!



AOP (おまけ)


ログ・キャッシュ・トランザクションは、似たようなコードをたくさん生み出す

// こんな形のコードが大量に生まれる

log.start("foo.method1()");

try {
foo.method1();
} catch (Exception ex) {
log.exception(ex);
throw ex;
}

log.end("foo.method1()");


これは decorator パターンを使っても解決できない

class LogFoo extends Foo {}

class LogBar extends Bar {}

class LogBazz extends Bazz {}


AOP を使うと、decorator を自動で生成してくれる

大きく分けて二種類ある


  • 実行時に動的に生成するタイプ

  • コンパイル前にクラスを生成するタイプ



追加の課題

ハンズオンを受けるまでもないなって人は課題にチャレンジ


このハンズオンのアプリを三社 (A, B, C) に販売することになりました。でも、ちょっとずつ要望が異なります。


  • A: そのままでいいよ!

  • B: キャッシュは昔痛い目にあった。ネットワークを増強するから、キャッシュしないでくれ。

  • C: キャッシュアルゴリズムは特許を持ってるから、こちら側でコード書くよ。あ、コードは開示できないからね!



まとめ


  • decorator パターンは利用する側がぱちぱちできる

  • うまく使うと、いくつかの問題の解決を後回しにできる



デザインパターンをうまく使えたときは楽しい!