これはなに
以下のurlで公開されている記事の日本語訳です。
OCP vs YAGNI - Enterprisecraftmanship.com
本文
今回は、「OCP vs YAGNI」というテーマで、「Open/Closed Principle」と「You aren't gonna need it one」の間の矛盾を取り上げたいと思います。
OCP
まず、OCPとは何かということを再確認することから始めましょう。Open/Closedの原則では、次のように述べられています:
ソフトウェアの実体(クラス、モジュール、関数など)は、拡張に対してはオープンであるべきだが、変更に対してはクローズであるべきである。
これは、バートランド・マイヤーが「Object-Oriented Software Constructionbook」という有名な本の中で初めて紹介したものです。その後、ボブ・マーティンがSOLIDの原則を紹介した際に広く知られるようになりました。
公式の定義は非常に曖昧で、根本的な意味を把握するのに役立っていません。そこで、この原則をより深く掘り下げてみましょう。
現在では、バートランド・マイヤーの解釈とボブ・マーティンの解釈の2つがあります。
ボブ・マーティンの解釈は、「波及効果の回避」に集約されます。つまり、あるコードを変更したときに、その変更に対応するためにコードベース全体に変更を加える必要がないようにすることです。理想的には、既存のコードに何も変更を加えることなく、新しい機能を追加することができるようにすることです。この原則では、修正のために元のモジュール(クラス、メソッドなど)を閉じ、代わりにその中に拡張ポイントを開くことを勧めています。この拡張ポイントによって、既存のコードベースを変更することなく、新しい機能を導入することができるようになります。
これは通常、ポリモーフィズムを使って実装されます。例えば、次の例はボブ・マーティン版のOCPに違反しています:
public void Draw(Shape shape)
{
switch (shape.Type)
{
case ShapeType.Circle:
DrawCircle(shape);
break;
case ShapeType.Square:
DrawSquare(shape);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
ここで、新しいshapeを導入するためには、Drawメソッドを修正する必要があります。新しいshapeを追加すると、既存のコードベースに影響が出、上記のswitch文を修正する必要が出てくるということです。
これを解決するには、Shapeの抽象クラスを作成し、そのサブクラスに描画ロジックを移動させる方法があります:
public abstract class Shape
{
public abstract void Draw();
}
public class Circle : Shape
{
public override void Draw()
{
/* … */
}
}
/* etc. */
こうすれば、新しいShapeを追加する必要がある場合は、サブクラスを作成し、Drawメソッドをオーバーライドするだけで済みます。OCPの用語を使うと、Shapeクラスを閉じて変更できるようにし、その中に拡張ポイントを開いたことになります。この拡張ポイントは、既存のコードを一切変更することなく、新しい機能を作成するために使用することができます。
この原則の背後にあるバートランド・マイヤーの本来の意図は異なっています。ボブ・マーティンの解釈が変更量を減らすことを目的としているのに対し、バートランド・マイヤーは後方互換性について述べています。
相互に依存する複数のモジュールがあり、それぞれが別のプログラマーチームによって開発されている場合、これをうまく機能させるためには、何らかのプロセスに従う必要があります。好きなときに好きなだけモジュールを変更できるわけではなく、そのモジュールのクライアントを考慮する必要があります。
例えば、メソッドを公開するライブラリがあるとします。
CreateCustomer(string email)
次のようにいきなり新しい必須パラメータを追加することはできません:
CreateCustomer(string email, string bankAccountNumber)
これは、すでにオリジナルバージョンのメソッドにバインドしているクライアントコードにとっては、破壊的な変更となります。
これがバートランド・マイヤーがOCPの原則で解決しようとしていた問題です。開発中のモジュールは、まだ誰もそれに縛られていないため、改変の余地があります。しかし、いったん公開したら、既存のクライアントと常に互換性を保てるように、そのAPIを最終的にクローズしなければなりません。もし、公開後に変更を加える必要がある場合は、新しいモジュールを作成します。
なお、バートランド・マイヤーは、ここで特にAPIについて述べており、モジュールの実際の実装について述べているわけではありません。モジュールのAPIを変更しないのであれば、実装を変更することは可能です。つまり、バグフィックスや破壊的でない変更はOKですが、メソッドのシグネチャを変更したり、新しい前提条件を要求したりするのはNGです。
以下は、APIを構成するものの全リストです:
- メソッドのシグネチャー:
- 名前、パラメータ、戻り値。
- 前提条件:
- メソッドを使用する前にクライアントが満たすべき要件のリスト。例えば、emailstringパラメータを特定の方法で形成することが要求されます。
- 後条件:
- モジュールが行う保証のリスト。例としては、新しく作成された顧客に挨拶メールを送るという約束がある。
- 不変条件:
- 常に真でなければならない条件のリスト。
メソッドのシグネチャを変更したり、前提条件を強化したり、後条件を弱めたり、不変量を修正したりすると、結果的に変更を壊すことになる。
- 常に真でなければならない条件のリスト。
Meyerがこの原則を書いた当時は、互換性の種類はバイナリ互換性だけでした。つまり、2つのライブラリがあり、一方のライブラリが2つ目のライブラリを使用する必要がある場合に、再コンパイルやブレークチェンジに対処することなく、2つ目のライブラリを使用できるようにすることです。しかし、この原則は現代でも適用することができます。
Web APIのバージョン管理の話題は、基本的にマイヤーのOCPの原則を大規模なプロジェクトに適用したものです。例えば、Microservice 2に依存するMicroservice 1があったとして、Microservice 2に壊れるような変更を加えることはできませんし、そのような変更のためにAPIはクローズされるべきです。しかし、新しいバージョンを作り、既存のクライアントに古いバージョンのままか、新しいバージョンに切り替えるかの選択肢を提供することはできます。
もう1つ重要な点は、マイヤー版OCPが意味を持つのは、複数の開発者チームが存在し、各モジュールが異なるチームによって開発されている場合だけだということです。一般的な企業向けソフトウェアでは、あなたはコードの作者とクライアントの両方であるため、このような複雑な慣習に従う必要はありません。コードを閉じる必要はないのです。なぜなら、あなたが導入する可能性のある変更に対処するために必要なものはすべて揃っているからです。モジュールやライブラリ、サービスを公開し、他のチームが利用できるようにしたときだけ、そのAPIを本当にクローズする必要があります。そうでなければ、変更点を修正することは問題ではありません。詳しくはこちらで書いています:
共有ライブラリ vs エンタープライズ開発
つまり、OCPの2つのバリエーションは、同じ名前であるにもかかわらず、その根本的な意図が異なるのです。このことは、OCPとYAGNIの議論をする上で重要です。先ほど持ってきたコード:
public void Draw(Shape shape)
{
switch (shape.Type)
{
case ShapeType.Circle:
DrawCircle(shape);
break;
case ShapeType.Square:
DrawSquare(shape);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
は、ボブ・マーティンのOCPの解釈には反しますが、マイヤーの解釈には矛盾しません。それは、新しいshapeを追加しても、DrawメソッドのAPIを変更する必要がないからです。既存のクライアントはすべてそれでそのまま残りますから、壊れるような変更はないでしょう。
その意味では、ボブ・マーティンの解釈の方が広義です。それは、一般的に変更の量を減らすこと、つまり、元のコードをほとんど、あるいは全く変更せずにソフトウェアの動作を拡張できることを目的としています。バートランド・マイヤーの解釈は、ブレークチェンジ、つまり複数のチームが一緒に仕事をするときに問題を起こすような変更を減らすことだけを目的としているのです。
YAGNI
YAGNIとは「You aren't gonna need it」の略で、基本的には「今すぐ必要でない機能には時間を割くべきではない」という意味です。この機能を開発したり、将来の出現を考慮して既存のコードを修正したりするべきではありません。その理由を説明する2つの大きなポイントを紹介します:
- ビジネス要件は常に変化する。
- ビジネス要件は常に変化します。今この瞬間には必要のない機能に時間を費やすと、今必要な機能から時間を奪うことになります。さらに、開発した機能が必要とされるようになったとき、その機能に対する彼らの見方が変化している可能性が高く、その調整をしなければならないこともあります。このような活動は無駄であり、実際にその機能が必要になったときにゼロから実装する方が有益であるため、結果的に純損失となります。
- コードは資産ではなく、負債である。
- コードが増えるとメンテナンス費用がかさむので、コードは少なくするのが望ましいです。緊急の必要性がないのに「念のため」コードを導入すると、コードベース全体の総所有コストが高くなります。追加したコードをリファクタリングし、バグがないようにし、テストでカバーするなどの作業が必要であることを忘れないでください。新しい機能の導入は、プロジェクトのできるだけ後期に延期するのが望ましいです。
YAGNIが適用できない状況はあるのでしょうか?あります。
将来的に変更しにくい機能を設計している場合は、YAGNIに違反することがあります。例えば、顧客向けのAPI、サードパーティのライブラリ、基本的なアーキテクチャの決定、UI(ユーザーは新しい外観を受け入れることに抵抗があるため、変更は困難な場合があります)です。このような状況では、時間をかけて、将来の機能が現在の決定とどのように関わってくるかを予測する価値があります。例えば、適切なWeb APIのバージョン管理システムに先行投資するのは良い考えです。なぜなら、公開した後は変更が不可能になるからです。同様に、一般に公開されているライブラリの消費者向けのメソッドやクラスは、たとえ不要になったと判断しても、後方互換性のためにそこに留まらなければなりません。このようなものを変更するのは大変なことです。
ですから、言い方を変えると、これから行う決定が定石となるようなものであれば、YAGNIは適用されないのです。この場合、将来起こりうる要件を考慮する必要はあります。
しかし、このような決断はできるだけしないほうがよいでしょう。少なくとも、後の段階に延期するようにしましょう。そうすれば、実際のビジネスニーズについてより多くの情報を収集することができます。また、あなたが行うほとんどの決定は、それらの中になく、かなり簡単に変更することができることを心に留めておいてください。YAGNIは、私たちが日常的に書いているコードのほとんどに適用できます。
OCP vs YAGNI
なお、YAGNIは未使用の機能を実装することそのものだけでなく、将来起こりうる新機能を考慮して既存の機能を変更することも禁じています。そして、そこに矛盾があるのです。この「将来起こりうる新機能を考慮する」というのは、まさにボブ・マーティン版OCPが提案していることです。
もう一度、Drawメソッドを見てみましょう:
public void Draw(Shape shape)
{
switch (shape.Type)
{
case ShapeType.Circle:
DrawCircle(shape);
break;
case ShapeType.Square:
DrawSquare(shape);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
一方、YAGNIは、このswitch文は、結果として得られるコードがシンプルで理解しやすく、保守しやすいものであれば問題ないと言っています。一方、ボブ・マーティンのOCPでは、元のコードを変えずに、つまりswitch文そのものを変えずに拡張できるようにする必要がある、と言っています。
どちらの優先順位が高いのでしょうか?
この問いに答えるために、ちょっとだけ後ろに下がってみましょう。なお、私が言っているのはYAGNIとボブ・マーティンのOCPの矛盾であって、バートランド・マイヤーのバージョンではないことに注意してください。それは、YAGNIが後者と矛盾しているわけではなく、基本的に両者は違うことを話しているからです。
ボブ・マーティン版については、2つの視点から見ることができます。1つ目は、自分が書いたコードの作者とクライアントの両方である場合です。これは、ほとんどのエンタープライズアプリケーション開発者が陥る状況です。もう1つは、自分のコードを外部向けに公開する必要がある場合です。NuGetパッケージやフレームワークが典型的な例です。
YAGNIがOCPに勝るのは、コードがどのように使用されるかを完全に制御できる場合です。それが、前述の最初の設定です。なぜでしょうか?YAGNIはKISSと並んで、ソフトウェア開発における最も重要な原則だからです。これに従うことは、あらゆるソフトウェアプロジェクトの最優先事項であるべきです。
また、ボブ・マーティンのOCPをより詳しく見てみると納得がいきます。考えてみてください。なぜ、たとえそれが過度の複雑化を招くとしても、早々にコードに拡張点を敷かなければならないのでしょうか?単純なswitch文を別のクラス階層に置き換えることは、本当に労力とさらなるメンテナンスコストに見合うものなのでしょうか? もちろん、そうではありません。それよりも、全体像を把握し、switch文が肥大化しすぎていると判断したときに、事後的に拡張ポイントを設けるほうがずっといいはずです。この場合、リファクタリングを実施し、クラス階層を抽出することができます。しかし、その必要性が明らかになる前ではダメです。
さて、自分のコードがどのように使われるかをコントロールできない場合は、また別の状況です。この場合、先ほど述べたように、すでに実装されている機能を変更するコストが高すぎるため、YAGNIは適用できません。コードの利用者が自分だけでないため、簡単にリファクタリングすることができないのです。また、そもそもコードをリファクタリングすることに意味があるとは限りません。利用者が特定のニーズを持っていて、ライブラリの幅広い利用者には当てはまらないこともあるからです。
このような状況では、潜在的な変化点を特定し、利用者がクラスを変更するのではなく、拡張できるようなインターフェースを作成する必要があります。ですから、上のコード・サンプルでは、Drawが消費者向けのメソッドになると予想され、それを拡張する手段を提供したい場合、前もってShapeの基本クラスに置き換え、消費者が独自の形状を作成できるようにするのが良いアイデアです。
バートランド・マイヤー版OCPの視点を知ることで、ボブ・マーティン版のOCPはさらに理解を深めることができるでしょう。拡張ポイントは、何らかの形でコードを外部に公開する必要がある場合にのみ敷設する価値があります。それ以外の場合は、YAGNIを遵守し、実際の必要性なしに追加の柔軟性を導入しないことです。
summary
- Open/Closedの原則には2つの解釈がある:
- オリジナルのBertrand Meyerのものは、後方互換性についてのものです。モジュール/ライブラリ/サービスのAPIを外部で利用する場合は(変更に対して)closedである必要があります。実装の仕方ではなく、まさにそのインタフェース部分の話をしています。かつ、それが外部のチームによって使用される場合に適用されます。
- Bob Martinのものは、ハレーションを避けるためのものです。元のコードをほとんど、あるいはまったく変更せずに、ソフトウェアの動作を拡張できる必要があります。これは、コードベースに拡張ポイントを置くことで達成されます。
- YAGNIは、拡張ポイントが実際に必要になる前に、前もってコードベースに拡張ポイントを置くべきでないと説いています。
YAGNIは、ボブ・マーティン版のOCPと矛盾しています。 - この矛盾は、ボブ・マーティンのOCPをオリジナルのマイヤーの視点に置き換えると解決します。つまり、自分のコードが外部のチームによって使用される場合にのみ、この原則を適用すればよいのです。
- あなたが自分のコードの唯一の消費者である場合(エンタープライズ・ソフトウェア開発)、YAGNIはボブ・マーティンのOCPに勝ります。
- あなたがコードの唯一の消費者ではない場合、ボブ・マーティンのOCPはYAGNIに勝ります(サードパーティーのライブラリ/フレームワークの開発)。
投稿者の感想
- おもしれ〜
- 歴史的な経緯としてふたつのものがごちゃ混ぜになってる、という指摘。Good
- まとめとしては、外部依存していて容易にコードを修正できない場合にOCPを適用するとよいとのこと
- が、外部依存しているかどうか? というレイヤーで切り分けるのは適切ではない気がする
- 容易にコードを修正できないが外部依存していないケースなんてザラにあるから
- 内部依存だけど依存の量がやばいことになってるケースではもちろんOCPを導入しておきたいはず
- これはミノ駆動本でもそんな扱いだった
- 容易にコードを修正できないが外部依存していないケースなんてザラにあるから