LoginSignup
568
576

More than 5 years have passed since last update.

チームとして少ないミスで素早くアプリを継続的・持続的に作り続けるためのメソッド

Last updated at Posted at 2015-04-27

この投稿は DroidKaigi で話そうと思ったけど採択されなかった RejectedKaigi な内容です。

プログラムは、書けば書くほど複雑になります。行数が増え、分岐や繰り返しが増え、メソッドが増え、クラスが増え、パッケージが増え、管理するものは日に日に増えていきます。これらのものを使う側からすると、使うものが増えるということは、それだけ覚えることが増えることになります。勿論、IDE やエディタプラグインによって、そのような労力が極力減らされることもありますが、覚えることが少ないに越したことはありません。

この記事では、IDE やエディタプラグインはひとまず脇に置き、チームでコミュニケーションを取りながらコードを書くという観点で、従来のプログラミングのプラクティスを基に、開発時のミスを少なくし、チームで素早くアプリを作り続けていく方法論を深めていこうと思います。


Agenda

  1. 型を使いこなす
  2. データの流れを見極める
  3. オブジェクトの生死を扱う
  4. 共通する処理を委譲する
  5. まとめ

型を使いこなす

文字列型の引数が増えてきたら型を検討する

文字列はとても便利です。名前も、IDも、その他いろいろなものが文字列で表されます。一方、プログラム上では、文字列の意味に関係なく文字列は文字列なので、どんなものも文字列型として扱うことができます。

これで何が起こるかというと、

UserApiClient.java

public interface UserApiClient {
    void create(String nickname, String familyName, String givenName, String password, String sex, int age);
}

みたいな API を想像してください。
このUserApiClientを使う人は、すべての引数が文字列ですから、何番目にどんな意味の文字列を渡すかを考えながらコードを書くことになります。


UserApiClient client = ClientPool.get(UserApiClient.class);
// 引数の型も意味もOK
client.create("KeithYokoma", "Keishin", "Yokomaku", "hogefuga", "male", 25);
// 引数の型はOKだけど意味はNG
client.create("KeithYokoma", "Keishin", "Yokomaku", "male", "hogefuga", 25);

IDE の力を借りれば、今どの引数を設定しようとしているかが見えるようになりますが、しかし、それはあくまで補助的な機能でしかないので、たとえ意味としては正しくなくとも、型さえ合っていればコンパイルは通ります。ただ、コンパイルは通るのですが、対応する意味とは違う文字列を渡してしまうと、うまく動作しないかもしれません。あるいは、動作はしても期待通りの結果が得られないかもしれません。

この問題は、型安全性として議論されています。
コンパイル時にプログラムの不正に対して警告を発し、修正を促すことで、問題の早期発見へとつなげています。

先ほどのUserApiClientの例では、以下のような引数にできれば理想的です。

UserApiClient.java

public interface UserApiClient {
    void create(User user);
}

非常に単純になりました。引数は一つ、Userを作るのですから、User型を引数に取るようにします。User型以外のオブジェクトは受け付けなくなりますので、変なオブジェクトが渡される心配がなくなります。プログラムの記述と意味がリンクすることで、UserApiClientを使用するプログラマも、メソッドの役割が理解しやすくなります。

では、User型はどのように実装されるでしょうか。

意味のある文字列には型を与える

単に、最初のcreateメソッドをそのままコンストラクタにしていたのでは意味がありません。

User.java

public class User {
    public User(String nickname, String familyName, String givenName, String password, String sex, int age) {}
}

結局、これでは最初の悪夢を再来させるだけです。

このUserのコンストラクタをうまく実装するには、少なくとも2つの方向性があるように見えます。

  1. sexは、取りうる値のパターンが決まっているので列挙型にする
  2. familyNamegivenNameを型にまとめる

まずは1について。性別を示すsexのパターンには、男性、女性があります(genderとは区別しておきましょう)。パターンが有るということは、それらを列挙して定義することができると言えます。文字列にしてしまうと、hoge等のどんな文字列でもsexにすることができてしまいますが、列挙型として型を与えると、常にsexMALEFEMALEnullかになります。nullを許すかどうかは仕様次第かもしれません。

User.java

public class User {
    public User(String nickname, String familyName, String givenName, String password, Sex sex, int age) {}

    public static enum Sex {
        MALE, FEMALE;
    }
}

次に2について。familyNamegivenNameは本名を構成するメンバと見て、本名を示す型を宣言してみます。

User.java

public class User {
    public User(String nickname, RealName realName, String password, Sex sex, int age) {}

    public static enum Sex {
        MALE, FEMALE;
    }

    public static class RealName {
        public RealName(String familyName, String givenName) {}
    }
}

Userのコンストラクタがずいぶんとスッキリしました。

2つの方向性をよりざっくりまとめると、以下のようになるでしょう。

  1. 文字列値にパターンが有る場合は列挙型を導入し、型を与える
  2. 複数の文字列があるデータ構造のメンバをなす場合は、そのデータ構造を示す型を与える

本名の例は極端すぎるかもしれませんが、意味のある単位で型を宣言していくことの重要性は見えたと思います。

さて、このUser、どう見ても簡単に User を一意に見分ける方法が無いように見えます。nickname やその他のプロパティからハッシュをとって計算する、でも出来るかもしれませんが、おそらくそのような実装はせず、専用の ID を与えて一意なキーを発行するのが王道でしょう。
更にいうと、ユーザを作成する際にはpasswordが必須ですが、それが常に必要かと言われると疑問です。例えば、Userのプロフィールを表示するためにわざわざpasswordまで知っておく必要はないはず。つまり、passwordは常に必須ではなく、オプショナルな値であるということです。この意味では、ID なるものも、ユーザの作成リクエストを送る前には ID を割り振る事ができないかもしれませんね(サーバサイドで ID を割り振っている場合)。また、年齢は公開レベルが設定でき、公開レベルの範囲外の人には見えないようにする仕様があるかもしれません。

そうすると、コンストラクタがごちゃごちゃし始めます。

User.java

public class User {
    public User(String nickname, RealName realName, Sex sex) {}
    public User(String nickname, RealName realName, Sex sex, int age) {}
    public User(String nickname, RealName realName, String password, Sex sex) {}
    public User(String nickname, RealName realName, String password, Sex sex, int age) {}
    public User(String id, String nickname, RealName realName, Sex sex, int age) {}
    public User(String id, String nickname, RealName realName, String password, Sex sex, int age) {}

    // 組み合わせを増やしすぎてもうこれ以上パターンを書くのはいやだ
}

うわあ。組み合わせの分だけコンストラクタが増え続けます。しかも、コンストラクタの名前は必ずクラス名になるので、どれがどの用途で使用されるのか意図が理解しづらくなります。

ビルダパターンを活用してオプショナルな値に対応する

常に必須なメンバと、たまに必要ないメンバとをうまく使いこなすには、ビルダパターンを活用します。

まず、Userのコンストラクタのうち、必須パラメータのみを引数に取るものだけ残し、他のパッケージから見えないようにします。次に、ビルダクラスを用意し、ビルダの初期化で必須パラメータを渡します。

あとは、setter メソッドを宣言して要求に応じて使う側が選択的にフィールドの値を設定できるようにしていけば、コンストラクタが混乱を起こすことがなくなります。

User.java

public class User {

    // コンストラクタを他のパッケージで使わせたくないので隠す
    /* package */ User(String nickname, RealName realName, Sex sex) {}

    public static class Builder {
        private final User user;

        public Builder(String nickname, RealName realName, Sex sex) {
            user = new User(nickname, realName, sex, age);
        }

        public Builder setId(String id) {
            user.id = id;
            return this;
        }

        public Builder setPassword(String password) {
            user.password = password;
            return this;
        }

        public Builder setAge(int age) {
            user.age = age;
            return this;
        }

        public User build() {
            return user;
        }
    }
}

さて、これで柔軟性を保ちつつ、それなりに扱いやすそうなUser型が宣言できました。いよいよ、Web API リクエストに載せてユーザを作成したり、Web API のレスポンスからUserを取り出してプロフィールを表示したりということができるようになります。

無事プロフィールを表示する実装が終わったところで、年齢を非公開にしている人のプロフィールを見に行くと、なぜか年齢が見えない人にも見えていることがわかりました。しかも、設定した年齢とは異なる年齢が見えているようです。

プリミティブ型の代わりにラッパー型を使う

Web API と通信する場合、データはシリアライズしなければ通信経路に乗せられません。多くの場合、Json や Xml、MessagePack のような形式にシリアライズされます。シリアライズされたものを戻す操作をデシリアライズと言いますが、このシリアライズ・デシリアライズはフォーマットごとに決まった操作をするので、GsonJackson などのライブラリとしてまとめられたものを使うことが多いでしょう。

この時、シリアライズされたデータには存在しないフィールドが、オブジェクトの生成時にどのように初期化されるかは、言語仕様に依存します。Userを例に取ると、User型の宣言には存在するint型のageが、シリアライズされたデータにはageというint型のフィールドが存在しなかった場合(まさに、公開レベルの制御でこのような状況はいくらでも生まれ得る)、int型の初期値が設定されます。これは、言語仕様では0であると定められているので、自動で0が代入されます。

しかし、ageは年齢であり、数え年でなければ0は年齢として正しい値です。デシリアライズされたオブジェクトを眺めただけでは、それが公開レベルによって非公開にされ存在しないことになっていたのか、はじめから0だったかを知るすべはありません。

最も簡単にこれを判別できるようにするには、プリミティブ型を使わずラッパー型を使うようにします。存在しなければnullで、存在すれば任意の数字になります(おそらくこの話は Java 特有の問題かも…)。

User.java

public class User {
    // ...

    public static class Builder {
        private final User user;

        // ...

        public Builder setAge(Integer age) {
            user.age = age;
            return this;
        }

        // ...
    }
}

ああ、これでようやく、コードがただコードではなく、意味を持ったコードになりました。Web API もこれでもう怖くないですね。そう、ネットワークさえ繋がっていれば……

データの流れを見極める

ローカルにあるデータソース

Web API の多くは、Json や MessagePack、XML などの形式を利用して直列化されています。これによって、データの表現を統一することが出来、どのプラットフォームでも、決まった表現をバイナリに変換するロジックさえあれば同じように取り扱うことが出来ます。先ほどの型を使いこなす例でも Json をもとに少し取り上げました。

ところで、Web API への通信は、できることなら少なく済ませておきたいところです。通信インフラが整っているとはいえ、常時接続を前提に出来るほどではないのが現状だからです。つまり、ある程度オフライン時の対応が必要となります。通信ができない環境下でも、過去に Web API から取り出したデータを何かしらの形で永続化しておけば、最低限以前にユーザがアプリを使用した時までのデータは、通信をしなくても表示まで出来そうですね。多くの場合、キャッシュとして実装することになりそうな気がします。

データのキャッシュ

すでに私達は、データを文字列化する方法を心得ており、その仕組みも持っています。であれば、文字列化したものをファイルに書き出してしまえば、簡単にキャッシュの仕組みを作ることが出来そうです。

Cache.java

// キャッシュを扱うクラスのインタフェース。何らかの型 T のデータを取り扱う。
public interface Cache<T> {
    void set(T t);
    T update(T t);
    void remove(T t);
    T get();
}
UserCache.java

// User 型のデータのためのキャッシュ
public class UserCache implements Cache<User> {
    @Override public void set(User user) {}
    @Override public User update(User user) {}
    @Override public void remove(User user) {}
    @Override public User get() {}
}

クラス自体は簡単ですね。キャッシュに乗せる、値を更新する、キャッシュから取り出す、キャッシュを消す。それだけです。

さて、ファイルに書き出す場合、どのように書き出すのがよいでしょうか。set()するたび以前のUserを上書きするくらい単純化したものでしょうか、それとも、異なるUserごとに異なるファイルを生成すべきでしょうか。あるいは、同じファイルに配列のように付け足していくのでしょうか。なんとなく、値の更新の時にもその設計が影響しそうな気がしますね。勿論、get()の実装もそれによって左右されそうです。

もう少し利便性を求めるならば、SQLite を使ってしまうのも良さそうです。データベースとスキーマさえ作ってしまえば、CRUDの操作はデータベースに任せることが出来ます。

いずれにしても、最終的には I/O が発生します。つまり、実行スレッドをブロックする時間が生まれます。そうすると、このキャッシュの操作をメインスレッドから実行されると困ったことになります。

スレッドをブロックする操作を明示する

完全にメモリ上でのデータは取り扱わず、ディスク上のデータを取り扱う場合は、確実にブロックする操作が発生するので、メソッド名でこれを明示しておきたいところです。そうすれば、キャッシュクラスを使う人は非同期処理を用いて実装しなければならないことに容易に気付くことが出来ます(そしておそらく、このキャッシュがモデルに隠蔽されることも)。

Cache.java

public interface Cache<T> {
    void blockingSet(T t);
    T blockingUpdate(T t);
    void blockingRemove(T t);
    T blockingGet();
}

この方法では、ランタイムに「呼ばれても問題ない」ようにすることが出来ません。コードとしては何の問題もないので、メインスレッドをブロックするような呼び出しであろうと実行は可能です。もし仮にメインスレッド上で呼び出されても、例外を吐いて警告を促す仕組みをいれてもよいでしょう。

もし、SharedPreferencesのようにメモリ上にもある程度のキャッシュを持っておく場合、メモリ上にデータがる場合はブロックしないことになります。このような場合、スレッドがブロックするかどうかはメモリキャッシュにうまくヒットするかどうかに依存するため、コールバックの仕組みを用いて対応すると良さそうです。

Cache.java

public interface Cache<T> {
    void set(T t, Callback<T> callback);
    void update(T t, Callback<T> callback);
    void remove(T t, Callback<T> callback);
    void get(Callback<T> callback);
}

メモリキャッシュにヒットしたかどうかにかかわらず、操作の結果はすべてコールバックに返されます。ヒットすればそのままコールバックを呼び出し、しなければ一旦非同期処理を走らせたあと、その結果をメインスレッドでコールバックに返すようにしておけば、使う人はコールバックが必ずメインメソッドで実行される前提でコードを書くことが出来ます。コールバックの詳細については後ほど述べます。

ところで、データベースを取り扱うにしても、ファイルを取り扱うにしても、Android であればContextが必要になります。キャッシュであれば、画面ごとにキャッシュクラスのインスタンスがそれぞれに生成されるようなことになると、同期化を意識しないといけなかったり、そもそも画面ごとにオブジェクトを作っても、それらが全て異なる振る舞いをするわけではなのでメモリの無駄遣いだったり、問題が出てきそうです。できることなら、アプリケーションが動いている間キャッシュのインスタンスは 1 つになっていて欲しいです。しかし、すべてのクラスがそうなってしまうと、メモリが常に圧迫されてしまうので、それはそれで困ったことになります。つまり、オブジェクトはその役割に応じてライフサイクルを管理しなければならないのです……

オブジェクトの生死を扱う

ApplicationContext vs Context

Android の場合、Contextクラスが各種のリソースとコードをつなぐブリッジの役割を果たしています。すべてのリソースはContextによって解決されますが、そのContextの振る舞いは様々な状況に応じて変わっていきます。特に、アプリケーションそのもの(Application)、UI(Activity)、常駐サービス(Service)はすべて、Contextを親として、ユーザの操作等によってダイナミックに状態を変えていきます。
そして、Contextはまた、Android がもつ様々なフレームワークへの窓口としての役割も持ちます。たとえば、なんちゃらManager系(ActivityManagerとかAccountManagerとかPackageManagerとか)はContextを介して取得しますし、データベースへのアクセスも、ContextからContentResolverを得てから行います。

これらのことから、おそらくアプリのコード上には、いたるところにContextが現れます。

UserCache.java

public class UserCache implements Cache<User> {
    private final Context context;

    // What is this context actually??
    public UserCache(Context context) {
        this.context = context;
    }
}

しかし、前述のとおり、Contextとは一口に言っても、それがActivityである場合もあれば、Serviceである場合もあり、Applicationである場合もあります。継承関係の問題で、それら全てはContextへキャスト可能なので、単にContextといった場合には、使う側はどのContextが要求されているのかに注意を向ける必要があります(勿論、そのようなクラスを設計する側も、どのようなContextが渡されるのが適切か、その前提を踏まえて設計しなければならないのですが)。

なぜ気をつけなければいけないかは多くの人達がその経験から語っています。世に言うメモリリークをこす原因のトップが、このライフサイクルに起因するところになります。

命名規則

では実際、どの Context が必要かを示す手段にはどのようなものがあるでしょう。

1つには、命名規則によって示す方法があります。命名規則ですので、個々のプロジェクトごとにも様々異なることがあり得ますが、おおよその感じでは以下のようになるでしょうか。

Something.java

public class Something {
    public Something(Context applicationContext) {}
}

あるいは

Something.java

public class Something {
    public static Something with(Context context) {}
}

つまるところ、ActivityServiceのような短いライフサイクルであるはずのものが、それを超えてApplicationのライフサイクルで長生きしてしまうことがなければ良いので、引数名にApplicationContextかどうかを示しておけば、どんなContextを要求しているかを示すことが出来るようになります。

しかし、ついウッカリと、間違えてしまう可能性は排除できませんね。

具象に寄せる

そこで大胆にもContextを使わず、もっと具象のActivityServiceApplicationを使ってしまう方法があります。これであれば、どのContextが求められるかは使う側からしても明らかです。いずれにしても全てはContextなのですから、やりたいことは同じように出来るはずです。

Something.java

public class Something {
    private final Application application;
    public Something(Application application) {
        this.application = application;
    }
}

ないしは

Something.java

public class Something {
    public static Something with(Activity activity) {}
}

のような。

いや、面倒臭ければ、以下のようにしてしまうのも手かもしれません。

Something.java

public class Something {
    private final Context applicationContext;

    public Something(Context context) {
        applicationContext = context.getApplicationContext();
    }
}

取り敢えずは、引数にどんなContextを渡されても、このクラス自身はApplicationのライフサイルで生きることを前提に作られていることがわかります。このクラスをいじる人からするとそのことは明白ですが、このクラスを使う人からするとどうでしょう。Activityのライフサイクルで生きてくれそうな気がしてnew Something(this)等とActivityで書けそうですが、もしこのSomethingが、Activityへコールバックするような仕組みを持っていたとしたら……

コールバック

UI で起きるイベントもListenerと呼ばれるオブザーバパターンのコールバックがありますが、それにかぎらず、非同期処理の結果を受け取る際にもコールバックを用います。UI で起きるイベントの中にも、実は非同期でコールバックを受けることのあるものがありますし、ライフサイクルの異なるものにコールバックを設定するものもあります(代表例:SharedPreferences)。非同期、と言う時点でライフサイクルがActivityServiceと異なっていますので、まとめてライフサイクルの異なるものへのコールバックの設定に注意が必要ということになります(IPC を含む)。

さて、コールバックオブジェクト(コールバック関数)の管理については、コールバックを設定する側のContextに合わせて、そのContextがメモリから回収可能になった時点でコールバックも消えるような実装をすれば、夜も眠れなくなるような問題は無くなりそうです。この話題はおれおれコールバック設計コールバックと上手に付き合うにまとめてあります。キーワードは、弱い参照です。

一方で、Android フレームワークを見ていると、ライフサイクルに応じて適切にコールバックの管理をするよう、API を使う側に求めるものも見受けられます。

Serviceであれば、bindServiceunbindServiceという対になるメソッドがあり、他にも、SharedPreferencesであれば、registerOnSharedPreferenceChangeListenerunregisterOnSharedPreferenceChangeListenerという対になるメソッドがあります。

Android フレームワークにおいては、このようにライフサイクルに応じて管理が必要なものに対する API には、メソッド名に一定の命名規則を持っています。

登録 解除
Service bind** unbind**
SharedPreferences register** unregister**
その他 add** remove**

特にライフサイクルに応じた管理が必要ないものは、setなメソッドが生えているだけです。

もし自分たちで同じような設計を取るのであれば、Android フレームワークに準じた命名規則でメソッドを宣言すると統一がとれてよいでしょう。

実は、コールバックの仕組みを単純化するのであれば、自分たちで仕組みを実装する他にも方法があります……

EventBus

UI で起こるイベントの他、モデルの各種の変更、設定の変更、実行環境の変化などもイベントとして捉える事ができます。それらのイベントをルーティングする仕組みとして、EventBusという仕組みが出てきました。Android の BroadcastReceiver ほどの面倒臭さがなく、シンプルな API で簡単に取り扱うことが出来ます。


public class MyActivity extends Activity {
    private Bus bus; // otto による EventBus

    @Override
    public void onStart() {
        super.onStart();
        bus.register(this); // この Activity を登録してイベントの受信を開始する
    }

    @Override
    public void onStop() {
        bus.unregister(this);
        super.onStop();
    }

    @Subscribe
    public void onSomethingHappen(SomethingCompleteEvent event) {
        // 何かが起きた
    }
}

命名規則も分かりやすくなっていますね。忘れなければどうということはありません。

ただし、通常のinterfaceによるコールバックと異なり、イベントの受信をするメソッドが実装されていることをコンパイラがチェック出来なくなるので、イベントを受信するメソッドを規定の通り書かないでいると、実行できても永遠にイベントが受信できません。

また、イベントの粒度がまちまちだと、ありとあらゆるイベントがあちこちで定義されはじめ、収集がつかなくなることもあります。多くの場合、誰がどんなイベントを発生させているかが混乱してしまうことに問題がありますが、これを分かりやすくするには、イベント専用のネームスペースを用意しないで、代わりに、イベントを発生させるクラスのstaticな内部クラスとして宣言する方法があります。

SomeModel.java

public class SomeModel {
    private final Bus bus;

    public void something() {
        // なにかする

        bus.post(new SomethingCompleteEvent())
    }

    public static class SomethingCompleteEvent {}
}

イベントの発生元とイベントの種類の対応付けがこれで分かりやすくなりました。受信側は以下のようにすれば、誰がイベントを発生させたかがはっきりします。


public class MyActivity extends Activity {
    @Subscribe
    public void onSomethingHappen(SomeModel.SomethingCompleteEvent event) {
        // 外側のクラス名から名前を解決する
    }
}

ここまでで、レイヤの異なるオブジェクト同士の関連やコミュニケーション、そしてライフサイクルをうまく取り扱う部分が見えてきました。これらを元に、アプリケーションの要求にしたがって実装を進めていくことになります。

さて実装を進めていくと、画面が増え、モデルが増え、多くのクラスが作られていきます。その中で、「これってこのアプリケーションの中でもよく見かけるパターンだな……」というような、アプリケーションに横断的な共通項が見えてきます。そういった、よく見る共通項はどのようにしてうまくまとめると良いでしょうか……

共通する処理を委譲する

ある程度大きなプロジェクトになると、各所で似たような処理が現れてきます。例えば、GoogleAnalytics を導入して Activity のトラッキングをするような処理は、概ねどの Activity でも同じように記述するようになるはずです。

継承を使う

最も単純で、考え方としては古くからある「差分プログラミング」的な手法を用いることで、処理の共通化を実現できます。使う側としては、そのクラスを継承しさえすれば勝手にあらゆることをしてくれるようになるため、一見するととても楽なように見えます。


public abstract class BaseActivity extends Activity {
    @Override
    public void onStart() {
        super.onStart();
        // ここで Google Analytics とか共通処理をする
    }

    @Override
    public void onStop() {
        // ここで Google Analytics とか共通処理をする
        super.onStop();
    }
}

ここで、Support Library の存在について考えてみます。このライブラリでは、古い Android 向けに最新のプラットフォームで利用可能な各種の API をバックポートしてくれていますが、Activity も例に漏れず、ActionBar や Fragment 等を扱うためのバックポートクラスが存在します。Java では多重継承を許していない為、Support Library の Activity を使うには、BaseActivityとは別にさらにBaseFragmentActivityBaseActionBarActivityBaseAppCompatActivityといったものを作り続ける必要があります。これでは本末転倒で、折角共通化したのに、共通化のためのコードが分散してしまいます。また、共通 Activity の存在を無視されるとつらいものがあります(設計思想上、はじめから継承を避けるため継承関係を辿らないようにできているライブラリもあるくらいです[otto 等])。

委譲オブジェクトを使う

そこで、Activity からは委譲オブジェクトを参照し使うだけにしておいて、実際の処理はその委譲オブジェクトで実行する方法で解決します。


public class SomeDelegate {
    public void onStart(Context context) {
        // ここで Google Analytics とか共通処理をする
    }

    public void onStop(Context context) {
        // ここで Google Analytics とか共通処理をする
    }
}

public class SomeActivity extends Activity {
    private SomeDelegate someDelegate;

    @Override
    public void onStart() {
        super.onStart();
        someDelegate.onStart();
    }

    @Override
    public void onStop() {
        someDelegate.onStop();
        super.onStop();
    }
}

こうすると、親 Activity が増えてもSomeDelegateを使っておけばだれでも共通した処理を利用でき、共通部分の分散が防がれます。

特に Activity に特化した場合では、ActivityLivecycleCallbacks を用いることで同じことができるようになります。


public class ActivityWatcher implements Application.ActivityLifecycleCallbacks {
    // ...

    @Override
    public void onActivityStarted(Activity activity) {
        // ここで Google Analytics とか共通処理をする
    }

    @Override
    public void onActivityStopped(Activity activity) {
        // ここで Google Analytics とか共通処理をする
    }

    // ...
}

引数に Activity が渡ってくるので、ここでEventBusのような仕組みを用いてActivityregisterすることも簡単に出来ます。
なにより、Support Library の Activity ごとにBaseActivityを作らなくてもここですべてが賄えることが大きな利点と言えるでしょう(但し ActivityLifecycleCallbacks は IceCreamSandwich 以降のみで利用可能)。

アノテーションを使う

アノテーションプロセッサを用いて、特定のアノテーションの付いたメソッドに対して共通する処理を注入することで、共通部分を自動生成する方法もあります。


public class SomeActivity extends Activity {
    @Override
    @GoogleAnalyticsStart
    public void onStart() {
        super.onStart();
    }

    @Override
    @GoogleAnalyticsStop
    public void onStop() {
        super.onStop();
    }
}

もし特定の Activity でのみ共通処理を実行しないというような柔軟性を備える必要があるならば、BaseActivity のような継承によるパターンは完全に無意味と化します。委譲オブジェクトまたはアノテーションによって、それぞれの利用者側の判断で対応できるような余地が生まれます。少なくとも、「呼ぶのを忘れたので事故った!!」というパターンに対しては、継承を使おうと委譲オブジェクトを使おうと同じです(むしろ継承を用いたパターンでsuperを呼び忘れる事のほうが圧倒的に邪悪です。IDE が勝手に色々やってくれる時代であるとはいえ……)。ならば、委譲やアノテーションによって柔軟性を得ることのほうがメリットが大きいように思います。

とは言え、どうしても継承を利用するパターンは存在します。Dagger 等のフレームワークを用いる場合がまさにそうで、それぞれActivityServiceごとに基底クラスを作っておくことで適切に依存性の注入を実行できるようになります。概ね、ライブラリに閉じた話をするのであれば、BaseActivityのような継承を利用するパターンが視野に入ってくることでしょう。

まとめ

以下の4つの観点で、チームとして少ないミスで素早くアプリを継続的・持続的に作り続けるためのメソッドを見てきました。

  1. 型を使いこなす
  2. データの流れを見極める
  3. オブジェクトの生死を扱う
  4. 共通する処理を委譲する

どれも使い古された手法が多く見られますが、プラクティスとしては今日でも有用です。特に、作り方としては iOS でもつぶしが利くように思います(多くの UI を持つアプリケーションを作る場合においても応用できそう)。Android OpenSource Project にも沢山の参考になるコードがありますし、今や GitHub にも様々なプロジェクトが公開されています。それらから学ぶことも多くあると思っているので、初めて Android に触れる人でも、そこから真似て使ってみるということから始められると思います。

568
576
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
568
576