⏮️

シフトレフトがなぜ効果的なのか「抽象度」から考える

2023/12/18に公開

はじめに

ログラスの龍島(@hryushm)です。
ソフトウェア開発において、「シフトレフト」すなわち開発の早い段階でテスト計画を立て、実施していくことが全体的なコスト削減や価値提供の早期化につながるとよく言われています。
この記事では、シフトレフトによってもたらされる効果をログラスでの実例を用いて紹介した上で、なぜ効果が出るのか?を「抽象度」というキーワードから紐解いてみようと思います。

本記事ではスクラム開発においてPBIを完了させる中でシフトレフトしていくことを念頭に書いていきますが、ソフトウェア開発の任意のタイミングにおいて適用できる概念だと考えています。

テスト設計を実装前にやることの有用性

まずシフトレフトによって何が起こるのか?を考えます。PBIに書かれた受け入れ基準(≒仕様)を満たすようプロダクトに実装する際、実際にプログラムを実装する前にテスト設計を行いテストケースをつくることで下記のような効果が得られます。

仕様の確認と明確化

テスト設計を実装前に行うことで、仕様の不明瞭さや考慮漏れを初期段階で発見することができます。これは、開発者が実装に着手する前に、仕様を完全に理解していることを確認するためのチェックポイントとして機能します。具体的なテストケースを考える過程で、予期しない問題や追加すべき機能が明らかになることもあります。

開発プロセスの効率化

早い段階で問題を特定、認識し、それに対応することで、後の段階での時間を節約し、手戻りを減らすことができます。結果として、短いスプリントサイクル内での迅速な機能実装が可能となり、ユーザーに素早く価値提供することができます。

コードの品質向上

テスト設計を先に行うことで、その機能が満たすべき仕様が明確になり、開発者はプログラムの設計をしやすくなります。コードの品質、保守性が向上することで継続的な開発の効率が上がります。これは、特に複雑なビジネスロジックやシステム要件を扱う場合に非常に有効です。

実際の例

ログラスの開発においてシフトレフトがうまく機能した例を紹介します。ユーザーが既存のイベントの集合に対してイベントを追加する際の制御について、シフトレフトでテストケースのパターン出しを行っていると、このような表と図ができました。


※プロダクトの詳細に関わるためかなりぼかしが入ってしまっています

一旦整理することはできましたが、状態を制限するルールが多くあり、パターンとして25種類、かなり複雑な仕様になっていることがぼかし越しにも伺えると思います。
この整理の過程と結果の複雑さから「現在既に複雑な機能に対して追加でこれを本当に実装するのか?」「不必要に仕様を難しくしてしまっていないか?」といった会話が生まれ、結果的に少しの仕様変更をすることで下記のような整理をすることができました。

1つの大きな概念になっていたものを2つに分離し、それぞれ仕様をシンプルにすることでテストが必要なパターンも減らすことができ、実装のコストも大いに削減できました。(シンプルなので理解を補助する図も不要になりました)
改善前の仕様で進んでしまっていた場合、実装、テスト実施共にかなりハイコストになった上、複雑な仕様がすぐに負債として今後の継続的な開発を妨げることになったと予想されます。

このように、事前にテスト設計することで仕様に対する解像度を高め、必要に応じて仕様変更等のコミュニケーションが可能になり(仕様の確認と明確化)、仕様がシンプルになることでソースコードも単純化され(コードの品質向上)、機能開発が高速に行えることでユーザーへの価値提供を早めることができます(開発プロセスの効率化)。

テスト設計と実装の関係性を抽象度から紐解く

テスト設計と実装の関係性について「抽象度」をキーワードにもう少し深掘ることで、テストを実装よりも先に考えることがなぜ有用なのか考察してみます。

仕様を実装するとは

仕様を満たすプログラム実装するという行為は仕様の抽象度を下げて具体化すること(=抽象度の差を埋めること)だと言えます。例えば「与えられた2つの数値を割り算する」という仕様が与えられ、それを実装するとします。JavaScriptだとこのような感じでしょうか。

function divide(a, b) {
    if (b === 0) {
        throw Error("0で割ることはできません")
    }
    return a / b
}

仕様では0で割る指示が出されたときにどのような挙動をすべきかは指定されていませんが、実装上は考慮しなければなりません。これが仕様と実装の抽象度の差で、実装するとはその差を埋める作業だとわかります。すべてのインプットに対してアウトプットを定められるような完璧な仕様もありえはしますが、ソフトウェア開発においてそういったものはほぼ存在せず、仕様と実装の間には抽象度の差が生まれることがほとんどです。
特に今回のようなシンプルな関数を作るだけであれば抽象度の差は小さいですが、複雑なビジネスロジックを扱うアプリケーションであれば差が大きくなることはイメージしやすいかと思います。

テストを設計するとは

テストを設計する(テストケースを考える)も同じく、仕様を具体化する作業だと言えます。先程の「与えられた2つの数値を割り算する」という仕様だと、雑ですが下記のようなテストケースが考えられます。

ケース番号 a b 期待値
1 4 2 2
2 3 2 1.5
3 1 0 エラー

これも仕様に対して具体的なインプットがどのようなアウトプットを返すか定義しており、抽象度を下げる作業だと言えます。ポイントは取りうる全てのパターンに対するテストケースを作ることはせず、仕様を表現する具体的な例を洗い出すような作業になっているところです。

抽象度の差

実装はすべてのテストケースを満たし、テストケースは実装上あり得るパターンの一部しかカバーしないため、抽象度としては

【抽象度高】 仕様 > テストケース > 実装 【抽象度低】

という関係になります。

このことから、実装する前にテスト設計をすることは仕様から一足飛びに実装して抽象度を一気に下げるのではなく、中間の抽象度のテストケース作成を挟むことで抽象度の下げ方を段階的にしているのだと考えられます。
抽象度を一気に下げるという行為は、その差が大きいほど難易度が高く時間のかかる作業であり、間違いも生みやすいです。間のステップを作ることで誤りの発見や、そもそもの仕様へのフィードバックを早期化させることができます。
特に不確実性の高いプロダクト開発においては仕様の抽象度が高いことが多く、テストケース作成という中間のステップを作ることの効果は大きくなると考えられます。

抽象度を段階的に下げる他のアプローチ

https://levtech.jp/media/article/interview/detail_304/
ログラスではDDDが文化として根付いていますが、ドメインモデリングも仕様と実装の間を段階的にする作業だと捉えられます。複雑なドメインの仕様をドメインモデル図などを作成することで適切に実装に落とし込むことができるようになります。

まとめ

不確実性の高いプロダクト開発をする際には、仕様と実装の間に中間の抽象度のものを作るアプローチが有用です。パターン洗い出しなどのテスト設計を実装に先行して行うのはその一つで、問題点を早期に発見し、実装の質を高め、早くユーザーにプロダクトを届けることができます。
ログラスはシフトレフトに限らず、ドメインモデリングなど様々なアプローチを用いて複雑で不確実なプロダクト開発に立ち向かっています。

株式会社ログラス テックブログ

Discussion