まあ、OCPとLSPについては奇妙な誤解があり、いくつかは用語の不一致とわかりにくい例によるものです。両方の原則は、同じ方法で実装する場合にのみ「同じもの」です。パターンは通常、いくつかの例外を除き、何らかの方法で原則に従います。
違いについては後で詳しく説明しますが、最初に原則自体を詳しく見てみましょう。
開閉原理(OCP)
ボブおじさんによると:
クラスの動作を変更せずに拡張できる必要があります。
この場合の単語extendは、必ずしも新しい動作を必要とする実際のクラスをサブクラス化する必要があるという意味ではないことに注意してください。用語の最初の不一致で言及した方法を参照してください。キーワードextend
はJavaでのサブクラス化のみを意味しますが、原則はJavaよりも古いです。
オリジナルは1988年にバートランド・マイヤーから来ました:
ソフトウェアエンティティ(クラス、モジュール、関数など)は、拡張のために開かれている必要がありますが、変更のために閉じられている必要があります。
ここで、原理がソフトウェア実体に適用されるのは、より明確です。悪い例は、拡張ポイントを提供する代わりにコードを完全に変更しているため、ソフトウェアエンティティをオーバーライドすることです。ソフトウェアエンティティ自体の動作は拡張可能である必要があります。これの良い例は、Strategyパターンの実装です(GoFパターンバンチIMHOを表示するのが最も簡単だからです)。
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {
// Context is however open for extension through
// this private field
private IBehavior behavior;
// The context calls the behavior in this public
// method. If you want to change this you need
// to implement it in the IBehavior object
public void doStuff() {
if (this.behavior != null)
this.behavior.doStuff();
}
// You can dynamically set a new behavior at will
public void setBehavior(IBehavior behavior) {
this.behavior = behavior;
}
}
// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
public void doStuff();
}
上記の例でContext
は、はさらに変更できるようにロックされています。ほとんどのプログラマーはおそらく、クラスを拡張するためにクラスをサブクラス化することを望んでいますが、ここでは、インターフェイスを実装するものによって動作を変更できると想定しているため、ここでは行いませんIBehavior
。
つまり、コンテキストクラスは変更のために閉じられますが、拡張のために開かれます。実際には、継承の代わりにオブジェクト合成を使用して動作を設定しているため、別の基本原則に従います。
「「クラス継承」よりも「オブジェクト構成」を優先します。」(ギャングオブフォー1995:20)
この質問の範囲外であるため、読者にその原則を読んでもらいましょう。例を続けるには、IBehaviorインターフェースの以下の実装があるとしましょう:
public class HelloWorldBehavior implements IBehavior {
public void doStuff() {
System.println("Hello world!");
}
}
public class GoodByeBehavior implements IBehavior {
public void doStuff() {
System.out.println("Good bye cruel world!");
}
}
このパターンを使用すると、setBehavior
メソッドを拡張ポイントとして使用して、実行時のコンテキストの動作を変更できます。
// in your main method
Context c = new Context();
c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"
c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
そのため、「閉じた」コンテキストクラスを拡張する場合は常に、「開いた」共同依存関係をサブクラス化することによって行います。これは、コンテキスト自体をサブクラス化することと明らかに同じではありませんが、OCPです。LSPもこれについて言及していません。
継承の代わりにミックスインで拡張する
OCPを実行するには、サブクラス化以外の方法もあります。一つの方法は、ミックスインを使用してクラスを拡張用に開いたままにすることです。これは、たとえばクラスベースではなくプロトタイプベースの言語で役立ちます。アイデアは、必要に応じてより多くのメソッドまたは属性を持つ動的オブジェクト、つまり他のオブジェクトとブレンドまたは「混合」するオブジェクトを修正することです。
アンカー用のシンプルなHTMLテンプレートをレンダリングするミックスインのjavascriptの例を次に示します。
// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
render: function() {
return '<a href="' + this.link +'">'
+ this.content
+ '</a>;
}
}
// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
this.content = content;
this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
setLink: function(youtubeid) {
this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
}
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);
// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");
console.log(ytLink.render());
// will output:
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
目的はオブジェクトを動的に拡張することです。これの利点は、オブジェクトが完全に異なるドメインにある場合でもメソッドを共有できることです。上記の場合、特定の実装をで拡張することにより、他の種類のhtmlアンカーを簡単に作成できますLinkMixin
。
OCPに関しては、「ミックスイン」は拡張機能です。上記の例YoutubeLink
では、変更のために閉じられていますが、ミックスインを使用して拡張のために開いているソフトウェアエンティティです。オブジェクト階層がフラット化されているため、型を確認できません。しかし、これは本当に悪いことではありません。タイプをチェックすることは一般に悪い考えであり、ポリモーフィズムで考えを壊すことをさらに下で説明します。
ほとんどのextend
実装は複数のオブジェクトを混在させることができるため、このメソッドで複数の継承を行うことができることに注意してください。
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
覚えておく必要がある唯一のことは、名前を衝突させないことです。つまり、ミックスインは、オーバーライドされるいくつかの属性またはメソッドと同じ名前を定義します。私の謙虚な経験では、これは問題ではなく、もしそれが起こった場合、設計に欠陥があることを示しています。
リスコフの置換原理(LSP)
ボブおじさんはそれを次のように簡単に定義しています。
派生クラスは、基本クラスに代用可能でなければなりません。
この原則は古く、実際、ボブおじさんの定義は原則を区別していません。というのは、上記の戦略の例では同じスーパータイプが使用されているという事実により、LSPがOCPと密接に関連しているからです(IBehavior
)。それで、バーバラ・リスコフによる元の定義を見て、この原理について数学的な定理のように見える何かを見つけることができるかどうか見てみましょう:
何ここで望まれることは次の置換プロパティのようなものである:各オブジェクトの場合はo1
タイプのS
オブジェクトがあるo2
タイプのは、T
すべてのプログラムのために、このようなことP
の用語で定義されたT
、の動作はP
時に変更されていないo1
ために置換されo2
、その後S
のサブタイプですT
。
クラスについてはまったく言及していませんので、しばらくこれを肩代わりしてください。JavaScriptでは、明示的にクラスベースではありませんが、実際にはLSPに従うことができます。プログラムに、少なくとも2つのJavaScriptオブジェクトのリストがある場合:
- 同じ方法で計算する必要があり、
- 同じ動作をする
- それ以外は何らかの点で完全に異なります
...その後、オブジェクトは同じ「タイプ」を持つと見なされ、プログラムにとっては実際には問題になりません。これは本質的にポリモーフィズムです。一般的な意味で。インターフェイスを使用している場合、実際のサブタイプを知る必要はありません。OCPは、これについて明示的なことを何も言いません。また、ほとんどの初心者プログラマーが行う設計ミスを実際に特定します。
オブジェクトのサブタイプをチェックする衝動を感じているときはいつでも、あなたは間違いをしている可能性が高いです。
さて、それはすべての時間間違っているではないかもしれませんが、あなたには、いくつかの実行する衝動がある場合はその型チェックとinstanceof
もう少しそれが必要以上に自分自身のためのコンボリューションまたは列挙型を、あなたがプログラムを実行される可能性があります。しかし、これは常にそうではありません。解決策が十分に小さい場合、物事を機能させるための迅速で汚いハックは私の心に許す譲歩であり、容赦のないリファクタリングを実践すれば、変更が要求されると改善されるかもしれません。
実際の問題に応じて、この「設計ミス」を回避する方法があります。
- スーパークラスは前提条件を呼び出しておらず、代わりに呼び出し側に強制的に呼び出しています。
- スーパークラスには、呼び出し元が必要とするジェネリックメソッドがありません。
これらは両方とも、一般的なコード設計の「間違い」です。プルアップメソッドや、訪問者パターンなどのパターンへのリファクタリングなど、実行できるリファクタリングがいくつかあります。
ビジターパターンは、大きなif文のスパゲッティを処理でき、既存のコードで考えるよりも実装が簡単なので、実際にとても気に入っています。次のコンテキストがあるとします。
public class Context {
public void doStuff(string query) {
// outcome no. 1
if (query.Equals("Hello")) {
System.out.println("Hello world!");
}
// outcome no. 2
else if (query.Equals("Bye")) {
System.out.println("Good bye cruel world!");
}
// a change request may require another outcome...
}
}
// usage:
Context c = new Context();
c.doStuff("Hello");
// prints "Hello world"
c.doStuff("Bye");
// prints "Bye"
ifステートメントの結果は、それぞれの決定と実行するコードに応じてそれぞれのビジターに変換できます。次のようにこれらを抽出できます。
public interface IVisitor {
public bool canDo(string query);
public void doStuff();
}
// outcome 1
public class HelloVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Hello");
}
public void doStuff() {
System.out.println("Hello World");
}
}
// outcome 2
public class ByeVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Bye");
}
public void doStuff() {
System.out.println("Good bye cruel world");
}
}
この時点で、プログラマがVisitorパターンを知らなかった場合、代わりにContextクラスを実装して、特定のタイプかどうかをチェックします。VisitorクラスにはブールcanDo
メソッドがあるため、実装者はそのメソッド呼び出しを使用して、ジョブを実行するのに適切なオブジェクトであるかどうかを判断できます。コンテキストクラスは、次のようにすべての訪問者を使用(および新しい訪問者を追加)できます。
public class Context {
private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();
public Context() {
visitors.add(new HelloVisitor());
visitors.add(new ByeVisitor());
}
// instead of if-statements, go through all visitors
// and use the canDo method to determine if the
// visitor object is the right one to "visit"
public void doStuff(string query) {
for(IVisitor visitor : visitors) {
if (visitor.canDo(query)) {
visitor.doStuff();
break;
// or return... it depends if you have logic
// after this foreach loop
}
}
}
// dynamically adds new visitors
public void addVisitor(IVisitor visitor) {
if (visitor != null)
visitors.add(visitor);
}
}
どちらのパターンもOCPとLSPに従いますが、どちらもパターンについて異なる点を特定しています。それでは、原則の1つに違反する場合、コードはどのように見えますか?
1つの原則に違反するが、他の原則に従う
原則の1つを破る方法がありますが、それでも他の原則に従う必要があります。以下の例は、正当な理由で不自然に見えますが、実際に実稼働コードでこれらのポップアップが表示されています(さらに悪いことです):
OCPに従うが、LSPは従わない
与えられたコードがあるとしましょう:
public interface IPerson {}
public class Boss implements IPerson {
public void doBossStuff() { ... }
}
public class Peon implements IPerson {
public void doPeonStuff() { ... }
}
public class Context {
public Collection<IPerson> getPersons() { ... }
}
このコードは、オープンクローズの原則に従っています。コンテキストのGetPersons
メソッドを呼び出している場合、すべて独自の実装を持つ多数の人を取得します。つまり、IPersonは変更のために閉じられますが、拡張のために開かれます。ただし、使用する必要がある場合は、状況が暗転します。
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// now we have to check the type... :-P
if (person instanceof Boss) {
((Boss) person).doBossStuff();
}
else if (person instanceof Peon) {
((Peon) person).doPeonStuff();
}
}
型チェックと型変換を行う必要があります!上記でどのように型チェックが悪いことであるかを覚えていますか?大野!ただし、上でも述べたように、プルアップリファクタリングを行うか、Visitorパターンを実装することを恐れないでください。この場合、一般的なメソッドを追加した後、プルアップリファクタリングを実行できます。
public class Boss implements IPerson {
// we're adding this general method
public void doStuff() {
// that does the call instead
this.doBossStuff();
}
public void doBossStuff() { ... }
}
public interface IPerson {
// pulled up method from Boss
public void doStuff();
}
// do the same for Peon
現在の利点は、LSPに従って、正確な型を知る必要がなくなることです。
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// yay, no type checking!
person.doStuff();
}
OSPではなくLSPに従います
OSPではなくLSPに続くいくつかのコードを見てみましょう、それはちょっと不自然ですが、この1つに非常に微妙な間違いがあります:
public class LiskovBase {
public void doStuff() {
System.out.println("My name is Liskov");
}
}
public class LiskovSub extends LiskovBase {
public void doStuff() {
System.out.println("I'm a sub Liskov!");
}
}
public class Context {
private LiskovBase base;
// the good stuff
public void doLiskovyStuff() {
base.doStuff();
}
public void setBase(LiskovBase base) { this.base = base }
}
コンテキストが実際のタイプを知らなくてもLiskovBaseを使用できるため、コードはLSPを実行します。このコードもOCPに従うと思いますが、よく見ると、クラスは本当に閉じているのでしょうか?doStuff
メソッドが単に行を印刷するだけではない場合はどうなりますか?
OCPに続く場合の答えは単純です:NO。これは、このオブジェクト設計では、コードを他のもので完全にオーバーライドする必要があるためではありません。これにより、基本クラスからコードをコピーして動作させる必要があるため、カットアンドペーストワームの缶が開きます。doStuff
この方法は、確か拡張のために開いているが、それは完全に変更のため閉鎖されていませんでした。
これにTemplateメソッドパターンを適用できます。テンプレートメソッドパターンはフレームワークで非常に一般的であるため、知らないうちに使用していた可能性があります(たとえば、java swingコンポーネント、c#フォームおよびコンポーネントなど)。doStuff
変更のためにメソッドを閉じ、javaのfinal
キーワードでマークすることでメソッドが閉じられたままになるようにする方法の1つです。このキーワードは、誰もクラスをさらにサブクラス化することを防ぎます(C#sealed
では同じことを行うために使用できます)。
public class LiskovBase {
// this is now a template method
// the code that was duplicated
public final void doStuff() {
System.out.println(getStuffString());
}
// extension point, the code that "varies"
// in LiskovBase and it's subclasses
// called by the template method above
// we expect it to be virtual and overridden
public string getStuffString() {
return "My name is Liskov";
}
}
public class LiskovSub extends LiskovBase {
// the extension overridden
// the actual code that varied
public string getStuffString() {
return "I'm sub Liskov!";
}
}
この例はOCPに準拠しており、馬鹿げているように見えますが、これは処理するコードが増えてスケールアップされたものです。サブクラスがすべてを完全にオーバーライドし、オーバーライドされたコードのほとんどが実装間でカットアンドペーストされるプロダクションでデプロイされたコードを見続けています。それは機能しますが、すべてのコード複製と同様に、メンテナンスの悪夢のためのセットアップでもあります。
結論
これにより、OCPとLSPおよびそれらの違い/類似性に関するいくつかの質問が明らかになることを願っています。それらを同じように却下するのは簡単ですが、上記の例はそうではないことを示しているはずです。
上記のサンプルコードから収集することに注意してください。
OCPは、動作中のコードをロックダウンすることですが、何らかの拡張ポイントを使用して何らかの形でそれを開いたままにします。
これは、テンプレートメソッドパターンの例のように変化するコードをカプセル化することにより、コードの重複を避けるためです。また、破壊的な変更は痛みを伴うため(つまり、1つの場所を変更し、他のすべての場所で破壊する)、高速で失敗することもできます。変更は常に発生するため、メンテナンスのために、変更をカプセル化するという概念は良いことです。
LSPは、実際の型を確認せずに、スーパータイプを実装するさまざまなオブジェクトをユーザーが処理できるようにすることです。これは本質的にポリモーフィズムの目的です。
この原則は、型チェックや型変換を行う代替手段を提供します。これは、型の数が増えると手に負えなくなり、プルアップリファクタリングまたはVisitorなどのパターンを適用することで実現できます。