1. Qiita
  2. 投稿
  3. PHP

PHPerがドメイン駆動設計と関数型プログラミングを学んで得たもの(前編)

  • 12
    いいね
  • 0
    コメント

はじめに

動機

先日こんな記事を書きました。

ドメイン駆動設計と関数プログラミングをScalaで - Qiita

ドメイン駆動設計と関数プログラミングをElixirで - Qiita

Scala にしても Elixir にしても、純粋な関数型プログラミング言語ではなくて、オブジェクト指向の要素も取り入れたマルチパラダイムな言語だと思いますが、一方で旧来のオブジェクト指向な言語に関数型のパラダイムが入り込んできているという傾向 (map, reduce とかラムダとか) もあり、これから先も、両者が共に近づいてそれぞれのいいところを活かしながら発展し、ソフトウェア開発における生産性や変更容易性が上がっていくのかなぁと期待が膨らみます。

この記事では、関数型の何がいいのか、PHP + オブジェクト指向で書くにあたって、何か取り入れられるところはないか、という点について、ドメイン駆動設計のイディオムも交えながら、自分なりにまとめてみたいと思います。

ドメイン駆動設計については、ドメイン駆動設計という単語が明示的に使われることがなくなるほどに浸透してほしい、と思うくらいに素晴らしいコンセプトだという確信を得たので、本稿ではサンプルコードの流用程度に留めます。

結論 (TL;DR)

PHPを使ったオブジェクト指向プログラミングに関数型パラダイムを混ぜることについては、利点もあるけれども、今すぐ導入すべきとは言えない、という結論に達しました。

そもそも、関数型言語のオブジェクト指向言語に対する最大の優位性は、並行・分散処理において純粋関数の持つ特性が効力を発揮する、という点だと思っていて、Java ならまだしも、PHP においてはその恩恵が受けられることはほぼないですから、少なくともウェブアプリケーションで使われるのであれば、メリットは少ないでしょう。

もちろん、多くの関数型言語の持つ、変更容易性を高める機構の一部を取り入れて、より (ただしそれほど劇的ではない) 堅牢なプログラムを書くことはよいことだ、とも思います (チームで開発しているなら、メンバーのスキルや意向を考慮して判断するといいでしょう)。

というわけで、ポエムの範疇を出ていないかもしれませんが、それでもなおご興味ある方は以下をお読みください。

関数型プログラミング言語の特徴

関数型プログラミング言語の特徴は、ざっくりこんな感じかなぁと思っております。

  • 参照透過な関数
  • 不変な変数
  • 高階関数
  • パターンマッチ
  • 遅延評価

(カリー化とか関数の部分適用とかモナドとかもありますが、PHP に適用するにはハードルが高いので除外します)

これらのほとんどは、PHP (遅延評価(注)は 5.5 以上) においても実現可能なので、どういった点が生産性や変更容易性を高めてくれそうか、以下に、サンプルコードを示しながら、どのあたりを取り入れていけばいいのかを、探っていきたいと思います。

: PHPは正格評価な言語なので、遅延評価に関してはジェネレータのみを取り上げます

環境

PHP 7.0.5

参照透過な関数と不変な変数

このふたつはセットで取り上げた方がよさそうなので、一緒にしました。

関数が参照透過である、というのは、ざっくり言うと、入力が同じなら出力が常に同じになる、ということです。関数を参照透過にすることにより、テストが容易になり、意図しない不具合を減らすことができます。

一方、参照透過でない関数の場合、外部の状態に依存したり、逆に外部の状態を変更したりするので、それらの影響をすべて考慮しなければならなくなります。

副作用のあるコード.php
$object = new SomeObject();
// この間に $object に対する何らかの処理があり、その結果によって calculate の返す値が違ってくる
// また、calculate が関数内で外部状態を変更していると、それに依存している他のオブジェクトにも影響が出る
// calculate の処理が依存している「状態」が増えれば増えるほどテストは困難になり、不具合も出やすくなる
$result = $object->calculate();

関数型に限らず、オブジェクト指向においても、関数が依存する外部の状態を極力減らすこと (低結合) が保守性を高めるために必要な施策です。

一方、変数が不変である、というのは一見矛盾していますが、一度初期化された変数が以降変更されない、ということです。

ドメイン駆動設計で言うところの「値オブジェクト」を操作するとき、インスタンスを不変とし、演算に参照透過な関数を使うことができます。

ただ、オブジェクト指向で参照透過な関数を使うことにこだわると、オブジェクト指向のメリットを削ぐことにもなる (状態を内包していることがオブジェクト指向のメリットだと考えます) ので、盲目的に導入するのは避けたいです。

コードを見ながら考えましょう。

まず、以下の add のような関数は参照透過ではありません (サンプルコードは「実践ドメイン駆動設計」1から拝借し、PHPに置き換えた上で一部改変しています)。

<?php
declare(strict_types=1);

namespace App;

class MonetaryValue
{
    private $amount;
    private $currency;

    public function __construct(int $amount, string $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function add(MonetaryValue $m)
    {
        // $this->currency と $m->currency が違った場合は $m を変換する必要があるけど省略
        $this->amount += $m->getAmount();
    }

    private function getAmount(): int
    {
        return $this->amount;
    }
}

$this->amount の値が変われば、入力 $m が同じでも出力が変わってきます。

これを、

    public static function add(MonetaryValue $m, MonetaryValue $n): MonetaryValue
    {
        // $m->currency と $n->currency が違った場合は $n を変換する必要があるけど省略
        return new MonetaryValue($m->getAmount() + $n->getAmount(), $m->currency);
    }

とすれば参照透過な関数になるわけですが、これではオブジェクト指向の良さ (状態と振る舞いをカプセル化する) が失われてしまいます。

インスタンス自体がメソッドを持っているのに、わざわざ static メソッドを呼ばないといけない点が、煩わしいというか不自然な感じになっています (これを不自然と思わないのであれば、この形式でもぜんぜんいいと思います、関数型パラダイムにこだわる必要がないのと同様にオブジェクト指向のパラダイムにこだわる必要もありません)。

$m1 = new MonetaryValue(100, 'JPY');
$m2 = new MonetaryValue(200, 'JPY');
$m3 = MonetaryValue::add($m1, $m2);
// App\MonetaryValue {
//  -amount: 300
//  -currency: "JPY"
//}

落とし所としては、以下のように、

    public function add(MonetaryValue $m): MonetaryValue
    {
        // $this->currency と $m->currency が違った場合は $m を変換する必要があるけど省略
        return new MonetaryValue($this->getAmount() + $m->getAmount(), $this->currency);
    }

とすれば、プロパティが変更されないことを条件に、イミュータブルな値オブジェクトになります (これはエリック・エヴァンスの本でも塗料を混合する例で似たような構造になっていたかと思います)。

$m1 = new MonetaryValue(100, 'JPY');
$m2 = new MonetaryValue(200, 'JPY');
$m3 = $m1->add($m2);
// App\MonetaryValue {
//  -amount: 300
//  -currency: "JPY"
//}

値オブジェクトとその演算をファンクショナルに実装するときのポイントは、

  • 値オブジェクトのクラスがイミュータブルである (初期化された値が変更されないことが保証されている) こと
  • 演算結果は新たにインスタンスを生成して返すこと

ですが、PHP ではプロパティを読み取り専用にしたり、変更不可にするような修飾子はありませんので、初期化された値が変更されないことを保証できません。なので、

  • setter メソッドを作らず、外部から値を変更できないようにする
  • 内部でもプロパティを変更しないようにする
  • 演算結果は新たにインスタンスを生成して返す

ということになろうかと思います。めんどくさいですね。

PHP にも val 修飾子が導入されることを願うばかりです。

高階関数

高階関数は、ざっくり言うと、関数を引数にしたり、戻り値にしたりする関数のことです。

function higherOrderFunction(callable $function): callable
{
    return function (int $x) use ($function) {
        return $function($x);
    };
}

$func = higherOrderFunction(function (int $x) {
    // $x を使った処理
});

$ret = $func($someInt);

array_map と foreach

関数型プログラミングでは、ループを使わずに関数を引数に渡してコレクションを操作したりします。

array_map.php
$incremented = array_map(function ($n) {
    return $n + 1;
}, [1, 2, 3, 4, 5]);
// => [2, 3, 4, 5, 6]

これの何がうれしいのか、というと、foreach を使ったバージョンと比較すると、

foreach.php
$incremented = [];
foreach ([1, 2, 3, 4, 5] as $n) {
    $incremented[] = $n + 1;
}
// => [2, 3, 4, 5, 6]

array_map を使ったバージョンは処理が関数内で閉じているのに対し、foreach を使ったバージョンでは、開いている (ブロック内に関係のない処理をいくらでも追加できてしまう) ことが分かるでしょう。

あるコレクション (ここでは配列) に対し、何らかの処理を施したい場合、より堅牢なのはどちらのバージョンか、と考えたとき、関数に切り出して処理を局所化している array_map バージョンがより堅牢である (高凝集) と言えないでしょうか。

また、関数として処理をくくりだすことで、意図が明確になる、という利点もあります。上記のように無名関数ではなくメソッドを引数に取れば、コレクションに対する操作が局所化される上に、名前によって操作の意図が明確になるので、よりビューティフルなコードになるんじゃないでしょうか。

array_map.php
class Incrementor
{
    public function __invoke(int $n): int
    {
        return $n + 1;
    }
}

$incrementor = new Incrementor();
$incremented = array_map($incrementor, [1, 2, 3, 4, 5]);
// => [2, 3, 4, 5, 6]

array_map や array_reduce より foreach の方が速い、という問題もありますし(注)、可読性にも問題がある、という意見もあります (個人的には慣れの問題が大きいとは思いますが)。

: 上記コードを要素数を 10,000 にして計測したところ、foreach バージョンと array_map バージョンは速度において約8〜10倍の差がありました。array_map に無名関数を使ったバージョンと、class を使ったバージョンは速度、メモリ使用量ともに大差なかったです。

ジェネレータ (yield) を使わない場合は、メモリ使用量の点でも問題になるケースが出てくるでしょう。

いずれにしても、これらの関数 (array_map, array_reduce, etc.) の利用は慎重に検討するのがよさそうです。

Strategy パターンとしての高階関数

別の高階関数の使い所として、引数に関数を渡すことで、処理の流れを外部から構築する、というものもあるので、そちらも例を交えて考えてみます。

「エリック・エヴァンスのドメイン駆動設計」2 から、貨物輸送アプリケーションにおける行程規模ポリシー (LegMagnitudePolicy) の例を拝借します (Java から PHP に置き換え、一部改変しています)。

これは、GoF本3に出てくる Strategy パターンの使用例ですが、「別名 ポリシー」と記載されており、実装例でも Policy という名前になっていたので、以下 Policy と呼びますが、Strategy パターンと使い方は同じです。

このアプリケーションでは、行程規模が最も小さい行程を計算してルート選択を行いますが、行程規模の導出方法が複数存在する、という仕様です。

LegMagnitudePolicy.php
<?php

abstract class LegMagunitudePolicy
{
    abstract protected function length(Leg $leg): float;

    public function __invoke(Leg $leg): float
    {
        return $this->length($leg);
    }
}

/**
* 行程時間規模
*/
class LegTimeMagunitude extends LegMagunitudePolicy
{
    protected function length(Leg $leg): float
    {
        // 規模を計算して返す
    }
}

/**
* 行程金額規模
*/
class LegMoneyMagunitude extends LegMagunitudePolicy
{
    protected function length(Leg $leg): float
    {
        // 規模を計算して返す
    }
}

この Policy オブジェクトを使って行程規模を計算するのは行程 (Itinerary) クラスで行います。

Itinerary.php
<?php

class Itinerary
{
    private $legs = [];

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

    public function magnitude(LegMagunitudePolicy $policy): float
    {
        return array_reduce($this->legs, function ($acc, $leg) use ($policy) {
            return $policy($leg);
        }, 0.0);
    }
}

さらに、呼び出し側は経路選択サービス (RouteService) に Policy オブジェクトを渡す仕様になっています。

Client.php

$policy = $policyFactory->factory(); // 何らかのロジックで Policy を選択する
$routeService->find($policy);

計算ロジックを外部から注入し、処理の一部を置き換え可能にしているのがまさに Strategy パターンですが、規模計算のパターンが増えるごとに Policy クラスが増えていくのが、やや大仰な感じもします。

ここで、PolicyFactory がクラスではなく関数を返すようにすると、Factory に計算ロジックのパターンを閉じ込めることができます。

PolicyFactory.php
<?php

class PolicyFactory
{
    public function factory(): callable
    {
        // 何らかのロジックで Policy を選択する
        // return [$this, 'legTimeMagnitude'];
        // return [$this, 'legMoneyMagnitude'];
    }

    public function legTimeMagnitude(Leg $leg): float
    {
        // 規模を計算して返す
    }

    public function legMoneyMagnitude(Leg $leg): float
    {
        // 規模を計算して返す
    }
}

ただし、大きなデメリットもあって、PHP では関数を型ヒントに使えないので (たとえば、「Leg を受け取って float を返す関数」という制約を与えられない)、すべて callable あるいは Closure になってしまうという点です。

たとえば、Scala では引数で受け取る関数のシグネチャを指定することができます。

def magnitude(policy: Leg => double): double

PHP ではこうなります。

Itinerary.php
public function magnitude(callable $policy): float

この制約 (制約を与えられないという制約) が、インタフェースを利用したポリモーフィズムに比べて脆い設計になってしまう恐れがあるので、この点においても、高階関数の利用に際しては十分に注意すべきと思います。

まとめ

というわけで、「結論」に書いたとおりではありますが、現時点では、PHP に関数型パラダイムを導入するメリットはあまりなさそうです。

とは言え、関数型プログラミングでは、オブジェクト指向プログラミングよりも高凝集・低結合なプログラムを書きやすい (というか、自然とそうなる)ようになっている、というのは身を持って体験した方がいい、と個人的には思います。

「パターンマッチ」と「遅延評価」は後編に書きます (あまり有益な内容にならなそうなので書かないかもしれない…)。

PHP で関数型プログラミングを取り入れるとこんなメリットがあるよ、という方がいたら、コメントいただければと思います。