オブジェクト指向プログラミングを極める

より良いプログラマを目指すブログ

カプセル化=データの変更制限ではない

カプセル化オブジェクト指向プログラミングの三大要素*1のひとつでオブジェクト指向プログラミングの入門書の最初に出てくる言葉だ。そして、カプセル化の説明としてよく使われるのが「データを封じ込めメソッドを介してのみ変更可能とすることでデータの予期せぬ変更を防ぎプログラムを堅牢とする」という様な説明だ。実際、私も技術書や講義でこの説明をされたので長年そういうものだと信じていた。最初のうちはこの理解でも良いのかもしれないが、より良いプログラムを書きたいのであればこの認識を捨てなければならない。なぜならカプセル化=データの変更制限という認識はカプセル化の重要な意味を見落としてしまうからだ。

カプセル化されていないデータの何が悪いのか

そもそもプリミティブをそのまま使うことの何が問題なのだろうか。まずはそこからおさらいしてみたい。次のコード(C#)はロボットを指定された秒数だけ移動させる。

int time = 3;
Console.WriteLine("ロボットを" + time + "秒間移動します");
robot.Move(time);

シンプルなコードだ…。バグの入り込む余地もない。だがこのコードには重大な問題がある。それはtimeが「秒」であるというコードに現れない前提条件があることだ。もしある日突然、時間設定をミリ秒で行う仕様に変更されたらこの前提条件は崩壊し、コードのtimeに依存している場所すべてを変更しなければならなくなる。しかもコンパイラはこの前提条件をチェックしてくれないのでMove() メソッドの引数に渡された数値が時間以外…温度とか速度とかでも問題なく受け入れられてしまう。プログラマーは常にtimeの中身を気にしなければならなくなる。このように、コードに現れない前提条件を隠れた結合*2と呼ぶ。隠れた結合こそプログラムが脆弱となる要因だったのだ。

プリミティブtimeカプセル化するとどうなるか。

class Second
{
    readonly int time;

    public Second(int time)
    {
        this.time = time;
    }

    public override string ToString()
    {
        return time + "秒";
    }
}
Second time = new Second(3);
Console.WriteLine("ロボットを" + time.ToString() + "間移動します");
robot.Move(time);

timeカプセル化されSecondという意味付けがされたことで、コードが何をやっているのか一目瞭然となった。また、Move()メソッドがSecondオブジェクトを受け取るようになりコンパイラのチェックが働くようになったので、プログラマーはtimeの中身を気にする必要がなくなった。カプセル化は隠れた結合を排除することでプログラムを堅牢なものとする。

カプセル化の本質はデータを現実の物体として振舞わせること

先ほどの例ではint型の数値をカプセル化することで「秒数」として意味付けた。言い換えてみれば 、intが「秒数」という現実の物体として振舞っている。突き詰めていくと、カプセル化の本当の意味はデータを現実の物体すなわちオブジェクトとして振舞わせることだということに気付く。例えば、下記はint型のコードで管理されるトランプのカードを表すオブジェクトだ。

// トランプ
interface PlayingCard
{
    int Suit();    // スート
    int Number();    //数字
}
// int型のコードで管理されるトランプ
class IntCard : PlayingCard
{
    readonly int code;
    
    public IntCard(int code)
    {
        this.code = code;
    }

    public int Suit()
    {
        return code / 13;
    }  

    public int Number()
    {
        return code % 13 + 1;
    }

    public override string ToString()
    {
        string[] suit = {"スペード", "ハート", "クラブ", "ダイヤ"};
        return suit[this.Suit()] + "の" + this.Number();
    }
}
// 使い方
PlayingCard card = new IntCard(0);  // スペードの1

IntCardはint型のコードをカプセル化し、外界に対してはあたかもトランプのカードのように振舞わせている。フィールドはコードだけでSuitやNumberなどのデータは一切持っていない点に着目してほしい。振る舞いはあくまでも実行時に決まるものだ。コンストラクタでsuitやnumberを計算してフィールドに持たせてはいけない。データを現実の物体として振舞わせるイメージを持つと、Getter/Setterがなぜ悪いのか分かる気がする。カプセル化=データの変更制限という認識を捨てることで Getter/Setter の乱立を防ぎより洗練されたプログラムを書けるようになる。