この国では犬が

本と芝居とソフトウェア

StrategyパターンはLSP違反ではない

長年の疑問がおおむね解消された気がするので、記念エントリ。

わかっている人には「ではない」て言われても「そりゃそうなのでは?」という感じだと思いますが、ご容赦ください。

デザインパターン(というか、オブジェクト指向設計)のあっさい理解が原因の悲しい誤解ストーリーです。

 

あらすじ

 

Strategyパターンは、GoFデザインパターンのうちの1つで、振る舞いを切り替えるパターン。

LSP(Liskov Substitution Principle)は、派生型の定義の一種で、簡単に言うと「スーパークラスのオブジェクトを使用できる場所では、そのサブクラスも使用できなければならない(置き換え可能でなければならない)」とするもの。

 

で、Strategyパターンは「サブクラス化によって振る舞いを切り替える」わけだから、LSPに違反してしまうのでは?と思っていたのですが……、どうやらそういうわけではなかった!

 

StrategyパターンはLSP違反ではないようだ?

 

下図をご覧ください。

 

f:id:enk_enk:20131204225356p:plain

 

Strategyパターンの簡単なクラス図(例)です。

Performerクラスは、Performer.walk()が呼ばれたら、WalkingStrategyインタフェースのwalk()を呼び出し、生成されている実オブジェクト(NormalWalkingStrategyかMoonWalkingStrategy)に応じて、その具象クラスのwalk()のコードが実行されます。

 

NormalWalkingStrategyなら普通に歩きますし、MoonWalkingStrategyならムーン・ウォークします。

 

さて、かつての僕が注目していたのは、「NormalWalkingStrategy」と「MoonWalkingStrategy」は「振る舞いが異なる」ので、「置き換え可能ではない」のでは?というところでした。

 

が!

おわかりのように、たとえばMoonWalkingStrategyは、NormalWalkingStrategyのサブクラスではありません。し、逆もまた然り。あくまでどちらもWalkingStrategyインタフェースを実装しているだけです。

よって、NormalWalkingStrategyがMoonWalkingStrategyに置き換え可能である必要はありません。(逆もまた然り)

 

StrategyパターンはLSP違反ではない!

 

さて、両クラスが実装しているインタフェースであるWalkingStrategyのwalk()は、呼ばれたら「歩く」ことを約束しているみたいです。よって、LSPに従うと、WalkingStrategyインタフェースを使用しているすべての場所で、NormalWalkingStrategyとMoonWalkingStrategyの両方を使用できる必要があります。*1

 

できます。

 

というか、WalkingStrategyインタフェース自体がまさにNormalWalkingStrategyとMoonWalkingStrategyの共通項を抽象したものとも言うべきものなので、できるに決まってます。(できるように作ったのだから、できる)

 

WalkingStrategyのwalk()が「前に歩く」とは言っていないことがポイントです。

WalkingStrategyのクライアントであるPerformerは、もちろん、「前に歩くとは限らない」ということを知ったうえで、WalkingStrategyインタフェースを使用する必要があります。

 だから、WalkingStrategyのドキュメントを作るとしたら、ただ「歩く」と書くだけじゃなくて、「歩く(前に、時には後ろや横に、斜めもあるかもね)*2」とか書くべきかもしれません。(普通の人は前に歩きますしね……)

 

StrategyパターンをLSP違反だと勘違いしていた理由

 

僕が「StrategyパターンはLSP違反なのでは?」と心配しているときにうすぼんやりと思い浮かべていたのは、実のところ、こういった設計だったのでした。(念のため注意:この図はStrategyパターンではありませんし、別にかつての僕がStrategyパターンをこういう形で理解していたわけでもありません。浅い理解こわい)

 

f:id:enk_enk:20131204232046p:plain

 

寂しくなりましたね。

WalkingStrategyがインタフェースではなくなり、具象クラスになって、更にStrategyパターンのときの「NormalWalkingStrategy」のwalk()が持っていた実装を持っています。

 

これは、LSP違反です。

NormalWalkingStrategyのwalk()は「前に向かって歩く」ことを約束しているので、「ムーン・ウォークする」MoonWalkingStrategyによって置き換え可能ではありません。

 

そう……

 

StrategyパターンはLSP違反ではなく、むしろLSP違反を避けるための設計パターンだったのだ!

 

(たぶん……)

 

まとめ

  • StrategyパターンはLSP違反ではない。なぜなら、具体的なStrategyどうし(それぞれに振る舞いが違う)は継承関係を持たず、それらの共通項を抽象したインタフェースを実装しているだけだから。
  • むしろStrategyパターンを適用することでLSP違反を避けられる。

 

参考文献

 

*1:なお、WalkingStrategyはインタフェースなので、インスタンス化できません。よって、冒頭に述べたLSPの簡単な説明の「スーパークラスのオブジェクトを使用できる場所では……」という前提がそもそも成り立ちません。もしかしたら、厳密に言うと、StrategyパターンはLSPを適用する必要自体ないケースなのかも。とりあえず、WalkingStrategyインタフェースを前述の説明における「スーパークラス」と見なして話を進めます

*2:もちろん、プロダクションコードに「かもね」とか書いたらたぶんしばかれます。むしろしばきます