Immutableについて

昨日チーム内でImmutableについての話をしたので今日はその辺りを書きたいと思います。

Immutableとは

Immutableとは一般的に不変と訳され、状態変更ができないことを意味します。

逆に状態変更ができることをMutable(可変)と言います。

Mutableなクラスだけで実装するとクラスの外部からでもクラス内の変数を書き換えることが可能になります。

簡単に書き換えられてしまうと思わぬところで値が変わっていて想定と異なる挙動をすることに繋がり、バグの温床となります。

1つ悪い例を挙げてみます。

class Car
{
    public Body $body;

    public function __construct(Body $body)
    {
        $this->body = $body;
    }
}

class Body
{
    public $color = 'red';
}

上記のようなCarクラスとBodyクラスがあるとします。

$body = new Body();
$myCar = new Car($body);
echo $myCar->body->color . PHP_EOL;

新しく$myCarというインスタンスを生成します。

これを実行するとBodyクラスをデフォルトのまま変えていないので、

red

という文字列が出力されます。

ここでもう1つインスタンスを生成します。

$yourCar = new Car($body);
$yourCar->body->color = 'blue';

もう1つインスタンスはボディの色を青にします。

ここで

echo $myCar->body->color . PHP_EOL;

とすると、

blue

と出力されてしまいます。

$yourCarの色を変えたつもりが$myCarまで色が変わってしまいました。

修正すべき点

同じ$bodyを使いまわしていることも原因の1つですが、そもそもCarBodyもMutableなクラスです。

どちらもプロパティがpublicになっている為、外部からアクセス可能になっています。

その為、上記のコードのように後から変更することが可能になってしまいます。

これを防ぐにはプロパティにはコンストラクタで値を入れ、後から変更ができないようにすることです。

例えば、

class Body
{
    private string $color;

    public function __construct(string $color)
    {
        $this->color = $color;
    }
}

このようにプロパティをprivateにし、外部からアクセスできないようにした上でコンストラクタで値を入れる。

一旦値を入れたらそのインスタンスは後から変更することができません。

こうすることで不変性を担保することができ、思わぬバグが生まれにくくなります。

同様にCarクラスも$bodyプロパティをprivateにすることで後から変更できないようにします。

class Car
{
    private Body $body;

    public function __construct(Body $body)
    {
        $this->body = $body;
    }
}

また、Carインスタンスの生成にFactoryパターンを使うとよりベターです。

class CarFactory
{
    public function createNewCar(string $color = 'red'): Car
    {
        return new Car(new Body($color));
    }
}

こうすることでBodyインスタンスの使い回しも防ぐことができ、より堅牢なコードにすることができます。

まとめ

以上簡単ではありますが、Immutableについて書いてみました。

プログラミングの世界ではImmutableがデファクトスタンダードになってきているように感じます。

JavaScriptでも変数宣言の際、以前はvarしかありませんでしたが、ES6以降はconstletを使うことがもはや常識となっています。

他にもRustではImmutableがデフォルトになっているようです(触ったことは無いですが)。

バグを生まないためにも、Immutableなクラスを作ることを意識して実装することが重要だと思います。

Mutableな構造、つまり再代入や変更が可能な構造になっている場合にはImmutableな構造にできないか再検討した方が良いように思います。