ユニットテストは、定義上、ユニット(通常は 1 つのクラス)内の動作に関するものです。テストを適切に実行するには、テスト対象のユニットを他のユニットとの相互作用から分離するために最善を尽くします (たとえば、モッキング、依存関係の挿入、およびすぐ)。
OCP は、ユニット (「ソフトウェア エンティティ」)間の動作に関するものです。エンティティ A がエンティティ B を使用する場合、それを拡張することはできますが、変更することはできません。(ウィキペディアの記事がソース コードの変更だけに重点を置いているのは誤りだと思います。この問題は、ソース コードの変更によって取得されたものであろうと、他のランタイム手段によって取得されたものであろうと、すべての変更に適用されます)。
A が B を使用する過程で B を変更した場合、B も使用する無関係なエンティティ C が後で悪影響を受ける可能性があります。この場合、通常、適切な単体テストでは破損を検出できません。これは、単体に限定されていないためです。これは、A が B を使用し、次にC も B を使用しようとするという、微妙で特定の一連のユニット間の相互作用に依存します。統合、回帰または受け入れテストがそれをキャッチするかもしれませんが、実行可能なコードパスを完全にカバーするようなテストに頼ることはできません (単体テストであっても、1 つのユニット/エンティティ内で完全にカバーすることは困難です!-)。
いくつかの点で、これの最も明確な実例は、動的言語で許可され、そのような言語の実践者の一部のコミュニティで人気のあるモンキーパッチの論争の的となっている実践にあると思います (すべてではありません!-)。モンキー パッチ (MP) は、ソース コードを変更せずに実行時にオブジェクトの動作を変更することを目的としているため、ソース コードの変更だけではOCP を説明できないと思います。
MP は、今挙げた事例をよく表しています。A と C の単体テストは、各ユニット自体が正常に動作するため、(両方ともモックではなく実際のクラス B を使用している場合でも) 見事に合格できます。両方をテストしても (すでに UNIT テストをはるかに超えています)、たまたま A の前に C をテストしても、すべて問題ないように見えます。しかし、たとえば、A がメソッド B.foo を設定して B にサルパッチを適用し、45 (B が文書で提供し、C が依存しているため) ではなく 23 (A の必要に応じて) を返すとします。これは OCP を壊します: B は変更のために閉じられるべきですが、A はその条件を尊重せず、言語はそれを強制しません。次に、A が B を使用 (および変更) し、C の番になると、C はテストされたことのない状態で実行されます。すべてのテストで常に45 を返しました...!-)。
MP を OCP 違反の標準的な例として使用することの唯一の問題は、MP をあからさまに許可しない言語のユーザーの間で誤った安心感を生み出す可能性があることです。実際、構成ファイルとオプション、データベース (すべての SQL 実装が許可する場所ALTER TABLE
など;-)、リモーティングなどを通じて、十分に大きく複雑なプロジェクトはすべて、たとえそれが Eiffel で書かれていたとしても、OCP 違反に注意を払わなければなりません。またはHaskell(そして、CやC ++のように、適切なキャスト呪文が配置されている限り、「静的」とされる言語が実際にプログラマーがメモリのどこにいても好きなものを突き刺すことができる場合はなおさらです-今ではそれがそのようなものですあなたは間違いなくコードレビューでキャッチしたいです;-)。
「修正のため閉鎖」は設計上の目標です。バグが見つかった場合、バグを修正するためにエンティティのソース コードを修正できないという意味ではありません (その後、コード レビュー、回帰テストを含むさらなるテストが必要になります)。もちろん、バグは修正されています)。
IWhateverEx
「リリース後に変更不可」が広く適用されているのを私が見た 1 つのニッチはIWhatever2
、Microsoft の古き良き COM などのコンポーネント モデルのインターフェイスですIWhateverEx2
。インターフェースへの修正が必要であることが判明した場合 --オリジナルに決して変更しないでIWhatever
ください!-)。
それでも、保証された不変性はインターフェイスにのみ適用されます。これらのインターフェイスの背後にある実装には、バグ修正、パフォーマンス最適化の調整などを含めることが常に許可されています (「最初から正しく実行する」ことは、SW 開発では機能しません。 : 100% 確実にバグがなく、使用されるすべてのプラットフォームで可能な最大の必要なパフォーマンスが得られる場合にのみソフトウェアをリリースできる場合、何もリリースすることはなく、競合他社があなたのランチを食べてしまいます。 d 倒産します;-)。繰り返しになりますが、このようなバグ修正と最適化には、通常どおりコード レビューやテストなどが必要になります。
あなたのチームでの議論は、バグ修正 (それらを禁止することについて議論している人はいますか?-) やパフォーマンスの最適化からではなく、むしろ新しい機能をどこに配置するかという問題から来ていると思います -foo
既存のクラスA
に新しいメソッドを追加する必要がありますむしろ、 「変更のために閉じられた」ままになるように、拡張A
しB
て追加foo
するB
だけですか?A
単体テスト自体は、まだこの質問に答えていません。既存のすべての使用法を実行できない可能性があるためですA
(A
エンティティがテストされるときに、別のエンティティを分離するためにモックアウトされる可能性があります...)。したがって、1 つのレイヤーに進む必要があります。より深く、foo
正確に何が行われているか、または行われている可能性があるかを確認します。
foo
が単なるアクセサーであり、それが呼び出されたインスタンスを変更しない場合、A
それを追加することは明らかに安全です。foo
インスタンスの状態とその後の動作を他の既存のメソッドから観察できるように変更できる場合は、問題があります。OCP を尊重foo
し、別のサブクラスを配置すると、変更は非常に安全で日常的になります。に簡単に配置したい場合は、広範なfoo
コードレビュー、軽い「ペアワイズコンポーネント統合」テスト、すべての使用を調査する必要がありますA
A
など。これはアーキテクチャの決定を制約するものではありませんが、どちらの選択にも異なるコストがかかることを明確に示しているため、適切に計画、見積もり、優先順位を付けることができます。
マイヤーの口述と原則は聖典ではありませんが、適切に批判的な態度をとれば、特定の具体的な状況に照らして研究し、熟考する価値があります。