この投稿は DroidKaigi で話そうと思ったけど採択されなかった RejectedKaigi な内容です。
プログラムは、書けば書くほど複雑になります。行数が増え、分岐や繰り返しが増え、メソッドが増え、クラスが増え、パッケージが増え、管理するものは日に日に増えていきます。これらのものを使う側からすると、使うものが増えるということは、それだけ覚えることが増えることになります。勿論、IDE やエディタプラグインによって、そのような労力が極力減らされることもありますが、覚えることが少ないに越したことはありません。
この記事では、IDE やエディタプラグインはひとまず脇に置き、チームでコミュニケーションを取りながらコードを書くという観点で、従来のプログラミングのプラクティスを基に、開発時のミスを少なくし、チームで素早くアプリを作り続けていく方法論を深めていこうと思います。
Agenda
- 型を使いこなす
- データの流れを見極める
- オブジェクトの生死を扱う
- 共通する処理を委譲する
- まとめ
型を使いこなす
文字列型の引数が増えてきたら型を検討する
文字列はとても便利です。名前も、IDも、その他いろいろなものが文字列で表されます。一方、プログラム上では、文字列の意味に関係なく文字列は文字列なので、どんなものも文字列型として扱うことができます。
これで何が起こるかというと、
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
の例では、以下のような引数にできれば理想的です。
public interface UserApiClient {
void create(User user);
}
非常に単純になりました。引数は一つ、User
を作るのですから、User
型を引数に取るようにします。User
型以外のオブジェクトは受け付けなくなりますので、変なオブジェクトが渡される心配がなくなります。プログラムの記述と意味がリンクすることで、UserApiClient
を使用するプログラマも、メソッドの役割が理解しやすくなります。
では、User
型はどのように実装されるでしょうか。
意味のある文字列には型を与える
単に、最初のcreate
メソッドをそのままコンストラクタにしていたのでは意味がありません。
public class User {
public User(String nickname, String familyName, String givenName, String password, String sex, int age) {}
}
結局、これでは最初の悪夢を再来させるだけです。
このUser
のコンストラクタをうまく実装するには、少なくとも2つの方向性があるように見えます。
-
sex
は、取りうる値のパターンが決まっているので列挙型にする -
familyName
とgivenName
を型にまとめる
まずは1について。性別を示すsex
のパターンには、男性、女性があります(gender
とは区別しておきましょう)。パターンが有るということは、それらを列挙して定義することができると言えます。文字列にしてしまうと、hoge
等のどんな文字列でもsex
にすることができてしまいますが、列挙型として型を与えると、常にsex
はMALE
かFEMALE
かnull
かになります。null
を許すかどうかは仕様次第かもしれません。
public class User {
public User(String nickname, String familyName, String givenName, String password, Sex sex, int age) {}
public static enum Sex {
MALE, FEMALE;
}
}
次に2について。familyName
とgivenName
は本名を構成するメンバと見て、本名を示す型を宣言してみます。
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つの方向性をよりざっくりまとめると、以下のようになるでしょう。
- 文字列値にパターンが有る場合は列挙型を導入し、型を与える
- 複数の文字列があるデータ構造のメンバをなす場合は、そのデータ構造を示す型を与える
本名の例は極端すぎるかもしれませんが、意味のある単位で型を宣言していくことの重要性は見えたと思います。
さて、このUser
、どう見ても簡単に User を一意に見分ける方法が無いように見えます。nickname やその他のプロパティからハッシュをとって計算する、でも出来るかもしれませんが、おそらくそのような実装はせず、専用の ID を与えて一意なキーを発行するのが王道でしょう。
更にいうと、ユーザを作成する際にはpassword
が必須ですが、それが常に必要かと言われると疑問です。例えば、User
のプロフィールを表示するためにわざわざpassword
まで知っておく必要はないはず。つまり、password
は常に必須ではなく、オプショナルな値であるということです。この意味では、ID なるものも、ユーザの作成リクエストを送る前には ID を割り振る事ができないかもしれませんね(サーバサイドで ID を割り振っている場合)。また、年齢は公開レベルが設定でき、公開レベルの範囲外の人には見えないようにする仕様があるかもしれません。
そうすると、コンストラクタがごちゃごちゃし始めます。
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 メソッドを宣言して要求に応じて使う側が選択的にフィールドの値を設定できるようにしていけば、コンストラクタが混乱を起こすことがなくなります。
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 のような形式にシリアライズされます。シリアライズされたものを戻す操作をデシリアライズと言いますが、このシリアライズ・デシリアライズはフォーマットごとに決まった操作をするので、Gson や Jackson などのライブラリとしてまとめられたものを使うことが多いでしょう。
この時、シリアライズされたデータには存在しないフィールドが、オブジェクトの生成時にどのように初期化されるかは、言語仕様に依存します。User
を例に取ると、User
型の宣言には存在するint
型のage
が、シリアライズされたデータにはage
というint
型のフィールドが存在しなかった場合(まさに、公開レベルの制御でこのような状況はいくらでも生まれ得る)、int
型の初期値が設定されます。これは、言語仕様では0
であると定められているので、自動で0
が代入されます。
しかし、age
は年齢であり、数え年でなければ0
は年齢として正しい値です。デシリアライズされたオブジェクトを眺めただけでは、それが公開レベルによって非公開にされ存在しないことになっていたのか、はじめから0
だったかを知るすべはありません。
最も簡単にこれを判別できるようにするには、プリミティブ型を使わずラッパー型を使うようにします。存在しなければnull
で、存在すれば任意の数字になります(おそらくこの話は 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 から取り出したデータを何かしらの形で永続化しておけば、最低限以前にユーザがアプリを使用した時までのデータは、通信をしなくても表示まで出来そうですね。多くの場合、キャッシュとして実装することになりそうな気がします。
データのキャッシュ
すでに私達は、データを文字列化する方法を心得ており、その仕組みも持っています。であれば、文字列化したものをファイルに書き出してしまえば、簡単にキャッシュの仕組みを作ることが出来そうです。
// キャッシュを扱うクラスのインタフェース。何らかの型 T のデータを取り扱う。
public interface Cache<T> {
void set(T t);
T update(T t);
void remove(T t);
T get();
}
// 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 が発生します。つまり、実行スレッドをブロックする時間が生まれます。そうすると、このキャッシュの操作をメインスレッドから実行されると困ったことになります。
スレッドをブロックする操作を明示する
完全にメモリ上でのデータは取り扱わず、ディスク上のデータを取り扱う場合は、確実にブロックする操作が発生するので、メソッド名でこれを明示しておきたいところです。そうすれば、キャッシュクラスを使う人は非同期処理を用いて実装しなければならないことに容易に気付くことが出来ます(そしておそらく、このキャッシュがモデルに隠蔽されることも)。
public interface Cache<T> {
void blockingSet(T t);
T blockingUpdate(T t);
void blockingRemove(T t);
T blockingGet();
}
この方法では、ランタイムに「呼ばれても問題ない」ようにすることが出来ません。コードとしては何の問題もないので、メインスレッドをブロックするような呼び出しであろうと実行は可能です。もし仮にメインスレッド上で呼び出されても、例外を吐いて警告を促す仕組みをいれてもよいでしょう。
もし、SharedPreferences
のようにメモリ上にもある程度のキャッシュを持っておく場合、メモリ上にデータがる場合はブロックしないことになります。このような場合、スレッドがブロックするかどうかはメモリキャッシュにうまくヒットするかどうかに依存するため、コールバックの仕組みを用いて対応すると良さそうです。
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
が現れます。
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つには、命名規則によって示す方法があります。命名規則ですので、個々のプロジェクトごとにも様々異なることがあり得ますが、おおよその感じでは以下のようになるでしょうか。
public class Something {
public Something(Context applicationContext) {}
}
あるいは
public class Something {
public static Something with(Context context) {}
}
つまるところ、Activity
やService
のような短いライフサイクルであるはずのものが、それを超えてApplication
のライフサイクルで長生きしてしまうことがなければ良いので、引数名にApplication
のContext
かどうかを示しておけば、どんなContext
を要求しているかを示すことが出来るようになります。
しかし、ついウッカリと、間違えてしまう可能性は排除できませんね。
具象に寄せる
そこで大胆にもContext
を使わず、もっと具象のActivity
やService
、Application
を使ってしまう方法があります。これであれば、どのContext
が求められるかは使う側からしても明らかです。いずれにしても全てはContext
なのですから、やりたいことは同じように出来るはずです。
public class Something {
private final Application application;
public Something(Application application) {
this.application = application;
}
}
ないしは
public class Something {
public static Something with(Activity activity) {}
}
のような。
いや、面倒臭ければ、以下のようにしてしまうのも手かもしれません。
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
)。非同期、と言う時点でライフサイクルがActivity
やService
と異なっていますので、まとめてライフサイクルの異なるものへのコールバックの設定に注意が必要ということになります(IPC を含む)。
さて、コールバックオブジェクト(コールバック関数)の管理については、コールバックを設定する側のContext
に合わせて、そのContext
がメモリから回収可能になった時点でコールバックも消えるような実装をすれば、夜も眠れなくなるような問題は無くなりそうです。この話題はおれおれコールバック設計やコールバックと上手に付き合うにまとめてあります。キーワードは、弱い参照です。
一方で、Android フレームワークを見ていると、ライフサイクルに応じて適切にコールバックの管理をするよう、API を使う側に求めるものも見受けられます。
Service
であれば、bindService
とunbindService
という対になるメソッドがあり、他にも、SharedPreferences
であれば、registerOnSharedPreferenceChangeListener
とunregisterOnSharedPreferenceChangeListener
という対になるメソッドがあります。
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
な内部クラスとして宣言する方法があります。
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
とは別にさらにBaseFragmentActivity
やBaseActionBarActivity
やBaseAppCompatActivity
といったものを作り続ける必要があります。これでは本末転倒で、折角共通化したのに、共通化のためのコードが分散してしまいます。また、共通 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
のような仕組みを用いてActivity
をregister
することも簡単に出来ます。
なにより、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 等のフレームワークを用いる場合がまさにそうで、それぞれActivity
やService
ごとに基底クラスを作っておくことで適切に依存性の注入を実行できるようになります。概ね、ライブラリに閉じた話をするのであれば、BaseActivity
のような継承を利用するパターンが視野に入ってくることでしょう。
まとめ
以下の4つの観点で、チームとして少ないミスで素早くアプリを継続的・持続的に作り続けるためのメソッドを見てきました。
- 型を使いこなす
- データの流れを見極める
- オブジェクトの生死を扱う
- 共通する処理を委譲する
どれも使い古された手法が多く見られますが、プラクティスとしては今日でも有用です。特に、作り方としては iOS でもつぶしが利くように思います(多くの UI を持つアプリケーションを作る場合においても応用できそう)。Android OpenSource Project にも沢山の参考になるコードがありますし、今や GitHub にも様々なプロジェクトが公開されています。それらから学ぶことも多くあると思っているので、初めて Android に触れる人でも、そこから真似て使ってみるということから始められると思います。