Railsで学ぶSOLID(3)リスコフの置換原則(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: SOLID Principles #3: Liskov Substitution Principle | Netguru Blog on Ruby/Ruby on Rails 原文公開日: 2018/03/21 著者: Marcin Jakubowski サイト: netguru 訳注: 原文のparent classは原則として「基底クラス」と訳出しました。 Railsで学ぶSOLID(3)リスコフの置換原則(翻訳) 「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。 それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを向上させることを目的としています。 単一責任の原則(SRP: Single responsibility principle) オープン/クローズの原則(OCP: Open/closed principle) リスコフの置換原則(LSP: Liskov Substitution Principle)(本記事) インターフェイス分離の原則(ISP: Interface Segregation Principle) 依存関係逆転の原則(DIP: Dependency Inversion Principle) 今回は、3番目の「リスコフの置換原則」を見ていきましょう。 3: リスコフの置換原則(LSP) 基底クラスから派生したクラスは、望ましくない挙動を一切伴わずに、常に基底クラスと置き換え可能でなければならない。 次のように表すこともできます。 SがTの派生型であるとすると、プログラム内における型Tのオブジェクトは、プログラム内の望ましいプロパティを一切変更することなく、型Sのオブジェクトに置き換え可能である。 実践的に書くと、派生クラスは基底クラスを継承するときに、常に基底クラスの挙動を一切変えないようにすべきであるということです。 最も古典的なLSP違反例を以下のスニペットで示します(Gist)。 class Rectangle attr_accessor :height, :width def calculate_area width * height end end class Square < Rectangle def width=(width) super(width) @height = width end def height=(height) super(height) @width = height end end rectangle = Rectangle.new rectangle.height = 10 rectangle.width = 5 rectangle.calculate_area # => 50 square = Square.new square.height = 10 square.width = 5 square.calculate_area # => 25 数学的にはどこもおかしくはありません。ここで扱っているのは正方形なので、高さと幅が同じでなければなりません。高さ(と幅)を10に設定し、続いて幅(つまり高さも)を5に設定して面積を算出します。 基底クラスと派生クラスで同じ手順を踏んでいますが、両者の振る舞いが異なっていることが見て取れます。インターフェイスが基底クラスと派生クラスで一貫していないのです。 つまり、publicなインターフェイス(そしてもちろんそれらの振る舞いも)は、基底クラスと派生クラスで同じになってなければならないということが言えます。 LSPとポリモーフィズム もうひとつ極めて重要な点があります。「得られる結果が異なるからLSPに違反する」のではありません。「期待しない振る舞いが生じるからLSPに違反する」のです。 次の例で考えてみましょう(Gist)。 class Shape def draw raise NotImplementedError end end class Rectangle < Shape def draw # 四角形を描画 end end class Circle < Shape def draw # 円を描画 end end drawメソッドは、派生クラスに応じて異なる図形を描画しますが、描画はまったく期待を裏切っていません。 この点をもう少し考えてみましょう。「得られる結果が異なれば即LSP違反」ということになってしまえば、OOPの極めて強力なツールであるポリモーフィズムは全部LSP違反になってしまいます。 基底クラスのメソッドを派生クラスでオーバーライドするとき、クラスの振る舞いは変更すべきではありませんが、派生クラスの特定の側面によって振る舞いを拡張(extend)できます。 前提条件を派生型で増強してはならず、事後条件を派生型で弱めてはならない。 または次のようにも言えます。 サブクラスは、要求事項を(基底クラスよりも)増やすべきではなく、できることを(基底クラスよりも)減らすべきではない。 LSPに従うことで、自信を持ってポリモーフィズムを使えるようになり、期待しない結果が生じる心配なしに、基底クラスを参照している派生クラスを呼び出せるようになります。 問題はいったいどこに潜んでいるのか この問題は、抽象化の中に潜んでいます。数学的には、正方形は四角形の一種ですが、プログラミングにおいては(少なくともこの場合は)違います。つまり、抽象化のモデリングに誤りがあったというだけのことです。 私がこの例を愛する理由 上の例には、OOPに関する重要なポイントが1つ示されているからです。OOPは、現実世界を単純にオブジェクトにマッピングしただけのものではありません。 OOPとは「抽象を作り出す」ことであり、「概念を作り出す」ことではありません! もう少し改良を加える 正直に申し上げると、完全無欠の解決方法というものはありません(いつものことですが)。 上の例に登場するクラスたちに共通の振る舞いが存在しないことを認識します。こういう2つのクラスを結合してはいけません。振る舞いの異なる2つのクラスを作るだけにしましょう。 インターフェイスの型を「シミュレート」する抽象レイヤーを1つ追加します(解決は継承によっても行われますが、方法は異なります)。 第2の解決法の例を示します(Gist)。 class Shape def calculate_area raise NotImplementedError end end def Rectangle < Shape attr_accessor :height, :width def … Continue reading Railsで学ぶSOLID(3)リスコフの置換原則(翻訳)