Android: Picassoで使われているデザインパターン

Picassoで使われているデザインパターンを紹介する記事です.

Singletonパターン

Singletonパターンは, インスタンスの生成を1つに制限するパターンになります.

https://github.com/square/picasso/blob/d35058278cff55874d133cfd63286dd0f1ff0d50/picasso/src/main/java/com/squareup/picasso/Picasso.java#L672

public static Picasso with(Context context) {
  if (singleton == null) {
    synchronized (Picasso.class) {
      if (singleton == null) {
        singleton = new Builder(context).build();
      }
    }
  }
  return singleton;
}

Picasso#withは, すでにPicassoのインスタンス singleton が生成されていればそれを返し, 生成されていなければ, インスタンスを生成して返します.

このパターンのメリットは, インスタンスを多くても1つしか作らないのでメモリ的に有利な点です(使い回せる) しかし, singletonなインスタンスは, 複数のクラスから使われる可能性があるので, スレッドセーフである必要があります.

スレッドセーフにするためには, 全てのfieldの値をfinalにする. 排他的制御を入れるなどの方法があります.

Builderパターン

Builderパターンはインスタンス生成時に多数のパラメータが必要なときに便利なパターンです.

https://github.com/square/picasso/blob/d35058278cff55874d133cfd63286dd0f1ff0d50/picasso/src/main/java/com/squareup/picasso/Picasso.java#L702

public static class Builder {
  private final Context context;
  private Downloader downloader;
  private ExecutorService service;
  private Cache cache;
  private Listener listener;
  private RequestTransformer transformer;
  private List<RequestHandler> requestHandlers;
  private Bitmap.Config defaultBitmapConfig;

  private boolean indicatorsEnabled;
  private boolean loggingEnabled;

  public Builder(Context context) {
    if (context == null) {
      throw new IllegalArgumentException("Context must not be null.");
    }
    this.context = context.getApplicationContext();
  }

  ...
  ...

  public Picasso build() {
    Context context = this.context;

    if (downloader == null) {
      downloader = Utils.createDefaultDownloader(context);
    }
    if (cache == null) {
      cache = new LruCache(context);
    }
    if (service == null) {
      service = new PicassoExecutorService();
    }
    if (transformer == null) {
      transformer = RequestTransformer.IDENTITY;
    }

    Stats stats = new Stats(cache);

    Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

    return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
        defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
  }

必ず必要なパラメータContextはコンストラクタ引数として渡し, オプション的なパラメータは必要に応じてセットします. 最後に, buildメソッドをコールして, 対象のインスタンスを取得します.

new Builder(context) // 必ず必要なパラメータ
    .debugging(true) // debuggingをtrueに
    .memoryCache(memoryCacheInstance) // 専用のmemoryCacheを使う
    .build(); // パラメータに異常がなければインスタンスを返す

Builderパターンを使うことで, コンストラクタが指数的に増えてしまう問題を防ぐことが出来ます. また, Hoge(int, int, int)の時, 与えるintの順番を間違える可能性が高いですが, Builderパターンだと名前付きメソッドで値を指定出来るので, よりリーダブルであると思います(主観).

static factoryパターン

static factoryパターンは, コンストラクタの代わりに, クラスのインスタンスを返すstatic methodを使用するパターンです.

public static Picasso with(Context context) {
  if (singleton == null) {
    synchronized (Picasso.class) {
      if (singleton == null) {
        singleton = new Builder(context).build();
      }
    }
  }
  return singleton;
}

Picasso#with(Context)は, Picassoのインスタンスを返します. static factoryメソッドを使うことで, コンストラクタ以上の柔軟性を提供することが出来ます. 上の例で言うと, Picasso#withは, シングルトンパターンにより, 毎回インスタンスを生成する必要がありません. コンストラクタを使う場合は, 毎回インスタンスを生成する必要があります. さらに, static factoryは自分自身だけでなく, サブクラス, インターフェースの実装を返すことも可能です. 上の例で言うとPicasso#withはPicassoのサブクラスを返しても問題なく動作します(もちろんサブクラスにバグがなければ).

早期リターンパターン(early return pattern)

早期リターンパターンは, メソッドの先頭 で, 何もせずにメソッドを終了するか, 例外をスローするパターンです.

https://github.com/square/picasso/blob/d35058278cff55874d133cfd63286dd0f1ff0d50/picasso/src/main/java/com/squareup/picasso/RequestCreator.java#L519

public void into(Target target) {
  long started = System.nanoTime();

  // こっから例外などの判定
  checkMain();

  if (target == null) {
    throw new IllegalArgumentException("Target must not be null.");
  }
  if (deferred) {
    throw new IllegalStateException("Fit cannot be used with a Target.");
  }

  if (!data.hasImage()) {
    picasso.cancelRequest(target);
    target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);
    return;
  }
  // 例外などの判定修了

  // こっからメインロジック
  Request request = createRequest(started);
  String requestKey = createKey(request);

  if (shouldReadFromMemoryCache(memoryPolicy)) {
    Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
    if (bitmap != null) {
      picasso.cancelRequest(target);
      target.onBitmapLoaded(bitmap, MEMORY);
      return;
    }
  }

  target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);

  Action action =
      new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
          requestKey, tag, errorResId);
  picasso.enqueueAndSubmit(action);
}
  1. 変数targetがnullなら例外をスロー
  2. 変数deferredがtrueなら例外をスロー
  3. data.hasImage()がfalseなら, cancelRequestをコールしてreturn
  4. メインのロジックの実行

メソッドのエラー処理の部分をメソッドの最初に, メインロジックの部分をその後にそれぞれ分割することで, 可読性を上げることが出来ます. アスペクト指向プログラミングに近い考え方だと思います.

アスペクト指向とは, メインロジック以外の副次的なロジック(セキュリティ要件を満たしているか, ログを取るなどなど)を, 宣言的に外部から注入できるプログラミングパラダイムです. 1つのメソッド, ルーチンの中に, 複数の異なるロジックが含まれていると可読性が損なわれるので, その部分を切り出すことが出来ます.

ViewHolderパターン

Android特有のパターンです. ListViewで子要素を切り替えるたびに毎回View#findViewByIdを実行するのはコストが高いので, Cacheしておくパターンです.(Picasso本体ではなく, exampleフォルダのコード例になります)

https://github.com/square/picasso/blob/ceafe59cbecbc1e1a75cc6a14d028ebba3145cbe/picasso-sample/src/main/java/com/example/picasso/SampleListDetailAdapter.java#L66

@Override public View getView(int position, View view, ViewGroup parent) {
  ViewHolder holder;
  if (view == null) {
    view = LayoutInflater.from(context).inflate(R.layout.sample_list_detail_item, parent, false);
    holder = new ViewHolder();
    holder.image = (ImageView) view.findViewById(R.id.photo);
    holder.text = (TextView) view.findViewById(R.id.url);
    view.setTag(holder);
  } else {
    holder = (ViewHolder) view.getTag();
  }

  ...
}

static class ViewHolder {
  ImageView image;
  TextView text;
}

BaseAdapter#getViewで, Viewを生成するときに処理に必要な情報をViewHolderに保存しておきます. こうすることで, 次回以降のコストを減らすことが出来ます.

Observerパターン

非同期な処理が完了, 状態が変化したことを, クライアント(主に呼び出し元のインスタンス)に通知をする時に使われるパターンです. 非常にポピュラーなパターンです.

https://github.com/square/picasso/blob/d35058278cff55874d133cfd63286dd0f1ff0d50/picasso/src/main/java/com/squareup/picasso/RequestCreator.java#L647

public void into(ImageView target, Callback callback) {
  long started = System.nanoTime();
  checkMain();

  ...

  if (shouldReadFromMemoryCache(memoryPolicy)) {
    Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
    if (bitmap != null) {
      picasso.cancelRequest(target);
      setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
      if (picasso.loggingEnabled) {
        log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
      }
      if (callback != null) {
        callback.onSuccess();
      }
      return;
    }
  }
}

into(ImageView, Callback)の, Callbackの部分がObserverパターンのポイントになります. intoメソッドは非同期な処理のため, 結果が成功したかを返り値として受け取ることが出来ません. そこで, 非同期処理が終わったら, 引数で渡したcallbackをコールするようにすることで結果を受け取ることが出来ます.

Written by
あんどろいどでぃべろっぱぁー🍎