LoginSignup
2
1

More than 5 years have passed since last update.

Decorator Pattern

Last updated at Posted at 2017-04-17
1 / 46

自己紹介

  • 氏名: 政倉 智 (まさくら とも)
  • 所属: codeArts 株式会社
  • 所属: html5j 鹿児島
  • 趣味: バイク

アジェンダ

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

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

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

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

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

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


サンプルコード


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


動かしてみる

$ ./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 パターンは利用する側がぱちぱちできる
  • うまく使うと、いくつかの問題の解決を後回しにできる

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1