カルテット開発部の後藤(@hidenorigoto)です。今回は、カルテットでの開発プロセス寄りの取り組みと、ちょっとしたプラクティスを紹介します。
(いきなり本題を読みたい方は、見えてきたことへ飛んでください。)
プロローグ
とある企業の、とある開発現場の日常風景
プログラマー「そろそろ #3396 のプルリク、マージして頂きたいんですけどー」
レビュアー「ごめんごめん、今日見とくから!」
:
3日後の朝礼にて
プログラマー「#3396 のプルリク、見ていただけました?」
レビュアー「あー、あのプルリク、チラ見したんですけどすごく育っちゃってますよね・・・、なかなか腰を落ち着けて見る時間とれなくて 」
プログラマー「あれをマージしていただかないと、後続の作業を進められないんですよー。なんとかお願いします」
レビュアー「あー、はい(と言われても・・・)。なんとか小さいPRに分割ってできません?」
プログラマー「(今更?)えーっ、できなくはないですけど・・・ごにょごにょ(以下略)」
レビュアー「あー、では仕方ないので、なんとか今日中に見てマージしますね」
(うーん、これは細かいところ把握できないな^^; テストもまあまあ書いてあってCI通過してるし、、、えーい、ままよっ! ポチッ(Merged))
:
その日の午後
テスト担当「今日マージされた◯◯機能の動作を確認してて、〜〜のところがおかしいような気がするので仕様を確認したいのですが」
マネージャー「なるほど! そのケースは確かに考慮してませんでした。POに仕様確認してきますー」
:
1時間後
マネージャー「というわけでプログラマーさん、#3396 の機能で考慮漏れがあったので、修正お願いします 」
プログラマー「えっ、そうなるのですか! レビューしていただいているので大丈夫だと思ってました。その修正だと、ちょっと今の実装では簡単にはできなさそうですよ」
マネージャー「そうですか、、、でもこの修正を適用しないとリリース不可なので、なんとかお願いします 」
プログラマー「分かりました、、、(今更?)」
:
1日後
プログラマー「うーん、あちら立てればこちら立たず・・・ブツブツ・・」
:
1日後
プログラマー「うーん、今のまま暫定で行くこともできるけど、、、、これはいろいろリファクタリング、いや設計を考えなおさないとキツイ! キーッ」
レビュアー「ビクッッッ」
:
:
4日後
PO「◯◯機能のリリース、まだですか?」
マネージャー「す、すみません、修正に時間がかかってしまってまして」
PO「そうですか、仕方ないですね。では軽微な方の△△だけでも現場で使いたいので、先にリリースってできませんか?」
マネージャー「す、すみません、、、あのー実装上◯◯の修正といろいろ関係している部分でして、◯◯の方が完了しないと△△もリリースできないんです、何卒もう少しお待ちください 」
PO「うーん、仕方ないですね。」
:
不穏ですね・・・
「ソフトウェア開発プロジェクト健全度チェック」があったとしたら、いかにもひっかかりそうなポイントがたくさんありますね。
こんな状況、皆さんの周りで見たことないでしょうか?
こんな状況に置かれたとき、皆さんならどのように解決していきますか?
『アジャイル宣言の背後にある原則』という有名な文章があります。 ここに書かれている原則をチームメンバーが理解して実践できていたとしたら、いろいろな事が上手くいきそうに思えますよね。
しかし、、、、この原則を読んでアレコレ考えた経験がいくらあったとしても、本当に現場で実践できるレベルに「理解する」ことは難しいことだと思います。実際に私自身、上記の原則は何度も読んで知識として知ってはいました。それでも、カルテットの開発現場で起こっている問題に対してすぐに役立てることはできませんでした。
地道にチームメンバーと問題について解決策を話し合って実施し、試行錯誤していく過程で、ようやく自分たちのソフトウェア開発においては、何に注力するのが最良かが、実感と結びつきながら分かってきたように思います。結局のところ、自分が立ち向かっている問題や置かれている状況、そこから得られた経験などとつながりができる瞬間がないと、本当の理解は得られないということなのでしょうね。
こんな風にして苦労しながらも得た、しかも、読み返してみればわりとありきたりに見えてしまう、私たちなりのルールというのを紹介します。
前提としている環境
カルテット開発部で開発しているのは、自社サービスであるLisketです。3年育てたSymfonyプロジェクトの現状で紹介したように、そこそこのサイズのコードベースに成長しています。この開発の特徴は、次のようになります。
- 自社の業務でも利用しており、サービスの改善が自社の業務効率に直結している
- 社内ユーザーからの要望で、機能の改善・追加を行う頻度が高い
- 業務に直結しているため、当然だが高い可用性が求められる
- 常に成長させ続ける開発(機能面、規模面)
一般ユーザー向けに提供しているサービスですが、それを最もヘビーに利用しているのが自社、という構造になっています。この構造は開発にも活かされていて、例えば、新規または追加実装された機能は、すべて一旦社内ユーザーのみが利用する段階を設け、そこで一定の品質が担保された機能を順次一般ユーザーへ提供するというリリースステップを踏んでいます。
Lisketの開発当初は、以降で紹介するようなプラクティスは意識的には実施していませんでした。そのためか、特に大きめの機能のリリースタイミングの周辺で、それに引きづられて他の修正のリリースがずれ込んだり、多くの手戻りが発生するといったことが何度か発生していました。せっかく自社にヘビーユーザー/ベータテスターがたくさんいるのに、それを有効活用してスムーズに高品質なソフトウェアをリリースしていくというプロセスにおいて、私たちは未熟でした。
気づき
何度か失敗するうちに、1つの事にあらためて気づきました。それは、「自社ユーザーでベータテストをし、実務に耐えうる品質が確認できたものを一般リリースする」ことが可能なのが私たちカルテット開発部の強みであるということです。だからこそ、自社ユーザーでベータテストをしつつスムーズにリリースしていくという開発サイクルは、第一に上手く回さなければならないポイントだということです。これを土台に位置づけてプロセスや判断基準を整備すれば上手くいきそうです。そうして、いくつかのプラクティスを実施していくうちに、私たちの活動の基準が見えてきました。
見えてきたこと
- 明示的で小さなソフトウェア開発
- 素早いリリースサイクル
明示的で小さなソフトウェア開発とは、ソフトウェア開発のさまざまなアクティビティにおいて、対象、関連、影響範囲が明示的に小さくあるように選択・設計・行動することを表します。
私たちが関わっているのは、解決しなければならない問題が次々に生まれるビジネス現場です。そのような現場で使われるソフトウェアは、大きく複雑になる宿命を避けられません。その宿命は受け入れ、しかし、決して人海戦術に頼らず、最適なチームメンバーで効率的にソフトウェアを開発し続けるための戦略として、明示的な小ささを選択していくようにします。
また、明示的な小ささと同時に、素早いリリースサイクルを維持することも常に念頭に置くようにします。
この2つは、互いに影響しあっています(カルテットの現場ではそのように機能しています)。明示的で小さなソフトウェア開発を実践することで、リリースサイクルを素早く回し続けられます。逆に、リリースサイクルが素早く回る状態を維持することで、明示的で小さなソフトウェア開発を実施することを促進できます。
この2つを基本としてさまざまな判断基準に組み込むことで、カルテットの開発プロセスは次第に上手く回るよう改善しました。
次に、2つの基本を適用した3つのプラクティスを、例とともに紹介します。3つのプラクティスは、
- 明示的で小さな変更
- 明示的で小さな範囲
- 素早いリリースサイクル
です。
明示的で小さな変更
大きな変更が必要な場合でも小さな変更にブレークダウンし、1つ1つの変更の影響範囲や後方互換性が明示的であるようにします。
取り組み例:マージしやすいプルリクエストのプランニング
状況
新機能の実装や既存機能の修正の際、それによって動作が大きく変わってしまうとしたら、それをリリースするための準備などさまざまな点で労力も時間もかかります。そのような修正を1つのプルリクエストで実装すると、そのプルリクエストはなかなかマージできず、かつ、細かな修正などでプルリクエストが成長していくこともあると思います。プルリクエストが成長してmasterからの差分が大きくなると、変更インパクトが大きくなって、結局なかなかマージできないという悪循環に陥ります。
解決策
この悪循環を解消するために、次のようにプルリクエストを小さな単位で作成して、細かくマージしていけるようにします。
- 既存機能の動作に影響がない、新規のコード(クラス、メソッド)の追加のみのプルリクエストを先に作るようする。
- 影響範囲がある程度の大きさになるデータ構造の変更が必要な場合、次のように段階を踏む。
- 新しいデータ構造と古いデータ構造が共存する状態にする(プログラムの修正なし)。
- 新しいデータ構造から、古いデータ構造をエミュレートできるようにする(エミュレートの追加のみで、他のプログラムの変更なし)。
- 新しいデータ構造側にプログラムを対応させる。
- 古いデータ構造を削除する。
- 既存機能に大きな修正が必要な場合は、共通インターフェイスで別バージョンのクラス群を用意する。共通のインターフェイスかつ入出力の後方互換を維持した状態で開発する(旧コードには @deprecated マーク)。
- 可能であれば機能内部の整理も行い、新バージョン側でも再利用できる部分を取り出す(リファクタリングのプルリクエスト)。
- 新バージョン側のクラス群を実装するプルリクエストを作成する。この段階では既存機能に影響なし。
- 何らかの条件(特定のユーザー、フィーチャートグル等)によって、新バージョンの機能側を実行できるようにする。
- 安定動作を確認できた後に新バージョンに切り替え、旧コードの削除を行うプルリクエストを作成する。
- UIレベルでも追加機能を非公開/限定公開の状態でリリースできるように、フィーチャートグルを使う。
このようにプルリクエストをステップに分けて作成するためには、実装タスクごとに、単に「どうあるべきか」という内容を明確にするだけでは十分ではありません。そこからさらに、「どのようにマージし、リリースしていくのか」「どの部分が既存機能とバッティングするのか、しないのか」といったことを考え、計画してから実装に入る必要があります。
リリースまでを意識するというのは、計画が大変に思えるかもしれません。しかし、そのようなことを考えるメリットの方が遥かに大きいと思います。
- どのようにリリースしていくのかというのは、開発のどこかで必ず考えなくてはいけないことであり、それを早期に考えることになる。
- 1つ1つのプルリクエストが小さくなることで、内容に自信が持てるようになる。安心して変更できるようになる。
- 実装担当者がリリースのことまで意識するようになる。プロダクトに対する当事者意識の醸成。
ただし、最初からプルリクエストの分割のことばかりを考えすぎると、触っている部分のコード全体としてのバランスに作業者の目が届きづらくなる場合もあります。そのような広い視野での設計の見直し等は、別途時間を設けて取り組むなどできるとよさそうですね。
明示的で小さな範囲
次に挙げるような範囲を小さくし、かつ、その範囲が明示的であるようにします。
- 作業対象の範囲
- プログラムのパラメータや戻り値の範囲(定義域=ドメイン、値域)
- より狭い値の範囲
- クラスよりもインターフェイス
- データの利用範囲
取り組み例:エンティティを小さくする設計
状況
Lisketの機能には、複数の機能に渡ってよく似た形のデータを扱うケースがいくつかあります。
データの形が似ているからといって、共通化のためにテーブルやエンティティクラスを1つにまとめてしまうと、(複雑度にもよりますが)通常はコード量が減らせるため最初の実装コストは下がりますが、後々次のような事が発生します。
- 機能ごとに微妙に異なるデータ要件が出てくる。
- データ要件の差異に応じて、機能ごとの差が大きくなる。
それぞれの機能はどんどん成長していきます。共通したテーブルやエンティティクラスを使っていると、そのテーブル/エンティティクラスの変更に対して、影響する範囲が次第に大きくなっていきます。そのようなクラスに対する修正は慎重にならざるをえず、単純な面ではリリースサイクルの速度を低下させてることにつながります。また、場合によっては「そこを変更するよりも別の箇所で安易に対応」といった、判断の歪みにつながることも考えられます。
解決策
それぞれの機能の利用目的やライフサイクルが異なっている場合は、たとえデータの形がほとんど同じでも、別々のエンティティ(テーブル)に分けるようにします。これには、次のようなメリットがあります。
- テーブルのスキーマ変更の影響を、個別の機能単位に絞りこめる。
- 似て非なる複数の機能をすべて調整して一度にリリースするのではなく、機能ごとに修正作業を完結できる。
取り組み例:プログラミングにおいて型を利用
この取り組みは、まだまだ初期段階です。
Lisketでは、サーバーサイドはPHP、フロントエンドはJavaScriptを使っています。PHPは静的型付け言語ではありませんが、型情報のための機能が徐々に強化されてきています。まだ導入には至っていませんがPHP7以降で利用できる静的解析ツールPhanの利用を検証中です。今後はより積極的に型を使って、メソッドの引数の定義域の絞り込みなどに取り組んでいく計画です。稼働環境のPHPは現在バージョン7.0系なので、7.1以降に上げた後、戻り値の型宣言(nullableな戻り値も指定できるようになる)も含めて取り組んでいきたいです。
フロントエンドでは一部新規機能の開発にTypeScriptの利用を開始しています。既存のJavaScriptのコードにもJSDocの記述を追加するようにしています。これにより、IDEで補完を利用したり、将来的にコンパイラによるチェックを通すことも視野に入れています(ただし基本路線としてはTypeScriptへの移行を優先に考えています)。
取り組み例:連想配列撲滅運動
先の「型を利用」と似ていますが、特にPHPにおいて、連想配列の利用を減らすように随時リファクタリングを行っています。指針としては、クラスのpublicメソッドの引数や戻り値で受け渡しされるものに連想配列が使われていたら、連想配列の構造をオブジェクトでの表現に書き直すようにしています。利用がprivateメソッド、つまり特定のクラスの内部表現に留まる場合は問題としません。
素早いリリースサイクル
リリースを素早く行えるということを、常に判断基準に組み込むようにします。
取り組み例:Symfonyのユーザーロールを使ったシンプルなフィーチャートグル
プロダクトでフィーチャートグルを実現するためには、最も安直には定数やif文を使うことになりますが、ある程度ポリシーを持っておかないと、フィーチャーのための分岐を管理できなくなってしまいます。 フィーチャートグルのためのサードパーティのサービスとしてLaunchDarklyなどが有名です。ただしLisketで利用したい形態とは合わなかったため利用していません。その代わり、Symfonyのユーザーロールを利用したシンプルな形でフィーチャートグルを行っています。
Lisketの場合、機能の公開は3ステップで、それぞれのロール構造と合わせて次のようになっています。
- (A) Lisketの管理者権限ユーザー向けに公開
- 対象ユーザーのロールは
ROLE_ADMIN
(ROLE_QUARTET
とROLE_USER
を含む)
- 対象ユーザーのロールは
- (B) カルテットの社員向けに公開
- 対象ユーザーのロールは
ROLE_QUARTET
(ROLE_USER
を含む)
- 対象ユーザーのロールは
- (C) 一般ユーザー向けに公開
- 対象ユーザーのロールは
ROLE_USER
- 対象ユーザーのロールは
機能は最初 ROLE_ADMIN
または ROLE_QUARTET
向けにのみ公開し、その段階での検証を経て ROLE_USER
向けへの公開、つまり一般公開となります。例えば「HOGE」という機能を開発しているとすると、これをロール ROLE_FEATURE_HOGE
で表し、公開段階に応じて ROLE_ADMIN
または ROLE_QUARTET
配下に所属させます。アプリケーションのコンフィギュレーションファイルの書き換えによる適用になるため、カルテットの場合はこのファイルを直接書き換えることはしておらず、基本的にはすべてプルリクエストによる変更になり、アプリケーションのリリースデプロイによって反映する形です。
(A) の場合の security.yml
role_hierarchy:
ROLE_USER: ROLE_MANAGED_CUSTOMER
ROLE_QUARTET:
- ROLE_USER
ROLE_ADMIN:
- ROLE_ALLOWED_TO_SWITCH
- ROLE_USER
- ROLE_QUARTET
- ROLE_FEATURE_HOGE <-- ココにフィーチャーのロールを記述
(B) の場合の security.yml
role_hierarchy:
ROLE_USER: ROLE_MANAGED_CUSTOMER
ROLE_QUARTET:
- ROLE_USER
- ROLE_FEATURE_HOGE <-- ココへフィーチャーのロールを移動
ROLE_ADMIN:
- ROLE_ALLOWED_TO_SWITCH
- ROLE_USER
- ROLE_QUARTET
(C) の一般公開の場合は、フィーチャートグルの分岐コードを削除しています。
Twigのテンプレートでは、機能の公開段階を問わず、次のようにロールをチェックして条件に置き換えてJavaScriptで使う等しています。
var featureHogeEnabled = {% if is_granted("ROLE_FEATURE_HOGE") %}true{% else %}false{% endif %};
エピローグ
定例ミーティングにて
PO「△△の機能の動作確認は、来週火曜からでよい?」
マネージャー「その予定ですね。来週月曜のリリースで社内ユーザー向けに公開されますので、火曜から確認をお願いします。この機能ですと確認は3日くらいでしょうか?」
PO「そうだね、いや2日くらいでできるよ。多分問題ないと思うので、翌週のリリースで一般公開だね。」
マネージャー「OKです。」
プログラマー「この機能の実装ですが、先にこの部分をリファクタリングしておきたいのですけどー」
レビュアー「なるほど そうしておくとたしかにスッキリしますね 」
プログラマー「はい!多少インターフェイスの変更も行いたいので、ここでしか使っていない既存のクラスにはレガシーマークをつけるいつものやり方で進めますね 」
レビュアー「よろしく!」
司会「他に何か、コメントなどある方どうぞ」
プログラマー「最近、レビュアーさんのレビューの進み具合が良いんですよ」
レビュアー「プログラマーさんが、上手にプルリク作ってくれてるからですよ」
プログラマー「てへっ あ、でも、要件の段階でいろいろ考慮していただいているので、その分私はプルリクの作り方を考える余裕ができた気がしますー」
マネージャー「それは良かった 」
PO「みんな、よくやってくれてるね 先月リリースした機能もユーザーさんに好評だよ! そこで次の要望、いや今後1年くらいかけて作りたい機能の構想なんだけど(以下略」