こんにちは、WantedlyでAndroidアプリエンジニアをしている吉岡です。最近 十分な体力と筋肉があると良いコードが書けるらしいと知ったので、食生活を見直したり帰宅後に毎日40分程度運動を取り入れたりしています。成果も順調に出てますよ!
良いプロダクトを 素早く開発するための工夫 は様々あります。冒頭で挙げたとおり健康である事も例外ではないですし、先日公開した 週1リリースを実現する超高速アプリ開発フロー の記事では 見える化で共有とタスク管理を爆速化する方法 をご紹介しました。
ですが たとえタスク管理が爆速になっても、爆速なコード変更に耐えうる設計や手法を取らないとプロジェクトは破綻してしまいます。
こんな経験ありませんか? 新機能を実装するためにはコア部分の修正が必要だとわかり着手したところ、 結合している部品が多すぎて無関係なファイルまで編集 することに。やっとの思いでリリースしても、クラッシュ続発...
でもそれって実は「当たり前の事を当たり前に実践する」ことで解決出来るのではないかと思っています。何故ならより良いアプリを設計するためのノウハウは 既に語り尽くされているはず だからです。MVCやMVP、MVVMなどの設計パターン、テストコードを書くこと、オブジェクト指向の原則... これらは何故語られるのでしょう?
特にエンジニアとしてビギナーな頃は「コードを書くこと」が最優先になってしまい、その後のメンテナンスについて意識が向かない、なんて事は当たり前です。そして仮に 既存のコードがバッドプラクティスの塊だったら? ビギナーなエンジニアは それを真似する のです。バッドプラクティスが残ってしまう理由は様々ですが、参考となるものがそれしかないビギナーなエンジニアにとっては、これを真似するほか無いことになります。
私たちのチームでは爆速開発をし続けるため、"動くコード" で終わらない "良いコード" を心がけています。今回はそれらのエンジニアリング手法から3つご紹介します。ビギナーの方にもわかりやすいよう、 エンジニアリングの原則となる用語から 平易な言葉で解説していきます。
Androidアプリを例として用いますが それ以外にも応用出来る内容です。各コードスニペットに自然言語で解説も付けましたので、Androidを知らない方も参考にしてもらえればと思います。
1. 2つ以上の責務を持たせないこと
「 単一責任の原則 」という言葉を聞いたことがあるかと思います。簡単に言うと、「 1つの部品が2つ以上の責任を負ってはならない 」ということ。具体的な開発で言うならば「 複数の目的で編集される部品を作ってはならない 」というところでしょうか。
具体的に見てみましょう。
例えば Androidでリスト状のレイアウトを作り、それをタップしたら対応する画面を開きたいとします。AndroidにはRecyclerViewというリストを表示するウィジェットがあり、それに対してAdapterという コンテンツ管理とそのアイテム構築 をするクラスを使います。RecyclerView自体はアイテムを意識せずとも、Adapterに対して要求すれば表示すべきアイテムが返ってくることになります。
Adapterを実装してみます。こんなコードはどうでしょう?
具体的に何をしているかというと、リストのアイテムにクリックイベントを設定し、さらにその内容に応じて次に表示する画面を決定し、遷移させようとしています。(実際は端折っています)
一見良さそうな気がしますが、 責務分配における重大なミス を犯しています。
このコードが負おうとしている責務は、
- アイテムを管理する責務
- Viewを構築する責務
- 操作イベントに対する責務
の3つです。
この中でAdapterが実装すべきでない処理はonClickイベント、つまり操作イベントです。
Adapterが負うべき責務は「アイテムを管理し、Viewを構築すること」。表示やイベント処理など、ユーザが触れる部分の責務は任されていません。リストとして表示するウィジェット、つまりRecyclerViewが担うべきです。
このコードではアイテムがタップされた際に行うべき処理をAdapterが判断しています。例えば他の画面でも同じリストを表示したかった場合、タップされたら別の画面を表示するよう実装したいかもしれません。その場合、 このコードを流用できなくなってしまう わけです。
ではどうすべきだったでしょうか? 以下のように変えるとよさそうです。
こんなコード、見覚えがありませんか? Androidエンジニアであればよく見ますよね。
どんな変更を加えたのか解説すると、Adapterのコンストラクタに独自のリスナーを引数として追加しています。そして、以前はクリック後に画面を判定し表示まで行っていた部分を全て削除し、単純にリスナーを呼び出す形になりました。
各画面層で実装することで、 密結合な状態を避けやすくなり、再利用性が向上します 。
密結合をわかりやすく説明すると「各責務が密接に繋がっている状態」となるでしょうか。一つの画面を構成するためには「通信する」「加工する」「管理する」「表示する」などといったように様々な機能が必要になります。
それらを独立した責務として切り分け部品化することで、他の部品から参照できるようになり、さらに不具合が起きても問題の切り分けがしやすくなります。
これで「遷移先の画面を変更したい」という目的で修正されるべきファイルは画面のみになりましたね。
後述するテストコードを書く際は 密結合を避け、疎結合な状態にすべきです。
2. 依存性が低く、疎結合であること
疎結合とは「コンポーネント同士の結合が緩く、独立している」ことを指します。実装をイメージすると「実態を知らなくても利用できる」状態になります。
よくあるパターンとして、APIリクエストを処理するコンポーネントがありますね。ほとんどの場合、例えば認証用トークンをヘッダーに入れてからリクエストをする、なんてのがあるはずです。例えばこんな風に。
記述している内容はAndroid固有の機能ではなく、一般的なJavaのコードですね。
`ApiClient#auth` メソッドでユーザ名とパスワードを使い認証を試み、成功した場合は認証トークンを保存。`ApiClient#getAccountStatus`, `ApiClient#getUser` ではそれを利用しデータを取得しようとしています。
正しく実装していれば、これで認証とデータの取得が出来そうです。ApiClientのインスタンスをアプリ全体で持ち回り、各画面から呼び出すようにしました。
しかし後から「ネットが遅いからキャッシュを入れよう」「テストコードを実行する時に本番に通信されては困る」となるかもしれません。
キャッシュの場合はリロードなども絡んできますから、モード切り替えが必要そうです。新しく CacheManager なんて名前のクラスを作るとどうなるでしょう? 今まで実装したすべての画面を書き換える必要がありそうです。過程で思わぬバグを生み出してしまうかもしれません。
こうならないために、今回は Dependency Injection (DI) をしましょう。日本語訳すると、 依存性注入 です。依存を外部から入れ替えることで、実体が変わっても利用する側からは意識する必要が無くなり、疎結合な状態を作れます。
DIには様々なライブラリがあります。Androidの場合は Dagger が広く利用されています。
今回は簡単に、キャッシュ有無を切り替えられるようにしてみます。利用する際に必要なメソッドだけ切り出しました。
さきほどのApiClientはこれを実装していることになりますね。
キャッシュ処理するクラスもこれを実装しましょう。ApiClientをラップします。
新しく増えた `CacheApiClient#getCache`, `CacheApiClient#saveCache` はリクエストの結果を保存 / 取得するメソッドです。APIの仕様によっても異なりますが、ここでは期待する結果のクラスとパラメータをキーとして利用しています。
そして `IApiClient` の実装である `CacheApiClient#getUser` などのメソッドでは、キャッシュが存在するばあいはそれを、存在しない場合は `ApiClient` へインターネット経由での取得を要求し、その結果をキャッシュとして保存しています。
これを各画面からはServiceWrapper経由で呼び出すことで、キャッシュの有無を切り替え可能になります。
`ServiceWrapper#getApiClient` に与えられたフラグによって、直接取得するかキャッシュを利用するか切り替えています。
テスト用のクライアントも同様に、IApiClientを実装しダミーの結果を返すように実装すればよいでしょう。
3. テストを書くこと、書き過ぎないこと
私たちはグロースチームとして、 毎週1回リリース を実現しています。その過程では当然、既存の機能を拡張したが故に不具合が発生してしまうことも多々あります。毎回人の手だけで全ての画面を動作確認するのは酷ですし、どうしても漏れが出ます。
そのため基本的には 各画面に対して必ずUIテストを書くようにしており、リリース前には全て動作することを確認しています。
しかし、テストを書くということには膨大な時間が掛かります。よくカバレッジ、つまりコードの網羅率が重視されがちですが、 あえて私たちは100%にはしていません。
何故なら私たちの仕事は「テストを書く」ことではなく「ユーザにプロダクトを届け、良い体験を得てもらう」ことだからです。つまり本当に重要なのは、「良い体験を得てもらうために必要な機能を作る」こと。
そのため、私たちはコンバージョンに影響する箇所のテストは徹底し、それ以外は基本的操作を網羅出来る程度書くようにしています。具体的には画面が開けること、正しく通信されていること、ボタンを押し結果が反映されることなどです。
もしかしたらマーケティングなどに関わっていないエンジニアの場合「コンバージョン」という意識が薄いかもしれません。しかし私たちはエンジニアこそコンバージョンを意識すべきだと考えます。コンバージョンとは売上に繋がること、すなわちサービスを提供している側が想定する「顧客」を生み出すことです。
Wantedlyの会社訪問アプリの場合、「魅力的な仕事と出会える」「話を聞きに行く体験をできる」「会社と繋がってコミュニケーションを取れる」「ココロオドル仕事を出来る」ことがユーザから求められます。よって募集が見れなかったり、メッセージのやり取りが出来ないのは 最悪のシナリオとなります。そのためこれらの画面は徹底的にテストされています。
テストがあるおかげで、これらのコアとなる機能にも手を加えることが可能となっています。
まとめ
コードを交えながらエンジニアリングの原則をおさらいしてきました。
- 2つ以上の責務を持たせないこと
- 依存性が低く、疎結合であること
- テストを書くこと、書き過ぎないこと。
経験の豊富なエンジニアであれば当然のように実践出来るかもしれませんが、期限が迫っていたり ふと気が緩んだ時に忘れてしまいがちです。
そしてビギナーであれば、これらを常に意識することでより良いコードを書くことが出来るでしょう。
これら場数を踏むことではじめて良さがわかり、身に付く事です。
あなたはどこまで実践できていましたか? もし何か意識できていない事があるなら、明日から実践してみてください。常により良いコードを書くことは、自分のレベルを上げるだけでなく、チームのフットワークを軽くし、より楽しく仕事をする助けとなるはずです。
さいごに
最後まで読んでいただき、ありがとうございました。
私自身まだ新卒1年目で、まだまだ勉強する毎日です。良い手法や考え方をさらに吸収していきたいと思っています。
より良い方法を議論したい、相談してみたい、どんな風に開発しているか聞いてみたいという方、ぜひ気軽に遊びに来てください。