BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

【PHP】DDDにおける値オブジェクトを変更したい時のメモリ周りについて調べた

こんにちは! バックエンドエンジニアの高町咲衣です!

この記事では、PHPでDDD(ドメイン駆動設計)を扱う際に気になる「値オブジェクトを更新=作り直した時のメモリ周りの挙動」について調査した結果をまとめています。

値オブジェクトは不変である

DDDの文脈における値オブジェクト(ValueObject)の特徴の一つとして、不変(immutable)であることが挙げられます。

値オブジェクトは「値を表現する」オブジェクトであり、例えばプリミティブな値であるint、stringなどと同じように取り扱うべきだとされています。

// プリミティブな値を用いた、ごく一般的な感覚のコード例

$number = 1; // 値をセットする

$number = 2; // 値を入れ直す

var_dump($number); // 2
var_dump($number === 1); // false
// プリミティブな値を用いて、値そのものを変更した例(実際にはこんなことできない)

$number = 1; // 値をセットする

$number->setValue(2); // 今入っている値そのものに変更操作を行なう

var_dump($number); // 1
var_dump($number === 1); // false

// 左辺の「1」は内部的に2で、右辺の「1」は内部的に1なので等しくない。わけがわからないよ

よって、次のようなコードはNGとされます。

// NGな値オブジェクトコード例

// 呼び出し元での操作
$valueObject = new ValueObject($value); // 値をセット
$valueObject->setValue($newValue); // 今入っている値そのものに変更操作を行なう

// 操作対象の値オブジェクト
class ValueObject
{
    private $value;

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

    public function setValue($value)
    {
        $this->value = $value; // 一度設定した値を変更してはいけない
    }
}

ではどうするのかというと、次の例のように「新たなインスタンスを生成して返す」ようにします。

// 一般的な値オブジェクトの変更コード例

// 呼び出し元での操作
$valueObject = new ValueObject($value); // 値をセット
$valueObject = $valueObject->setValue($newValue); // 値を入れ直す

// 操作対象の値オブジェクト
class ValueObject
{
    private $value;

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

    public function setValue($value)
    {
        return new self($value); // 値は変更せず、新しい自身のインスタンスを返却する
    }
}

なるほどですね!

しかしここで一つ、懸念が生まれます。

回数を重ねるとメモリはどうなる?

return new self($value) が何度も行われた時、メモリはどうなるのでしょう?

調べてみました!

まず前提として、ValueObjectを持つ集約Aggregateに対して操作を行なうものとします。

// 検証コード

// str_repeat("1234567890", 1024) は、10KBのデータを生成し、計測する値をある程度大きくすることで細かい誤差の影響を少なくします。

public function execute()
{
    echo '------START------'."\n";

    // 10KBの値を持たせる
    $aggregate = new Aggregate(str_repeat("1234567890", 1024));

    echo "\n------集約生成直後------\n";
    echo memory_get_usage()."\n";
    echo "------集約生成直後------\n";

    for ($i = 1; $i <= 10; $i++) {
        $aggregate->setValue(str_repeat("1234567890", 1024));

        echo "\n------{$i}回目の値セット------\n";
        echo memory_get_usage()."\n";
        echo "------{$i}回目の値セット------\n";
    }

    echo "\n------END------\n";
}

まずはNGとされる、値そのものを変更したパターンから

まずはNGとされている例から見ていきます。

// 集約クラス

class Aggregate
{
    private $value;

    public function __construct($value)
    {
        $this->value = new ValueObject($value);
    }

    public function setValue($value)
    {
        $this->value->setValue($value);
    }
}
// 値オブジェクトクラス

class ValueObject
{
    private $value;

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

    public function setValue($value)
    {
        $this->value = $value;
    }
}

オブジェクトの生成が何度も行われないので、メモリはそんなに使わなさそうな予感がします。 出力結果を見てみましょう。

// NGパターンの出力結果

>>> $test->execute();
------START------

------集約生成直後------
25078576
------集約生成直後------

------1回目の値セット------
25078656
------1回目の値セット------

------2回目の値セット------
25078752
------2回目の値セット------

------3回目の値セット------
25078816
------3回目の値セット------

------4回目の値セット------
25078944
------4回目の値セット------

------5回目の値セット------
25079008
------5回目の値セット------

------6回目の値セット------
25079136
------6回目の値セット------

------7回目の値セット------
25079136
------7回目の値セット------

------8回目の値セット------
25079264
------8回目の値セット------

------9回目の値セット------
25079392
------9回目の値セット------

------10回目の値セット------
25079392
------10回目の値セット------

------END------

値そのものを変更している=新規にオブジェクトを生成していないので、回数を重ねてもメモリ使用量に大きな変化はありませんね。 予想通りの結果であると言えます。

推奨される「値オブジェクトを毎回生成して入れ直す」パターン

次に、推奨されている(return new self($value) する)パターンです。

// 集約クラス

class Aggregate
{
    private $value;

    public function __construct($value)
    {
        $this->value = new ValueObject($value);
    }

    public function setValue($value)
    {
        $this->value = $this->value->setValue($value);
    }
}
// 値オブジェクトクラス

class ValueObject
{
    private $value;

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

    public function setValue($value)
    {
        return new self($value);
    }
}

こちらは懸念の通り、オブジェクトを毎回生成するのでメモリ使用量が比例的に上がってしまいそうな感じがします。 出力結果を見てみましょう。

// 推奨パターンの出力結果

>>> $test->execute();
------START------

------集約生成直後------
25078792
------集約生成直後------

------1回目の値セット------
25078872
------1回目の値セット------

------2回目の値セット------
25078968
------2回目の値セット------

------3回目の値セット------
25079032
------3回目の値セット------

------4回目の値セット------
25079160
------4回目の値セット------

------5回目の値セット------
25079224
------5回目の値セット------

------6回目の値セット------
25079352
------6回目の値セット------

------7回目の値セット------
25079352
------7回目の値セット------

------8回目の値セット------
25079480
------8回目の値セット------

------9回目の値セット------
25079608
------9回目の値セット------

------10回目の値セット------
25079608
------10回目の値セット------

------END------

おや?特にメモリ使用量が上がってはいませんね。 NGパターンとそんなに差がありません。

インスタンスは適宜破棄されている

PHPでは「参照のなくなったインスタンス」はガベージコレクションにより破棄され、自動的にメモリ解放されます。

確かにこのテストケースではreturn new self($value) した後「古いValueObject」を参照している箇所が存在しないので、インスタンスが破棄(=メモリ解放)されていそうです。

気になるので、インスタンスの破棄タイミングを見てみましょう。

破棄タイミングの調査にメモリ使用量の出力は必要ないので、テストコードを次のように書き換えます。

// テストコード

// メモリ使用量の出力をしないようにする

public function execute()
{
    echo '------START------'."\n";

    // 10KBの値を持たせる
    $aggregate = new Aggregate(str_repeat("1234567890", 1024));

    for ($i = 1; $i <= 10; $i++) {
        $aggregate->setValue(str_repeat("1234567890", 1024));
    }

    echo "\n------END------\n";
}

また、インスタンスの生成・破棄のタイミングを知りたいので、値オブジェクトクラスで適宜文字列を出力するように書き換えます。

// 値オブジェクトクラス

// インスタンスの生成と破棄のタイミングで文字列を出力する

class ValueObject
{
    private $value;

    public function __construct($value)
    {
        echo "\n-----------インスタンスを生成します-----------\n";
        $this->value = $value;
    }

    public function setValue($value)
    {
        return new self($value);
    }

    public function __destruct()
    {
        echo "\n-----------インスタンスを破棄します-----------\n";
    }
}

出力結果を見てみましょう。

>>> $test->execute();
------START------

-----------インスタンスを生成します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

-----------インスタンスを生成します-----------

-----------インスタンスを破棄します-----------

------END------

-----------インスタンスを破棄します-----------

なるほどなるほど。 最初の生成と最後の破棄を除くと、交互に生成・破棄が行われていることがわかります。

つまり、次のような動作をしているのではないかと思われます。

// 値オブジェクトクラスの動作予想

public function setValue($value)
{
    // インスタンスを生成する
    return new self($value);
}

// メソッドを抜けたら参照の消えた古いインスタンスが破棄される

まとめ

ということで、DDDの値オブジェクトの扱いで推奨されるreturn new self($value)は、メモリ的に心配はないということがわかりました!

もちろん他から参照があればインスタンスは破棄されませんが、そのあたりをちゃんと考慮した設計にしたいですね!