Chapter 09

イミュータブルは最高だ

とっくり
とっくり
2022.06.14に更新

第3回「純粋関数は最高だ」では、引数のみが入力、返り値のみが出力である「純粋関数」は扱いやすいので、プロダクト中の純粋関数の割合を増やしたいという方針を示しました。

https://zenn.dev/tockri/books/dcaf6c55e64448/viewer/20a23e

副作用の種類

さて、副作用には様々なパターンがあります。

  • ネットワークやファイル、DBなどのI/O操作
  • メンバー変数を変更する
  • 引数で与えたオブジェクトを変更する
  • 別スレッドを発生させる etc

任意のプログラミング言語で、ある関数のコードを見ないでそれが純粋関数かどうかを判定する方法は残念ながらありません。そのため、上記の方針を理解していなかったり忘れてしまったメンバーによって、最初は純粋関数だった関数がいつのまにか純粋でなくなっていた、ということが起こりえます。

リファクタリングの悲劇

たとえば、あるページを表示するための情報を持つPageDTOというクラスがあって、設定ファイルとHTTPリクエストの情報を組み合わせて構築することになっているとします。

// 今回はサンプルコードをPHPにしてみます!

/**
 * ページを構成する情報DTO
 */
class PageDTO {
  private $xx;
  private $yy;
  function getXX() {
    return $this->xx;
  }
  // 設定ファイルから値をセット
  function setXX($xx) {
    $this->xx = $xx;
  }
  function getYY() {
    return $this->yy;
  }
  // HTTPリクエストから価をセット
  function setYY($yy) {
    $this->yy = $yy;
  }
}

共通部分で設定値からDTOを生成する関数と

function createDTOFromConfig(Config $config) {
  $dto = new PageDTO();
  $dto->setXX($config->getXX());
  return $dto;
}

リクエストをもとにDTOを完成させる

function makeupDTO(PageDTO $dto, Request $req) {
  $ret = clone $dto;    (引数を変更しないように新しいオブジェクトを作って返す)
  $ret->setYY($req->getYY());
  return $ret;
}

という純粋関数があったとします。単体テストもしっかり書いています。「できるだけ純粋関数で書こう」という約束事があるので、引数で与えた$dtoが関数実行後に変化していない、といったテストはわざわざ書いていません。

これを誰かが(このオブジェクトcloneしてるの意味なくね?)と考えて

function makeupDTO(PageDTO $dto, Request $req) {
  (メモリ節約のためcloneするのやめた)
  $dto->setYY($req->getYY());
  return $dto;
}

と変更したとします。おそらく返り値をチェックしている単体テストはそのまま通ります。そして、その後、リファクタリングや仕様追加の結果、makeupDTO()に渡したあとの$dtoをもう一度利用したりすると(たとえばログに出すなど)思いも寄らない不具合が発生しますが、どこが原因か追うのがとても難しいことでしょう。

インターフェイス仕様で悲劇を防ぐ

このような悲劇を産まないために、イミュータブル(変更できない)オブジェクトの出番です。PageDTOのつくりを、古典的な getter 、 setter の形でなく、setter のないイミュータブルにします。

/**
 * ページを構成する情報DTO
 * イミュータブル。
 */
class PageDTO {
  private $xx;
  private $yy;
  function __construct($xx, $yy) {
    $this->xx = $xx;
    $this->yy = $yy;
  }
  function getXX() {
    return $this->xx;
  }
  function getYY() {
    return $this->yy;
  }
}

イミュータブルなクラスのインターフェイスはたいてい、コンストラクタで値を与え、それ以外に内部変数を変更するメソッドを持ちません。メンバーの $xx$yy が参照型ならそれらもイミュータブルなクラスだとなお良いですが、言語やフレームワークが提供していないものをあまり厳密に求めすぎてもつらくなるので、ほどほどにしましょう。チームメンバーで約束事が守れればそれでいいのです。

(※ 例えばPHPの場合、Laravelが提供するオブジェクトはたいていミュータブルなので無理に全てをイミュータブルにしようとするとつらいです)

使用側のコードはこのようになります。

function makeupDTO(PageDTO $dto, Request $req) {
  return new PageDTO($dto->getXX(), $req->getYY());
}

慣れている人にとっては、「ああ、PageDTOはイミュータブルなんだな」と察しやすくもなります。

メモリ使用量増加?

いろんなクラスをイミュータブルにしていくと、このように、ちょっと値を変更するだけでも new でインスタンスをコピーするコードが増えます。それではメモリを食うんじゃないか、と心配する向きもあるでしょう。

メモリは、余分に食います。

ただしほとんどの場合、メモリを食うデメリットよりもコードが簡潔になる、バグが減るといったメリットのほうが遥かに大きいです。

またイミュータブルが保証されていることにより、安心してオブジェクトのシャロー(浅い)コピーで参照を使い回せるため省メモリになるケースもあります。