こんにちは、ネイティブアプリエンジニアリングマネジャーの頼です。
2018年アプリチームの立ち上げから早1年が経ちました。手探りの中始まったオープンワークアプリの開発ですが、ファーストリリースを乗り越えようやく安定した開発体制が整いつつあるのを感じています。
さて、いまでこそネイティブアプリディビジョンに所属していますが、実は2015年の入社以降の3年間はwebアプリエンジニアとしてサーバーサイド、フロントエンドの開発を行ってきました。いまでもレビュワーとしてアプリで使うAPIのコードレビューで関わっていますが、気がつけば初期のサーバーサイド開発を知る数少ないひとりになってしまいました。
そこで、このエントリでは普段語られることのないオープンワークサーバーサイドの変遷と、webアプリケーションエンジニアとして取り組んできたことについて主観で振り返ってみたいと思います。
Symfony導入
私が入社した2015年当初、エンジニアはアルバイト含め7名程度で渋谷ヒカリエのコワーキングスペースの一室で開発を行っていました。まだVorkersを名乗っていた弊サービスは全て協力会社が独自フレームワークで開発しており、これを内製化すべく半年以上かけてSymfonyへのマイグレーションを行っている真っ只中でした。この移行期間中は新しい機能開発はストップしていて、不具合修正は移行前と移行後両方のソースコードに反映するといった地道な作業の繰り返しでした。 このSymfonyマイグレーションをリリースする時は社長を含め全ての社員で動作確認を行い、まさに全社員が一丸となった大仕事に深い感慨を覚えました。
今では当たり前のように存在しているSymfonyベースのソースコードですが導入には膨大な時間と労力を必要としました。 しかし独自フレームワークを捨て洗練されたOSSなMVCフレームワークを採用することで
- ある程度均質なコードが書くことができる
- サードパーティのライブラリ利用で再発明が不要になる
- 他のフレームワーク利用者はキャッチアップが容易
- キャリアになるので採用もしやすい
などサービスの拡大とスピードアップに向けた内製化のためには重要で必要な決断だったと思います。
Symfony導入後
ストップしていた新機能リリースが解禁され、SNS連携や企業分析、求人検索、リクルーティングなどさまざまな機能がリリースされていきました。ただ新機能を開発するだけでなくABテストによるUX検証も行われるようになり、データドリブンでユーザー体験を追求しはじめた時期でもあります。 しかし、スピードが求められ、メンバーが増える中秩序なく増えていくコードは次第に課題が山積し始めました。ただMVCフレームワークにおんぶにだっこされるだけでは限界がきてしまいます。
そこで、問題を解決すべくシンプルで小さな設計と品質維持の仕組みを導入していきました。
Fat Controller
MVCで誰もが一度はまるアンチパターンではないでしょうか。 当時はこれを緩和するために「Entityが主体となるロジックはEntity内に書く」というところから始めました。 EntityはDoctrineのDBスキーマ定義に使われるデータクラスでデータ設計の段階で必ず作られるクラスの1つです。
例えば「"クチコミ閲覧権限のない"、"社会人"ユーザーにweb履歴書への登録を促してプレミアム会員化したい」という要件がありDMを送信したり、ある画面でweb履歴書登録への動線を表示したいとします。このとき判定ロジックはControllerではなくUserEntityのメソッドに実装し、メール送信可否やUIの動線表示などの判断に使います。
class User { public function isRecommendResume(): bool { return !$this->isPremium() // 閲覧権限がない && !$this->isGraduated() // 社会人 }
- すでにあるデータクラスに振る舞いを追加するだけ
- Entity主体の処理が結構ある
というコスパのよさから少しずつControllerにあった処理はEntityに書かれるようになりました。 また徐々にですがEntity以外のモデルクラスも作られるようになりFat Controllerの解消に役立っています。
ただこれだけでは不十分で下記のような課題が残りました。
- Entity以外のモデルの抽出の方針がない
- 複数のオブジェクトが関わる処理を表現できない
Smart UI
オープンワークではテンプレートエンジンにtwigを使っています。 twigファイルはデザイナも共同で作業するのでルールの統一が難しい側面もあり、表示ロジックが直接書かれることが多くあります。レイアウトとロジックがからまり無残なパスタになったtwigを前に絶望することが何度もありました。
前述のようにEntityなどのモデルに処理を移すことでこれは一部解消できますが、表示に強く結びつけて考えられるコードはtwigに直書きしたり、Controllerに書いてtwigに渡されていました。
- 性別や状態を示すタイプコードの文字列表現(例:status = 4 => "登録済み")
- 表示要件に合わせて複数のプロパティを組み合わせる(フルネームなど)
- 配列の連結文字列表現
これらの低レイヤーの表示ロジックの多くはEntityに1対1で紐づく情報になります。 そこでEntityと1対1で紐づくViewHelperというクラスを定義し、表示ロジックをまとめることにしました。 これもEntityを定義した時点で設計が決まるのでチームに展開しやすく多くの表示ロジックを共通の方法でまとめることができました。
一方、こちらも複数のオブジェクトを組み合わせるような複雑な表示ロジックの置き場がなくViewやControllerに書かれやすくなるといった課題も残りました。
外部データへのアクセス
データベースのような外部データへのアクセスは一般に複雑になりがちです。
- ファイルやDBなどリソースへのアクセス情報の管理
- プリミティブからモデルクラスへのマッピング
- APIなど外部サービスへの依存
DBへのアクセスはDoctrineが隠蔽してくれているのですが、TreasureDataやサードパーティAPI、環境変数へのアクセスなど、外部データへのアクセス部分を抽象化することで
- ドメインとは無関係なデータ変換にまつわる複雑さを隠蔽する
- 外部サービスや実行環境に依存することなく自動テストすることができる
という効果を得られることができました。
外部品質
Symfony導入当初は機能テストとしてControllerのActionに対するテストを書いていました。 しかし
- JavaScriptを含むフロントエンドのテストができない
- Dom操作が複雑で保守が困難
といった課題があり、E2EテストとしてSelenium IDEを使い始めました。 Selenium IDEはGUIで簡単にテストケースが作れる一方
- 操作を再利用できない
- テストケースがコードで記述できない
- ローカルマシンのブラウザで実行する必要がある
といった問題があることがわかりました。
最終的にSelenium WebDriverを採用し解決に至りました。
- Dockerコンテナにブラウザ環境を構築しLinuxサーバー上でE2Eテストを実行可能
- PHPUnitから WebDriverを操作するのでソース管理できる
- PageObjectパターンを導入して可読性、再利用性の高いテストコードを作成
といったメリットが得られE2E自動テストの生産性を飛躍的に改善できました。
現在
オープンワークでは8つの開発プロジェクトが並走しており30人程のエンジニアでこれらを支えています。アプリ開発がはじまりwebと共通だったりちょっと違ったり複雑な仕様を実現するような要件も生まれています。さらなる複雑さに対抗し開発をスケールするためにオープンワークではwebチームのテックリードを中心にクリーンアーキテクチャの導入に取り組み始めました。
日々是改善、道半ばですが
- モデルの操作をユースケースにまとめ複数のプロジェクト間で共有できた
- ViewModelとPresenterを導入し複数のオブジェクトを使用した複雑な表示ロジックをtwigから分離できた
- モックしたRepositoryをDIすることでUseCaseをユニットテストできるようになった
といった恩恵がすでに得られており、これまでの設計上の課題を解決してくれていると感じています。 また、レイヤーを分けることでモデル抽出しやすくなりユニットテストできるコードが増えたという意味でも、内部品質の改善にも一役買っています。
さいごに
これまでただ動くコードを書くだけでなくチームとして生産性を上げるにはどうすればよいかを考えながら開発を行ってきました。 ファウリアン(ファウラー信者)として設計を取り入れることでスケールするようなアプローチをとってきたわけですが、少しでもチームの生産性に良い影響を与えられていればと願います。 また裁量が大きく模索しながらでもいろいろな課題に取り組める環境があったことは本当によかったと思います。
今後もオープンワークはサービスも組織もさらに成長していきます。 プロジェクト管理やナレッジの共有、採用などコーディング以外の課題も出てきていますが、これまでと変わらず地に足を付け愚直に、エンジニアとしてサービスを支え牽引すべくチャレンジしていきたいと思います。