Konifar's WIP

親方!空からどらえもんが!

AndroidではBaseActivityはやめた方がいいかもしれない

先日行われたDroidKaigiで『BaseActivityの是非』が少し語られました。 セッションの中では反対派が多かったようです。

この話は以前から活発で、Qiitaやブログ上でも何度か議論されていました。

自分は業務でも個人アプリでもBaseActivityを作っていたんですが やっぱりBaseActivityはやめようかなぁと方針が変わったので、少し思考を整理しておきます。

双方の主張

まずは肯定派と否定派のポイントを整理してみます。

肯定派

  • 共通処理をまとめられるのでDRYになる。
  • 将来共通の変更があった時に対応しやすい。
  • 必ず必要なもののみ実装するようにすれば肥大化は防げる。

否定派

  • is-a関係じゃないのに継承を使うと依存関係がよくわからなくなる。
  • スキルレベルの違う複数人で開発すると、不必要な処理を追加しがち。

ざっくり言うと

肯定派は DRYな実装にしやすいし必要な実装のみに限定すれば肥大化しないしいいよね、という意見で、否定派は そもそもオブジェクト指向的にも継承を使うような場面じゃなくて委譲で解決できそうだし、結局肥大化しそうだからやめとこうぜって意見みたいです。

必ず必要な実装とは何か

肯定派の言う、BaseActivityで必ず必要な実装とはどんなものなのかというと、つまりは共通処理ですよね。そして全Activityにおける共通処理というのは、おそらく ライブラリやUIの初期化とアニメーションだけなのではないかと思います。

UIで言うと、自分が今やっているのはlayout、toolbarの初期化、TransitionのUtilメソッドの定義です。ライブラリはButterKnifeを使っていて、layoutをセットした時に呼ばれるようにしています。

public abstract class BaseActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutResId());
        ButterKnife.inject(this);
    }

    void initBackActionBar(int titleResId) {
        initBackActionBar(getString(titleResId));
    }

    void initBackActionBar(String title) {
        ActionBar bar = getSupportActionBar();
        bar.setDisplayHomeAsUpEnabled(true);
        bar.setDisplayShowHomeEnabled(true);
        bar.setDisplayShowTitleEnabled(false);
        bar.setHomeButtonEnabled(true);

        if (title != null) bar.setTitle(title);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finish();
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onBackPressed() {
        finish();
    }

    abstract int getLayoutResId();

    void overrideSlideEnterTransition() {
        overridePendingTransition(R.anim.activity_slide_start_enter, R.anim.activity_scale_start_exit);
    }

    void overrideSlideExitTransition() {
        overridePendingTransition(R.anim.activity_scale_finish_enter, R.anim.activity_slide_finish_exit);
    }

    void overrideFadeEnterTransition() {
        overridePendingTransition(R.anim.activity_fade_in, 0);
    }

    void overrideFadeExitTransition() {
        overridePendingTransition(0, R.anim.activity_fade_out);
    }

    void overrideNoTransition() {
        overridePendingTransition(0, 0);
    }

}

is-a関係かどうか

一般的に『オブジェクト指向言語において継承を利用すべきなのはis-a関係が成り立つ時』という指針があります。

参考 : Effective Java 16章「継承よりコンポジションを選ぶ」

で、先ほど必ず必要な実装としてあげた、ライブラリ・UIの初期化、アニメーション定義の機能を持つBaseActivityは、ProfileActivityのような子クラスとis-a関係にあるかというと、なんだかしっくりきません。

ライブラリ・UIの初期化、アニメーション定義という3要素から考えると、どちらかというとhas-a関係として捉えて設計した方がよさそうな気がします。

1.「継承」の関係 「継承」は「A is a B」のとき、つまり「AはBである」といえる場合に使用します。 例えば、「人間クラス」と「プログラマークラス」があった場合、「プログラマーは人間である」といえるので「継承」関係にすることが出来ます。これを「Is-a」関係と呼びます。

2.「コンポジション」の関係 「コンポジション」は「A has a B」のとき、つまり「AはBをもっている」といえる場合に使用します。 例えば、「プログラマークラス」と「パソコンクラス」があった場合、「プログラマーはパソコンをもっている」といえるので「コンポジション」関係にすることが出来ます。これを「has-a」関係と呼びます。

委譲で解決してみる

例えば、Animationのところを別クラスで委譲させれば、BaseActivityからAnimation定義の役割は消せます。

public class ActivityTransitionUtils {
    //(略)

    public void overrideSlideEnterTransition(Activity activity) {
        activity.overridePendingTransition(R.anim.activity_slide_start_enter, R.anim.activity_scale_start_exit);
    }

    public void overrideSlideExitTransition(Activity activity) {
        activity.overridePendingTransition(R.anim.activity_scale_finish_enter, R.anim.activity_slide_finish_exit);
    }

    public void overrideFadeEnterTransition(Activity activity) {
        activity.overridePendingTransition(R.anim.activity_fade_in, 0);
    }

    public void overrideFadeExitTransition(Activity activity) {
        activity.overridePendingTransition(0, R.anim.activity_fade_out);
    }

    public void overrideNoTransition(Activity activity) {
        activity.overridePendingTransition(0, 0);
    }
}

toolbarの初期化なんかに関しても同様に別のクラスに委譲できるはずです。

共通化の処理に関しては、継承を使わずに委譲でも解決できるということですね。

そんなに継承がイケてないのか

「委譲でもできるのはわかったけどクラス数増えちゃうし、そもそもそんなに継承イケてないの?」という意見もあると思います。これは難しいなんところですが、自分は2つの点であんまりよくないんじゃないかなぁと思ってます。

1. 親子の結合度が高い

継承はどうしても親子のクラスの結合度が高くなります。子は親のクラスがいなければ存在できないのだから当たり前の話です。そのため、共通化のために継承を使うのはやめておいた方がいいという先人の知恵が存在します。

参考 : 「共通化 → 継承」という誤った考え

とはいえ、そもそもAppCompatActivityなどActivityは継承を利用して開発する前提になっているので、もしかしたらそこまで問題にならないかもしれません。

2. 便利すぎて不要な処理を追加しがち

本命の欠点はこちらです。DroidKaigiやQiitaで指摘されていたのもこの問題です。

参考 : iOSアプリの設計でBaseViewControllerのようなのは作りたくない

自作のBaseActivityというのは便利なので、ちょっと共通の処理が発生した時についつい追加してしまいがちです。 これが本当に全Activityに必要な処理ならいいのですが、経験上そうならないことが多いです。

最悪なのは、BaseActivityに書いた処理の中で一部のActivityのみやらないケースが出てきた場合です。BaseActivityの中で子供のActivityを意識しなければならなくなってしまいます。

例えば、メインのActivityから他のActivityを開いた数をカウントして、10回開いてからメインのActivityに戻った時に何らかの処理をしたいとします。カウントアップの処理はBaseActivityでやると便利そうですが、メインのActivityは開いた数としてカウントしてはいけないので除外する必要があります。

こういうケースはActivityの数にもよりますが、実装者によっては、迷った結果BaseActivityに書いて条件分岐するという実装を選ぶことになってしまうかもしれません。

本当に必要な処理のみならいいのか

「じゃあBaseActivityには本当に必要な処理だけ記述して、あとは修正を加えないようにしたらいいんじゃない?」という意見もありそうです。しかし経験上、これはなかなかうまくいかないんじゃないかなと思います。

なぜかというと 本当に必要な処理の定義と、それを判断する人が必要になるからです。

メンバー個々の判断に委ねるといつか瓦解します。これはメンバーのスキル不足ということではなく、締切とのトレードオフを鑑みた結果、とりあえずBaseActivityに実装して回収されない、ということがあり得るからです。

かといって、誰か判断する人を置くのは難しいし、1人にその役割を委ねると開発スピードも落ちてしまいます。

継承でも委譲でもそんなに手間は変わらない

そもそも、本当に必要な処理のみということであれば、継承でも委譲でもそこまで実装の手間は変わらない気がします。 それなら委譲使った方がいいんじゃないか、という意見です。

Calligraphyを使ってフォントを変更したい場合は、Applicationクラスで初期化することも可能です。

GoogleAnalyticsを使いたい場合は、基本Activityで初期化して、必要があればGAletteなどのAnnotationベースのライブラリを使って手間を減らすのがいいんじゃないかと思います。

結論 : BaseActivityには反対

ということで、個人的には BaseActivityはやめた方がいい、という意見です。

以下理由まとめです。

  • BaseActivityは誘惑が多く実装に悩む。
  • BaseActivityは肥大化して責務が多くなる運命にある。
  • BaseActivityじゃなくても委譲で解決できるし、手間もあまり変わらない。

というか、そもそもBase~という名前がイケてないと思うんですよね。 この名前のせいで、継承是非の判断に使われるis-a関係がわかりにくいことになってるんじゃないかなぁと思ったり。

がーっと思ったことを書きましたが、あくまで現在の自分のWork In Progressな意見なので何か指摘があれば是非お願いします。