はじめに
最近、Androidの小さなゲームを作っているのですが、Activityが少し多めです。view、com、core、dao、service、utilなどのレイヤーで分けるとどうもしっくりこないので、調べてみたところ、このようなパッケージの分け方はもう主流ではないことに気付きました。
1. PBL (Package By Layer)
レイヤーごとにパッケージを分けてクラスを構成する、つまり従来の手法です。view、com、core、daoなどのパッケージに分け、同じ役割のクラスをまとめて配置します。
default packageの下にプロジェクト全体のすべてのクラスを書くこともできなくはありませんが、関連性のない多くのクラスが密集していると、保守性は言うまでもなく、見た目にも不快です。
src
└─net
└─ayqy
└─app_gowithme
SearchPlan.java
ShowDetail.java
ShowMap.java
ShowRoute.java
// または、もう少しマシな例
src
└─net
└─ayqy
└─app_notepad
DAO.java
Dialog.java
MainActivity.java
myEditText.java
ShowList.java
P.S. Windowsでは、tree /fで上記のような構造を生成できます。
私が慣れ親しんでいるパッケージの命名方法(あるいはクラスの構成方法)は、クラスの役割(いわゆるPBL)に基づいて分けるものです。例えば:
src
│ summary.txt
│
└─net
└─ayqy
└─app_rsshelper
│ MainActivity.java
│ SettingActivity.java
│
├─com
├─core
│ Consts.java
│ Urls.java
│
├─dao
├─service
│ HtmlFetcher.java
│ JsInvoker.java
│ MainServ.java
│ RssFetcher.java
│
├─util
│ HtmlHelper.java
│ InputStream2String.java
│ RssHelper.java
│
└─vo
RssItem.java
各レイヤーの役割は以下の通りです:
- default package
View層。UIの表示と画面遷移のみを実装するActivityを定義します。
- com (component)
カスタムコンポーネント。カスタムViewやViewの組み合わせを含みます。
- core
コアクラス。設定データや定数を定義します。
- dao (data access object)
データベース操作クラス。dbHelperを含みます。
- service
各Activityに対応するService。そのActivityのビジネスロジックの実装を担当します。また、Serviceが依存するActivityと密接に関連する他のクラス(Activityのcontextを必要とするもの、例えばJsInvokerなど)もここで定義します。
- util
ユーティリティクラス。modelHelper(例えばRssHelper)を含みます。
- vo (value object)
model、あるいはbean。データ構造の定義とgetter/setterのみを含みます(必要であればequalsやcompareToなども追加できますが、とにかくmodelを純粋に保つようにします)。
私はJSPから入門したため、このレイヤー分けの手法が良いとずっと信じており、Androidでもそのまま使っていました(WinFormやWPFにまで持ち込んでいました)。幸い、プロジェクトの規模がどれも小さかったため、特に不都合は感じていませんでした。別の選択肢があるとも思っていなかったからです。
PBLの欠点は以下の通りです:
- パッケージ内の凝集度が低く、モジュール化されていない
通常、同じパッケージ内の各クラスは互いに関連性がなく、今後も関連することはないかもしれません。
- パッケージ間の結合度が高い
通常、あるクラスの中で、異なるパッケージから来た大量のものをimportする必要があります。
- コーディングが面倒
ある機能を実装するために、異なるディレクトリにある複数のファイルを編集する必要があります。IDEで大量のタブを開きっぱなしにしてしまい、閉じると探すのが大変なので、閉じるのをためらいがちになります。
同様に、削除するのも面倒です。複数のパッケージを行ったり来たりして探し回る必要があり、一歩間違えるとミスにつながります。
2. PBF (Package By Feature)
機能(モジュール)ごとにパッケージを分けてクラスを構成します。アプリの機能に基づいて、login、feedback、settings、orders、shippingなどに分けます。例えば:
// 医療系アプリ
src
└─com
└─domain
└─app
├─doctor
│ DoctorAction.java - an action or controller object
│ Doctor.java - a Model Object
│ DoctorDAO.java - Data Access Object
│ database items (SQL statements)
│ user interface items (perhaps a JSP, in the case of a web app)
│ ... Javaコードに限らず、その機能に関連するものはすべてここに置くべきです
│
├─drug
├─patient
├─report
├─security
├─webmaster
└─util
簡単に言えば、1つの機能が1つのパッケージに対応し、パッケージ名が機能名になります。一般的なクラスはutilなどのパッケージで定義しますが、その機能を実装するすべてのクラスはそのパッケージ内に配置します。
メリットは明確で、以下の通りです:
- パッケージ内の凝集度が高く、パッケージ間の結合度が低い
新しい機能を追加する際は、特定のパッケージ内のものだけを変更すれば済みます。
クラスの役���によるレイヤー分け(PBL)はコードの結合度を下げますが、パッケージの結合度をもたらします。新しい機能を追加するには、model、dbHelper、view、serviceなどを変更する必要があり、複数のパッケージのコードに手を加える必要があります。変更箇所が多いほど、新たな問題が生じやすくなるのではないでしょうか?
機能ごとのパッケージ分け(PBF)では、featureAに関連するすべてのものはfeatureAパッケージにあります。機能内で高い凝集度と高度なモジュール化が実現され、異なる機能間の結合度は低くなります。関連するものがまとまっているので、探しやすくなります。
- パッケージにプライベートスコープ(package-private scope)がある
あなたがある機能の開発を担当する場合、そのディレクトリ内のすべてのものはあなたのものになります。
PBLの手法では、すべてのユーティリティメソッドをutilパッケージに置きます。Aさんが新機能の開発中にxxUtilが必要になったとしますが、それは汎用的なものではありません。では、どこに置くべきでしょうか?仕方なく、レイヤー分けの原則に従い、utilパッケージに置くことになります。しかし、しっくりきません。かといって他のパッケージに置くのはさらに不適切です。機能が増えるにつれ、utilクラスも増えていきます。その後、Bさんが別の機能の開発中にxxUtilが必要になったとします。これも汎用的ではありませんが、utilパッケージを見るとすでに存在し、しかも再利用できません。仕方なくxxという名前を諦め、xxxUtilに変更することになります……。なぜなら、PBLのパッケージにはプライベートスコープがないからです。すべてのパッケージがpublicです(パッケージ間のメソッド呼び出しは日常茶飯事であり、各パッケージは他のパッケージからアクセス可能です)。
PBFであれば、AさんのxxUtilは当然featureAの下に、BさんのxxUtilはfeatureBの下に配置されます。もしユーティリティが汎用的だと思えば、utilパッケージを見て、そのユーティリティメソッドを共通のxxUtilに追加するかどうかを判断します。これにより、クラス名の衝突はなくなります。
PBFのパッケージはプライベートスコープを持ちます。featureAはfeatureBのいかなるものにもアクセスすべきではありません(もしどうしてもアクセスが必要なら、それはインターフェースの定義に問題があることを示しています)。
- 機能の削除が簡単
統計の結果、新機能が誰にも使われていないことがわかり、このバージョンでその機能を削除しなければならないとします。
PBLの場合、機能の入り口からビジネスフロー全体に至るまで、関連する削除可能なコードとクラスをすべて見つけ出して削除しなければならず、一歩間違えれば破綻します。
PBFの場合は簡単です。まず該当するパッケージを削除し、次に機能の入り口を削除します(パッケージを削除すれば入り口で必ずエラーが出るので簡単に見つかります)。これで完了です。
- 高度な抽象化
問題を解決する一般的なアプローチは、抽象から具体へ進むことです。PBFのパッケージ名は機能モジュールの抽象化であり、パッケージ内のクラスは実装の詳細です。これは抽象から具体へという流れに合致していますが、PBLはその逆を行っています。
PBFはAppNameを決定することから始まり、機能モジュールに基づいてパッケージを分け、次に各部分の具体的な実装の詳細を考えます。一方、PBLは最初からdao層が必要か、com層が必要かなどを考慮しなければなりません。
- ロジックコードの分離をクラスのみで行う
PBLはクラスとパッケージの両方を分離しますが、PBFはクラスのみを通じてロジックコードを分離します。
パッケージによって分離する必要はありません。なぜなら、PBLでは以下のような気まずい状況が生じる可能性があるからです:
├─service
│ MainServ.java
PBLに従えば、serviceパッケージの下のすべてのものはControllerなので、Servというサフィックスは必要ないはずです。しかし実際には、コーディングの便宜上、serviceパッケージを直接importし、導入したクラスと現在のパッケージのクラス名が衝突するのを避けるためにServサフィックスが付けられます。もちろん、サフィックスを使わないことも可能ですが、その場合はnew net.ayqy.service.Main()のようにパッケージパスを明記しなければならず、面倒です。
しかしPBFなら非常に便利で、importする必要もなく、直接new MainServ()とするだけです。
- パッケージのサイズに意味が生まれる
PBLでは、機能が追加されるにつれてパッケージのサイズが無限に増大することは合理的です。
しかし、PBFでパッケージが大きすぎる(パッケージ内のクラスが多すぎる)場合、それはその部分のリファクタリング(サブパッケージへの分割)が必要であることを示しています。
3. PBFの具体的な実践 (Google I/O 2015)
最も代表的なのはGoogle I/O 2015です。構造は以下の通りです:
java
└─com
└─google
└─samples
└─apps
└─iosched
│ AppApplication.java Applicationクラスの定義
│ Config.java 設定データ(定数)の定義
│
├─about
│ AboutActivity.java
│
├─appwidget
│ ScheduleWidgetProvider.java
│ ScheduleWidgetRemoteViewsService.java
│
├─debug
│ │ DebugAction.java
│ │ DebugActivity.java
│ │ DebugFragment.java
│ │
│ └─actions
│ DisplayUserDataDebugAction.java
│ ForceAppDataSyncNowAction.java
│ ForceSyncNowAction.java
│ ...
│
├─explore
│ │ ExploreIOActivity.java
│ │ ExploreIOFragment.java
│ │ ExploreModel.java
│ │ ...
│ │
│ └─data
│ ItemGroup.java
│ LiveStreamData.java
│ MessageData.java
│ ...
│
├─feedback
│ FeedbackApiHelper.java
│ FeedbackConstants.java
│ FeedbackHelper.java
│ ...
│
├─framework
│ FragmentListener.java
│ LoaderIdlingResource.java
│ Model.java
│ ...インターフェースの定義と実装
│
├─gcm
│ │ GCMCommand.java
│ │ GCMIntentService.java
│ │ GCMRedirectedBroadcastReceiver.java
│ │ ...
│ │
│ └─command
│ AnnouncementCommand.java
│ NotificationCommand.java
│ SyncCommand.java
│ ...
│
├─io
│ │ BlocksHandler.java
│ │ HandlerException.java
│ │ HashtagsHandler.java
│ │ ...modelの処理
│ │
│ ├─map
│ │ └─model
│ │ MapData.java
│ │ Marker.java
│ │ Tile.java
│ │
│ └─model
│ Block.java
│ DataManifest.java
│ Hashtag.java
│ ...
│
├─map
│ │ InlineInfoFragment.java
│ │ MapActivity.java
│ │ MapFragment.java
│ │ ...
│ │
│ └─util
│ CachedTileProvider.java
│ MarkerLoadingTask.java
│ MarkerModel.java
│ ...
│
├─model
│ ScheduleHelper.java
│ ScheduleItem.java
│ ScheduleItemHelper.java
│ ...modelの定義とmodel関連操作の実装
│
├─myschedule
│ MyScheduleActivity.java
│ MyScheduleAdapter.java
│ MyScheduleFragment.java
│ ...
│
├─provider
│ ScheduleContract.java
│ ScheduleContractHelper.java
│ ScheduleDatabase.java
│ ...ContentProviderの実装
│ (providerが依存する他のクラス、例えばdb操作などもここで定義)
│
├─receiver
│ SessionAlarmReceiver.java
│
├─service
│ DataBootstrapService.java
│ SessionAlarmService.java
│ SessionCalendarService.java
│
├─session
│ SessionDetailActivity.java
│ SessionDetailConstants.java
│ SessionDetailFragment.java
│ ...
│
├─settings
│ ConfMessageCardUtils.java
│ SettingsActivity.java
│ SettingsUtils.java
│
├─social
│ SocialActivity.java
│ SocialFragment.java
│ SocialModel.java
│
├─sync
│ │ ConferenceDataHandler.java
│ │ RemoteConferenceDataFetcher.java
│ │ SyncAdapter.java
│ │ ...
│ │
│ └─userdata
│ │ AbstractUserDataSyncHelper.java
│ │ OnSuccessListener.java
│ │ UserAction.java
│ │ ...
│ │
│ ├─gms
│ │ DriveHelper.java
│ │ GMSUserDataSyncHelper.java
│ │
│ └─util
│ UserActionHelper.java
│ UserDataHelper.java
│
├─ui
│ │ BaseActivity.java
│ │ CheckableLinearLayout.java
│ │ SearchActivity.java
│ │ ...BaseActivityおよびカスタムUIコンポーネント
│ │
│ └─widget
│ AspectRatioView.java
│ BakedBezierInterpolator.java
│ BezelImageView.java
│ ...カスタムの小さなUIコントロール
│
├─util
│ AboutUtils.java
│ AccountUtils.java
│ AnalyticsHelper.java
│ ...ユーティリティクラス、静的メソッドを提供
│
├─videolibrary
│ VideoLibraryActivity.java
│ VideoLibraryFilteredActivity.java
│ VideoLibraryFilteredFragment.java
│ ...
│
└─welcome
AccountFragment.java
AttendingFragment.java
ConductFragment.java
...
上記では、機能(feature)にちなんで名付けられたパッケージ以外にも、レイヤー(layer)に似たパッケージが定義されています:
-
framework
-
io
-
model
-
provider
-
receiver
-
service
-
ui
-
util
PBLの一般的な規範と比較すると、以下のようになります:
| PBLの一般的な規範 | Google I/O 2015 |
|---|---|
| activities(画面で使用されるActivityクラス) | feature/ |
| base(各Activityクラスで共有されるものをBaseActivityクラスとしてまとめる) | ui |
| adapter(画面で使用されるAdapterクラス) | 汎用的なものはuiに、そうでないものはfeature/に |
| tools(共通のユーティリティメソッドクラス) | util |
| bean/unity(要素クラス) | model |
| db(データベース操作クラス) | データベース操作はproviderに、データ処理(json解析など)はioに |
| view/ui(カスタムViewクラス) | ui |
| service(Serviceサービス) | service |
| broadcast(Broadcastサービス) | feature/ |
4. まとめ
Google I/O 2015のコード構造を参考にすると、PBFは具体的に以下のように実践できます:
src
└─com
└─domain
└─app
│ Config.java 設定データ、定数
│
├─framework
│ インターフェースおよび関連する基底クラスの定義
│
├─io
│ データ定義(model)、データ操作(json解析など、ただしdb操作は除く)
│
├─model
│ modelの定義(データ構造およびgetter/setter、compareTo、equalsなど。複雑な操作は含まない)
│ およびmodelHelper(model操作に便利なapiを提供)
│
├─provider
│ ContentProviderおよびそれが依存するdb操作の実装
│
├─receiver
│ Receiverの実装
│
├─service
│ Service(IntentServiceなど)の実装。別スレッドで非同期に処理を行うために使用
│
├─ui
│ BaseActivity、およびカスタムviewやwidgetの実装。関連するAdapterもここに配置
│
├─util
│ ユーティリティクラスの実装、静的メソッドを提供
│
├─feature1
│ Item.java modelの定義
│ ItemHelper.java modelHelperの実装
│ feature1Activity.java UIの定義
│ feature1DAO.java プライベートなdb操作
│ feature1Utils.java プライベートなユーティリティ関数
│ ...その他のプライベートクラス
│
├─...その他のfeature
もちろん、このようなコード構成案はAndroidに限らず適用可能です。使ってみて良いと感じるものを採用すれば良いでしょう。
コメントはまだありません