mizoguche.info

オープン・クローズドの原則 - アジャイルソフトウェア開発の奥義一人読書会(2)

オープン・クローズドの原則(OCP: Open-Closed Principle)

オープン

モジュールの振る舞いを拡張できる

仕様変更に対して、モジュールに新たな振る舞いを追加することで対処できるようにすること。

クローズド

モジュールの振る舞いを拡張してもソースコードやバイナリが影響を受けない

ようするに

拡張がしやすくて、拡張しても修正箇所はできるだけ少なくなるような設計にするべき、という話。

「抽象」に依存する

モジュールをある固定した「抽象」に従属させておけば、修正に対してコードを閉じることができるのだ。なぜなら、「抽象」を使えば、コードを修正しなくても、その「抽象」の派生クラスを新たに追加するだけでモジュールの振る舞いを拡張できるからである。

修正するたびにバグを埋め込む機会を得ることになるので修正箇所はできるだけ少なくするべき。それを実現する方法が、抽象に対するプログラミング。

public class Client {
    private Server server;

    public Client(){
        server = new Server();
    }

    public void someMethod(String param){
        String str = server.process(param);
        // ...
    }
}

public class Server {
    public String process(String param){
        // ...
    }

    // 他の必要なメソッド
}

このように、具体的なクラスに依存している Client クラスは別の Server オブジェクトを利用したくなった場合はClient クラスを修正する必要がある。

public class Client {
    public interface ClientInterface{
        String process(String para);
        // 他の必要なメソッド
    }

    private ClientInterface server;

    public Client(ClientInterface clientInterface){
        server = clientInterface;
    }

    public void someMethod(String param){
        String str = server.process(param);
        // ...
    }
}

public class Server implements ClientInterface {
    @Override
    public void process(){
        // ...
    }

    // 他の必要なメソッド
}

このように、Client クラスが ClientInterface (抽象)に依存することによって、新しい Server クラスを利用したくなっても、Client クラスを修正する必要はない。新たに ClientInterface を実装したクラスを追加し、Client のコンストラクタに渡せばよい。

AbstractServer (抽象サーバー)ではなく、ClientInterface (クライアントインタフェース)と名づけたのか不思議に思っている人もいるだろう。その理由は、「抽象クラスはそれを実際に実装するクラスとの関係よりも、それを利用するクラスとの関係の方がずっと密接」だからだ。

オープン・クローズドの原則(OCP)に従わない例

本に書いてあるコードを、ゲームでありそうな例で書いてみた。コンパイルはしていない。

public class Item {
    public enum ItemType{
        POTION,
        WEAPON
    }
    public ItemType type;
    public String name;
    public int price;
}

public class PotionItem extends Item {
    int recoveryAmount;
    public PotionItem(String name, int price, int recoveryAmount){
        this.type = ItemType.POTION;
        this.name = name
        this.price = price;
        this.recoveryAmount = recoveryAmount;
    }
}

public class WeaponItem extends Item {
    int power;
    public WeaponItem(String name, int price, int power){
        this.type = ItemType.WEAPON;
        this.name = name
        this.price = price;
        this.power = power;
    }
}

public class ItemShop {
    public show(List<Item> items){
        for(Item item : items){
            switch(item.type){
            case ItemType.POTION:
                System.out.println(item.name + ": 回復量" + item.recoveryAmount + ", " + item.price + "ゴールド");
                break;
            case ItemType.WEAPON:
                System.out.println(item.name + ": 攻撃力" + item.power + ", " + item.price + "ゴールド");
                break;
            }
        }
    }
}

enum を使っているが、typeof を使ってサブクラスごとに処理を切り分けても本質的に同じ。

ShieldItem クラスを追加する場合、ItemType の種類が1つ増え、 ItemShopswitch 文の分岐が1つ増える。アイテムの種類が増える度に修正が重ねられていき、switch 文の中はコピペで増えていくだろう未来が確定されてヤバい。

さらに、開発が進むと ItemShop 以外の Item クラスを利用するクラスにも switch 文が登場し、アイテムの種類が増える度にいろんなクラスの switch 文を修正していく未来まで見える。週末がなくなり、終末が訪れるであろう。

こういうのが「硬い」・「もろい」設計。

オープン・クローズドの原則(OCP)に従った例

public abstract class Item {
    protected String name;
    protected int price;

    public abstract void getDescription();
}

public class PotionItem extends Item {
    private int recoveryAmount;
    public PotionItem(String name, int price, int recoveryAmount){
        this.name = name
        this.price = price;
        this.recoveryAmount = recoveryAmount;
    }

    @Override
    public void getDescription(){
        return this.name + ": 回復量" + this.recoveryAmount + ", " + this.price + "ゴールド";
    }
}

public class WeaponItem extends Item {
    private int power;
    public WeaponItem(String name, int price, int power){
        this.name = name
        this.price = price;
        this.power = power;
    }

    @Override
    public void getDescription(){
        return this.name + ": 攻撃力" + this.power + ", " + this.price + "ゴールド";
    }
}

public class ItemShop {
    public show(List<Item> items){
        for(Item item : items){
            System.out.println(item.getDescription());
        }
    }
}

こんな設計にすると、ShieldItem クラスを追加しても、上記のクラスはまったく影響を受けない

新しく追加した ShieldItem クラスを使うためには、たとえば ItemShop#show(List<Item>) の引数のリストをつくるクラスなんかに修正は必要だけれど、いろんなクラスで switch 文を修正していく未来よりも健康で文化的な生活が過ごせそうである。

そんなに上手く行かない

店でアイテムを表示する時、すべての武器を先に表示しなければならないという仕様変更が入ったら、Item クラスは上手く対応できない。

どんなに「閉じた(Closed)」モジュールであっても、閉じることのできない変更が必ずあるのだ。つまり、すべてのケースに適用できる自然なモデルなど存在しないのだ!

(中略)

あらゆる変更に対して完璧に閉じることが不可能なら、戦略的に閉じるしか無い。(中略)どういった種類の変更が頻繁に起こるのかを推測し、そういった変更から自分の身を守ることができるように「抽象」を構築するわけだ。

(中略)

では、どうすれば発生しそうな変更を推測できるだろうか? 適切なリサーチを行い、適切な質問を投げかけ、その上で経験と常識を使う。(中略)あとは実際に変更が起きるのを待つしか無い!

どんな変更に対応できるように設計するかがエンジニアの技術力だと思う。「とりあえずペンディング」という判断もときには必要だし、ペンディングした作業を行うタイミングを判断するのも相当技量が要るはず。経験がモノをいう感じは多分にある。まぁペンディングしっぱなしっていう技術的負債をためまくるという判断が現場では一番多い気がする。

“You Ain’t Gonna Need It”:「そんなの必要ないって」

前世紀には、起きる可能性のある変更に対応できるような「仕掛け」をプログラムに仕込んでおくという方法が流行った。

(中略)

しかしながら、実際には間違った仕掛けを仕込んでしまうことがほとんどだった。さらに悪いことに、それによって保守やサポートが必要な「不必要な複雑さ」の兆候が現れてしまった。(中略)設計に使ってもいない不必要な抽象をたくさん組み込んでしまうことは好ましくない。そういったものは、実際に望ましい抽象が必要になった時点でシステムに組み込むべきなのだ。

YAGNI である。Wikipedia から引用すると、

  • あとで使うだろうとの予測の元に作ったものは、実際には10%程度しか使われない。したがってそれに費やした時間の90%は無駄になる

  • 余計な機能があると我々の仕事が遅くなり、リソースを浪費する

  • 予期しない変更に対しては設計を単純にすることが備えとなるが、今必要とする以上の機能を追加すると設計がより複雑になってしまう

とかいう経験則である。

しかし、いろんな場面において、覚えたての知識・技術は使ってみたいし、使ってみて必要以上に複雑になって痛い目見るという「デザパタ厨期」を経ないと上手いバランス感覚は身につかないと思う。

逆に、そうやって痛い目を見れば、より一層 YAGNI の重要性がわかるし意識できるという意味では、「デザパタ厨期」はプログラマーとしてのイニシエーションみたいな感じある。

「痛い目見るのイヤやからデザインパターンなんて使いたくない」って考え方は、現状すでに痛い目にあってる状況を理解するところから始めなければならない感じがあって辛いのでノーコメント。

まとめ

色々な意味で、オープン・クローズドの原則(OCP)はオブジェクト指向設計の核心である。この原則に従うことで、オブジェクト指向技術から得られる最大の利益(柔軟性、再利用性、保守性)を享受できる

(中略)

早まった「抽象」をしないことも、「抽象」を使うのと同等に重要なことなのだ

「バグを減らす一番の方法はコードを書かないこと」という哲学っぽい言葉を実践しろという話であった。バグ取りが一番工数かかるから、設計の段階でバグが入り込みにくくすることは超重要。

なんかオブジェクト指向という枠組みの中での話なので「抽象」という概念がフォーカスされてる感じあるけど、「(仕様)変更が起きた時に修正が少なく済むように設計する」というコンセプトが一番重要っぽい。そう解釈すると、プログラミングという行為において一番重要な原則に感じる。

で、それをオブジェクト指向という枠組みで実現する方法として、「抽象」という方法を使う、と。

とりあえず OCP を意識して臨めば、新しいパラダイム学ぶ時にも、どうやったら OCP を実現できるかなーとかとりあえずの方向性が見えて良さそう。

そして、人間は万能ではないので YAGNI の心を忘れずにいなければならない。

でも忘れがちなので、レビューというありがたいシステムを使うべきである。客観視してもらうと YAGNI な部分が浮き彫りになる。