目次
アーキテクチャ設計
この項では、Android アプリのアーキテクチャの設計指針について解説します。
Android フレームワークの特徴
Android フレームワークは、MVC フレームワークに基いて設計されています。
Activity や Fragment は Controller の役割を果たし、Button や TextView は文字通り View の役割を果たしています。
これまでのサンプルコードでは、Activity や Fragment でイベントを受け取って、その処理をも Controller の中で実行していましたが、このようにして拡張を行うと、往々にして Activity や Fragment の役割が増え続け、肥え太ったクラスが出来上がってしまいます。
また、Activity や Fragment ですべてを請け負ってしまうと、非常にテストがしづらくなります。
このようなコードは保守性を損なうため、予め以下の様な指針に沿って、適切にレイヤ分けを行うことを強くお奨めします。
レイヤの分離
MVC フレームワークにおけるレイヤの分離方法を下記のようにすることを考えます。
- ビジネスロジック層(Model)
- 設定を保存、認証する、日記を投稿する等々のユースケースの動詞にあたる処理を行います。
- プレゼンテーション層(View)
- 画面レイアウト、アニメーション、View コンポーネントの設定を行います。
View コンポーネント間の連携(トグルする等)もこのレイヤになります。
Androidでは、理想的には View コンポーネント間で連携する必要がある場合、ViewGroup で纏めて独立した View として定義したい所ですが、現実的には再利用の当ても無い View を一々作っていくのはコストが高い為、
それらの処理を Helper クラスに委譲して Activity や Fragment 等コントローラ層のクラスから使用します。 - コントローラ層(Controller)
- プレゼンテーション層、ビジネスロジック層間の相互処理と、画面遷移処理を行います。
Androidにおいては、Activity や Fragment で行う View コンポーネントのイベントハンドラからビジネスロジック層を呼び出す処理やstartActivity*、onActivityResult の処理になります。Adapter や BroadcastReceiver もこのレイヤです。
また、プレゼンテーション層とビジネスロジック層の間でデータ構造の不一致が起きる場合(頻繁に起きる)、データ構造変換もこのレイヤで行います。
Android の場合、それらの処理も Helper クラス(Converter クラスとしても良い)に委譲します。
アプリの規模が大きくなるにつれ、Activity や Fragment が大きくなる問題が有りますが、それはプレゼンテーション層とビジネスロジック層の切り分けが出来ていないからです。
パッケージの命名
アプリの持つパッケージ名 (AndroidManifest に宣言するパッケージ名) 以下の名前空間を、次のように命名すると良いでしょう。
名前 | 意味 |
---|---|
<package_name> .app.<domain> .view |
View コンポーネントの場所 |
<package_name> .app.<domain> .ui |
Controller コンポーネントの場所 |
<package_name> .app.<domain> .ui.helper |
Controller から移譲される様々な処理の場所 |
<package_name> .app.<domain> .ui.state |
UI の状態を表す State オブジェクトの場所 |
<package_name> .app.<domain> .entity |
Model で取り扱うデータ構造の場所 |
<package_name> .app.<domain> .model |
Model の場所 |
<package_name> .app.<domain> .service |
Service コンポーネントの場所 |
<package_name> .app.<domain> .receiver |
BroadcastReceiver コンポーネントの場所 |
Dependency Injection (DI)
この項では、一般的な Dependency Injection の概要と、Android での使用について解説します。
DI とは
Dependency Injection (DI) とは、コンポーネント(クラス)の直接の依存関係を取り除き、実行時やコンパイル時にその依存関係を注入するデザインパターンのことです。
このようにすることで、各コンポーネントはインタフェースへ依存するようになり、保守性が向上します。
インタフェースの実装クラスを差し替えることが容易にできるようになるので、テスト時におけるオブジェクトのモックやスタブへの差し替えもずっと楽に行えるようになります。
様々な DI コンテナ
現在、下記に挙げる DI コンテナが提供されています。
- Java で使用する DI コンテナ
- Android 向けのもの
Android における DI コンテナの使用
今回は、Proton を例にその使い方を見ていきます。
以下は、Activity の実装です。
public class MainActivity extends ProtonActivity {
// @Inject アノテーションを付与したものに対して、オブジェクトが自動で注入されるので
// このコード上で具象クラスを new する必要がない。
// これによって、このクラスと具象クラスの依存関係がなくなり、インタフェースへと依存することになることで
// コンポーネントの差し替えが容易に行えるようになる
@Inject
private OptionsItemSelectionHandler mOptionsMenuHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (mOptionsMenuHandler.handle(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
@Inject
アノテーションを付与したフィールドに対して、実行時にオブジェクトが注入されます。
この例では、OptionsItemSelectionHandler
インタフェースを実装したクラスのインスタンスが注入されるようになっています。
OptionsItemSelectionHandler
インタフェースに対して、どの具象クラスを注入するか、という情報を設定するクラスは次のとおりです。
public class DependencyInjectionSampleModule extends DefaultModule {
@Override
protected void configure() {
super.configure();
// どのクラスがどのインタフェースに従っているかと、どのコンポーネントのライフサイクルに依存するかを規約として決める
bind(OptionsItemSelectionHandler.class).to(MainOptionsItemSelectionHandler.class).in(ContextScoped.class);
}
}
このクラスのconfigure()
は、DI コンテナの初期化の時に実行されます。
通常、DI コンテナの初期化はアプリケーションの開始時に行うため、下記のようにApplication
クラスを継承した独自のクラスを定義します。
// アプリケーションの Context を表すクラス
// アプリケーション全体に及ぶ規約をここで設定する
public class DependencyInjectionSampleApplication extends Application {
// アプリケーションが開始されるときに呼ばれるライフサイクルコールバック
@Override
public void onCreate() {
super.onCreate();
// DI コンテナの初期化
Proton.initialize(this, new DependencyInjectionSampleModule());
}
}
独自のアプリケーションクラスを定義する場合、AndroidManifest にもその旨を示す宣言をします。
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.mixi.sample.di"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="jp.mixi.sample.di.DependencyInjectionSampleApplication">
<activity
android:name="jp.mixi.sample.di.ui.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>