datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】何故デザインパターンが必要なのか

どうも、後輩の研究のソースコードがスパゲッティになっている、ということでソースコードレビューをしたら「◯◯さん、これまじで俺辛いっす!」って言われました。
来週の海外出立にそわそわしながらも現在も研究のソース書いてます。

さて、今回は前回いきなりシングルトンパターンというデザインパターンを紹介したが、なぜデザインパターンが必要なのか?別にOOの三本柱を使うだけでいいんじゃないのか?という疑問もあると思うので、ここでデザインパターンのメリットについて明確にしていきたいと思う。

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

デザインパターン?なにそれ美味しいの?



デザインパターンオブジェクト指向(以降、"OO"と呼称)の三本柱(継承、多態性カプセル化)を駆使して(広義の意味ではOOを利用しなくてもデザインパターンに属する 例:MVCモデル)一般的な問題に対してアプローチが可能なコーディング方法、
などと上でうだうだ書いてみたが、自分でも上手く言い表せていなかったのでここではWikipedia大先生に解説してもらおう。
デザインパターン (ソフトウェア) - Wikipedia

それ出来て何が嬉しいんですか?



デザインパターンを理解することで以下の様な沢山の恩恵が受けられる。

  1. コードのモジュール化が行われ、機能の単位が小さくなる
  2. 依存性の低いコードになる
  3. 保守性、柔軟性があがり、機能の追加が容易になる
  4. デザインパターンという共有知識を知ることでエンジニア同士での意思疎通が容易になる
    (ここらへんはUMLも同様に言えるね)
  5. デザインパターンが実装レベルの情報を内包してくれるので、設計レベルの話で実装の話に移るようなことが少なくなる

特に上記三点においてはどれもそれぞれ関係し合ってますね。
コードの単位が小さくなれば、それだけコードの重複も減るので、依存性が下がる。
その結果、メソッドの組み合わせもしやすくなり、柔軟性の高いコードになり、保守性も上がるというものだ。

でも悪いことも起きるんじゃないの?



もちろん、どんなものに欠点はある。

  1. そもそもデザインパターンを知らない人にコードを見せると難解な物に見える
  2. デザインパターンを何でも適応し始めると、簡単なコードが複雑なコードになり、難読なものになる
  3. オブジェクト指向を知らないと何をやってるか分からない魔法のコードになる

デザインパターンオブジェクト指向ありきのパターンなので、オブジェクト指向を理解してないような人が触ると、コードを理解するまでの時間が飛躍的に伸びる。
(逆に言えば、リーダーはオブジェクト指向の勉強やデザインパターンの勉強が出来る、ということでもあるので一概には欠点とも言えないけど)
また、デザインパターンは何にでも適応すればいいと言うものではない。
ましてや"Hello, world!"にデザインパターンを適応し始めた日には目も当てられない。

そもそも何故コードが汚くなるのか?



上記のメリットの裏返しが全て。
モジュール強度、モジュール結合度という言葉がある。
モジュール強度とは、モジュールがどれだけの機能を持っているか、ということ。シンプルな程、モジュールの強度はあがる。
モジュール結合度とは、その名の通りモジュールに変更を加える事で他のモジュールに影響がどれだけ出るか、ということ。影響が少ないほど結合度は下がる。
他にもコードの重複などがあるが、これも元を正せばモジュール強度が低いことにより、再利用性が低くなっているために、似たようなコードが乱立するのが原因。

このモジュール強度とモジュール結合度を以下に下げられるかが、綺麗なプログラムを書くための条件になる。
それを助けるのがデザインパターンという訳だ。

参考書に書かれている実装手順の例


とある会社でのゲーム

ある会社では泳いだり鳴いたりする多種多様な種類の鴨を表示するゲームを作成している。
初期の設計者はDuckクラスを作成して、すべての鴨がDuckクラスを継承して様々なタイプの鴨を定義できるようにした。

class Duck
{
    public quack();   // 鳴く
    public swim();    // 泳ぐ
    public display(); // 表示
}

class MallardDuck extends Duck
{
    display(){ /* マガモの表示 */ }
}

class RedheadDuck extends Duck
{
    display(){ /* アメリカホシハジロの表示 */ }
}

これだけ見るならまだ誰でも出来そうなレベルの設計だ。だがここから徐々に問題が発生してくる。

新たなる機能の追加

競合他社に負けないため、経営幹部はこのゲームに鴨が「飛ぶ」ことが出来るように命じた。
開発者はOOプログラムなので、OOの才能を発揮し、以下のように修正を加えた。

class Duck
{
    public quack();   // 鳴く
    public swim();    // 泳ぐ
    public display(); // 表示
    public fly();     // 飛ぶ
}

これにより全ての鴨が飛ぶことが一行付け加えることで出来るようになった。

当然の対応ですね。機能は徐々に追加されますが、それをスーパークラスに反映させればいいだけの話。

問題の発生

なんとこのゲームではゴム製の鴨が出現するようです。
株主総会で見せたデモでゴム製の鴨が空を飛び回り、大きな失態を晒しました。

ここでの認識の間違いは開発者がDuckのすべてのサブクラスが「飛ぶ」と考えていたことが間違いでした。
そこで開発者は以下のように変更を加える事でこの問題を回避しました。

class RubberDuck extends Duck
{
    quack(){ /* キューキューという音 */ }
    display(){ /* ゴム製の鴨 */ }
    fly(){ /* オーバーライドして何もしないようにする */ }
}

手っ取り早い解ではありますが、徐々に苦し紛れな感じになってきたことが見て取れますね。

しかし、更なる問題が!

今度は木製のおとりの鴨(飛ばないし、泣かない)を追加することになりました。
開発者は以下のようにコードを追加することで問題を乗り切りました。

class DecoyDuck extends Duck
{
    quack(){ /* オーバーライドして何もしないようにする */ }
    display(){ /* おとりの鴨 */ }
    fly(){ /* オーバーライドして何もしないようにする */ }
}

徐々に継承における問題が浮き彫りになってきましたね。


さて、このままどんどん鴨の種類が増えていけばどうなるか?
鴨の機能が追加されたらどうなるか?
毎回、少ししか違わないのにクラスに新しい機能やクラスが追加される度に書いていくのか?

そこで開発者は!?



インターフェースを使って「飛ぶ」機能、「鳴く」機能を分離させました。
これなら必要なDuckのサブクラスにimplementsをすることで問題を解決出来ると考えたのです。

interface Flyable
{
    fly();
}

interface Quackable
{
    quack();
}

class Duck
{
    swim();
    display();
}

class MallardDuck extends Duck implements Flyable, Quackable
{
    display();
    fly();
    quack();
}

class RubberDuck extends Duck implements Quackable
{
    display();
    quack();
}

class DecoyDuck extends Duck
{
    display();
}

さて、これに対して横から酷いツッコミを開発者は受けます。
「2個、3個のオーバーライドをしなければならないことを悪いことだと思うのなら、なぜ全48個の飛ぶことの出来るDuckサブクラスのfly()について変更を行う必要があることを悪いと思わないのか?」

これを見て自分が同じ事を一切やらないという自信を持てなかったのでちょっと心に来る…

コンポジションを用いて問題を解決した

結局の所、継承(is-a)ではなくコンポジション(has-a)を使うことでこの問題は解決に向かいました。
「飛ぶ」というインターフェースをDuckクラスに持たせ、それを内部でインスタンス化することでこの問題の対応にあたりました。

interface FlyBehavior
{
    fly();
}

class FlyWithWings implements FlyBehavior
{
    fly(){ /* 鴨の飛ぶ振る舞いを実装 */ }
}

class NoneFly implements FlyBehavior
{
    fly(){ /* 何もしない = 飛べない */ }
}

class Duck
{
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior; // 上記と同じような感じで宣言される
    performFly(){ flyBehavior.quack(); }
    performQuack(){ quackBehavior.quack(); }
}

これにより、振る舞いの数だけ実装し、クラスの内部で宣言時に作成することで問題が回避されました。
めでたし、めでたし。

例を見て感じた事



自分も研究のソースコードで頻繁に機能の追加、削除が行われる上に、とても速いサイクルで実装を要求されることもあった。
その経験から、開発者のような感じには自分もなったことがあるのでこの境遇が凄くわかります。

今まで沢山の残念なソースコードを生産し、これからもそのようなソースを生産して行くことでしょう。
何故ならその時は良い実装だと思っても、半年~一年後にはとてつもない残念なコードになっているからです。

これを見て、デザインパターンの重要性に気づければと思います。
そして、未来の自分が残念なコードと気づく期間も長くすることが出来るということを信じて、このデザインパターンの学習に臨みたいと思います。