バニラ php で学ぶクリーンアーキテクチャ その 3 - Web 編

前回はクリーンアーチテクチャ (CA) の実装例として コマンドライン (CLI) のツールを作りました。 今回はそれを Web に変更する事で CA の良いところを見ていきます。

TLDR

  • CA で作ると CLI から Web にしても "外側" だけの変更で済むよ
  • 修正範囲も最少限にできるよ

仕様

CLI 版の仕様はこんな感じでした。

  1. 実行すると、指定された商品情報に税込価格を追加して表示する
    • 税込価格は軽減税率を考慮して算出する
  2. コマンドラインツールとして作る。商品 ID を引数で指定する
  3. 商品情報は LTSV 形式で標準出力に表示する
  4. 商品情報として以下の項目がストレージに保存されている
    • 商品 ID
    • 商品名
    • 税抜価格
    • 軽減税率対象か
  5. ストレージはメモリ上に保存される

Web に作りかえるにあたり、以下のように 2, 3, 5 を変更します。

  1. 実行すると、指定された商品情報に税込価格を追加して表示する
    • 税込価格は軽減税率を考慮して算出する
  2. [変更] Web アプリケーションとして作る。商品番号は GET パラメータで指定する
  3. [変更] 商品情報は JSON 形式で返す
  4. 商品情報として以下の項目がストレージに保存されている
    • 商品 ID
    • 商品名
    • 税抜価格
    • 軽減税率対象か
  5. [変更] ストレージはファイルに保存される

実装

Web 版に変更したものが web ブランチ にあるので、 順番に見ていきます。

CLI 版から変更があったもののみ説明します。

main 関数

CLI 版同様、オブジェクトの生成と呼び出しのみ担当しています。 以前は ItemCommand を生成していましたが、 今回は ItemServer を作って実行しています。

Server

main 関数と Controller のつなぎをやります。 この場合、以下をおこなっています。

  • main 関数から渡された GET パラメータを取得する
  • Controller を呼び出す
  • Controller が正常終了した場合、 応答コードと Controller から返ってきた文字列を出力する
  • 例外が発生した場合、例外に合わせた応答コードとメッセージを出力する

Controller

Laravel など、一般的な Web フレームワークのコントローラと同じです。 以下をおこなっています。

  • リクエストからパラメータを取得
  • バリデーションを実施
  • Interactor (ユースケース) の呼び出し
  • 得られた値を Presenter に渡し、表示用に加工

Validator

入力された引数が正しいか検査し、正規化して返します。 渡されるパラメータの形式が違うので、その修正をおこなっています。

StorageClient

CLI 版ではメモリに保存していたデータを ファイルに保存する事としたため、追加し、 逆に、CLI 版であった MemoryStorageClient.php は削除しています。

インタフェースに変更はありません。

Presenter

渡された値を表示用に変換します。 CLI 版では LTSV に変換していましたが、 ここでは JSON に変換しています。

まとめ

以上が変更したクラスの説明となります。

ここで、 diff で変更されたファイルを確認すると、 CLI 版から変更したクラスは Infrastructure, Adapter 層のいわゆる "外側" に属するクラスのみで、 "内側" に属するクラス(UseCase, Entity)には変更が入っていません。

CLI から Web という、比較的大きな変更をしたにも関わらず、 変更した箇所は最低限に抑えられており、 CA の "大切なものを些細なものに依存させない" が有用な考え方である事が示されています。

CA は

  • クラスが増えがち
  • 慣れないとどこに書くか迷ってしまう

な点もありますが、上記のように良い面もありますので、 うまく使っていきたいですね。

バニラ PHP で学ぶクリーンアーキテクチャ その 2

前回はクリーンアーキテクャの各レイヤの説明をしました。 今回は実装の説明をしていきます。

今回はソースコードの説明をします。 まずは前回の商品情報管理ツールの仕様を再掲。

  1. 実行すると、指定された商品情報に税込価格を追加して表示する
    • 税込価格は軽減税率を考慮して算出する
  2. コマンドラインツールとして作る。商品 ID を引数で指定する
  3. 商品情報は LTSV 形式で標準出力に表示する
  4. 商品情報として以下の項目がストレージに保存されている
    • 商品 ID
    • 商品名
    • 税抜価格
    • 軽減税率対象か
  5. ストレージはメモリ上に保存される

こんな仕様でした。 では、実装を順に説明していきます。 リンクを張っておくので、ソースコードを見ながら 説明を読むとよいかもです。

main 関数

書籍のクリーンアーキテクチャの 26 章にも出てくる 最も抽象度の低い関数です。 オブジェクトの生成と呼び出しのみ担当しています。

Command

main 関数と Controller のつなぎをやります。 この場合、以下をおこなっています。

  • main 関数から渡された引数から必要なもののみ抜き出す
  • Controller を呼び出す
  • Controller から返ってきた文字列を標準出力に表示
  • 例外が発生した場合、メッセージを標準エラー出力に表示

Controller

入力された引数を受け取って、 Interactor を呼び出します。 その後、Presenter でフォーマットしたものを外部に出力しています。 Presenter では Interactor から取得した結果を LTSV 形式に変換しています。

Validator

入力された引数が正しいか検査し、正規化して返します。

Interactor

入力された商品 ID を元に リポジトリから商品 Entity を取り出します。 その後、消費税計算ロジックを呼び出し、結果を Controller に返します。

Repository

本体は Adapter 層ですが Interactor から利用したいので、 インタフェース(IItemRepository)を作っています。 商品情報を取得し、Entity に変換して返します。

StorageClient

メモリ上で商品情報を管理します。 本体は Infrastructure 層ですが Repository から呼び出したいので、 インタフェース(IStorageClient)を作っています。

Entity, Logic

商品を示すクラス(ItemEntity)と それにまつわるビジネスロジックです。

今回、Logic には消費税の計算ロジックを実装します。

※ Entity にビジネスロジックを書くやり方もありますが、 今回は両者を分けています。

Presenter

Controller で呼び出され、 Entity を表示形式(この場合 LTSV)に変換します。

ユニットテスト

以下が今回作成したユニットテスト(UT)になります。

各クラスの責務が明確なので、読み書きしやすいと思います。 今回は外部との入出力をおこなわないメソッドについてのみ UT を作っています。 こうすることで、モックなしてテストができ、 テストケースの作成・保守がかなり楽になります。

まとめ

クリーンアーチテクチャを実現した ソースコードの説明をしました。 各ファイルの use 文を見てもらえれば参照方向が Infrastructure -> Adapter -> Application -> Domain となっており、逆向きの参照がない事が分かると思います。

という事で、 「方針(policy) を詳細(detail) に依存させない」 実装になっていると思います。

※ 注意として、こればあくまで私の解釈で実装したものであり、 これが唯一の正解ではありません。

サンプルの動かし方

実際に動かしてみたり、変更してみたりするとよいかもです。 PHP 7.4.9 で動作確認しています。

$ git clone https://github.com/yosugi/clearn-architecture-with-vanilla-php.git
$ cd clearn-architecture-with-vanilla-php
$ composer install
$ php ./src/main.php 1
id:1 name:itemA price:100 isReducedTaxRate:false priceIncludingTax:110
$ php ./src/main.php 99
item id 99 is not found.

バニラ PHP で学ぶクリーンアーキテクチャ その 1

クリーンアーキテクチャの概要を PHP のサンプルコードを見ながら学びます。

対象

  • クリーンアーキテクチャの記事や本を見たけど腑に落ちなかった
  • クリーンアーキテクチャの概念は分かったが実際にどう実装していくか、動くコードを見たい

基本的には素の PHP ですが、 ある程度モダンな環境にしたかったので Composer, PHPUnit, PHP-CS-Fixer, PHPStan は入れてます。 大規模なフレームワークを使ってないくらいに考えてください。

題材

例として、商品情報管理ツールを作ります。

  1. 実行すると、指定された商品情報に税込価格を追加して表示する
    • 税込価格は軽減税率を考慮して算出する
  2. コマンドラインツールとして作る。商品 ID を引数で指定する
  3. 商品情報は LTSV 形式で標準出力に表示する
  4. 商品情報として以下の項目がストレージに保存されている
    • 商品 ID
    • 商品名
    • 税抜価格
    • 軽減税率対象か
  5. ストレージはメモリ上に保存される

リポジトリここ にあります。 記事を読みながら見るとよいです。

クリーンアーキテクチャのレイヤ

クリーンアーキテクチャといえば、 元記事の図 が有名ですが、ここで 以下のレイヤが定義されています。

  • Frameworks & Drivers
  • Interface Adapters
  • Application Business Rules
  • Enterprise Business Rules

それぞれについて説明します。

Frameworks & Drivers

  • html を生成するテンプレートエンジン
  • DB にアクセスするライブラリ
  • web フレームワーク

などの具体的な処理をおこないうものが含まれます。 ソースでは src/Infrastructure/ が対応します。 今回は引数の処理とストレージの実装をおこなっています。

Interface Adapters

外部との仲介をおこないます。

  • リクエストパラメータのバリデーション、正規化
  • ロジックの呼び出し
  • 出力のフォーマット
  • ストレージからデータを取得し、Entity に変換

などを担当します。 ソースでは src/Adapter/ が対応します。

今回は

をおこなっています。

Application Business Rules

アプリケーション固有のビジネスロジックを実装します。

ソースでは src/Application/ が対応します。

今回は以下をおこなっています。

  • ストレージへのアクセス
  • ドメインロジックの呼び出し
  • エラー時の制御

Enterprise Business Rules

アプリケーションによらないビジネスロジックを実装します。 分かりにくいですが、私の解釈では 「アプリケーションがなくても存在するようなロジック」 をここに書きます。 今回の場合だと「消費税率の計算」は アプリがなくても存在する(法律で規定されている) ため、ここに記述しています。

ソースでは src/Domain/ が対応します。

今回は税込価格の計算ロジックを実装しています。

依存の向きと依存性逆転の原則について

方針 (Policy) を詳細 (Detail) に依存させないために、 依存性逆転の原則 (DIP) を使います。

各レイヤは Detail -> Policy の順に

Frameworks & Drivers -> Interface Adapters -> Application Business Rules -> Enterprise Business Rules

となっており、この逆の依存はさせないようにします。 元記事 の図でいうと、外 -> 中は OK ですが、中 -> 外への依存は NG になります。

とはいえ、ビジネスロジックから DB 等にアクセスする事はあるので、 その際は Interface を定義し、それに依存するように実装します。

今回の場合 ItemInteractor (ビジネスロジック)から ItemRepository (DB 等にアクセス) を使う必要がありますが、 直接は参照できないため、IItemRepository インタフェースを定義して ItemRepository には依存させないようにしています。

実装でも ItemInteractorIItemRepository インタフェースのみを参照しています。 また、ソースを 'use' で検索しても外 -> 中の参照しかないことが分かります。

このようにすることで、例えばストレージが DB から Web API に変わったとしても、 内部(Application, Domain) への影響をおさえる事ができます。

まとめ

という事で、今回は各レイヤの説明をしました。 次回、ソースコードの説明をします。

サンプルの動かし方

実際に動かしてみたり、変更してみたりするとよいかもです。 PHP 7.4.9 で動作確認しています。

$ git clone https://github.com/yosugi/clearn-architecture-with-vanilla-php.git 
$ cd clearn-architecture-with-vanilla-php
$ composer install
$ php ./src/main.php 1
id:1 name:itemA price:100 isReducedTaxRate:false priceIncludingTax:110
$ php ./src/main.php 99
item id 99 is not found.

PHP でも関数型っぽく書く

なんとなく表題の事をやってみた。 とりあえず、

  • 関数合成
  • 部分適用

があればなんとかなりそうなので、作ってみる。

// 関数合成
function compose()
{
    $fns = func_get_args();
    $revFns = array_reverse($fns);
    return function ($initial) use ($revFns) {
        return array_reduce(
            $revFns,
            function ($acc, $fn) {
                return call_user_func($fn, $acc);
            },
            $initial
        );
    };
}

// 部分適用
function partial()
{
    $args = func_get_args();
    $fn = array_shift($args);

    return function () use ($fn, $args) {
        $newArgs = func_get_args();

        $allArgs = array_merge($args, $newArgs);
        return call_user_func_array($fn, $allArgs);
    };
}

できたので、いつもの偶数二乗和を求める関数を作ってみる。

// 偶数二乗和を求める
function evenSquareSum($n)
{
    // 最後の引数が配列になるように引数の順番を合わせておく
    $filter = function ($callback, $array) {
        return array_filter($array, $callback);
    };
    $map = function ($callback, $array) {
        return array_map($callback, $array);
    };
    $reduce = function ($initial, $callback, $array) {
        return array_reduce($array, $callback, $initial);
    };

    // 配列を一つ受け取り、偶数のみ抽出、各要素を 2 乗する、全ての要素を合計する関数を作成
    $selectEven = partial($filter, function ($n) { return $n % 2 == 0; });
    $squareEach = partial($map, function ($n) { return $n * $n; });
    $sum = partial($reduce, 0, function ($acc, $n) { return $acc + $n; });

    // それぞれの関数を合成し、偶数二乗和を計算する関数を作る
    $evenSquareSum = compose(
        $sum,
        $squareEach,
        $selectEven
    );

    $numList = range(1, $n);
    return $evenSquareSum($numList);
}
$ret = evenSquareSum(10);
var_dump($ret); // => 220

動いてそう。 これのカリー化バージョン作って比べてみると 部分適用との違いとか分かりやすいかも。

go の配列操作ライブラリを探す

go でそれなりに複雑なロジックを実装する時に欲しくなってくるのが配列操作ライブラリ。 何でも for でやるのが go way なのかもしれませんが、XXX.filter(...).map(...) とかやりたいですよね。 lodash みたいなのはないかと思って探してみたところ、一応いくつかあるみたいでした。

が、どれもリフレクションを使う必要があったり、コードを自動生成したりで、イマイチでした。 今のところ generics がないのでしょうがないかもしれないですが、 さらに探してみたところ、 go-linq というライブラリを見つけました。 LINQ は元々 C# などで使えたもので、それの Go 版みたいです。 これが type assersions を使う事なく使えるみたいなので試してみました。

package main

import (
    "fmt"

    linq "github.com/ahmetb/go-linq"
)

type MyNum struct {
    Num int
}

func main() {

    // MyNum のスライスを生成
    var nums []MyNum
    linq.Range(1, 5).
    SelectT(func (num int) MyNum {
        return MyNum{Num: num}
    }).
    ToSlice(&nums) // <- 変数に直接入れられる!!

    // いつもの偶数二乗和
    var result MyNum
    result = linq.From(nums).
    WhereT(func (myNum MyNum) bool { // <- 引数が interface{} でなくていい!!
        // 偶数のみ抽出
        return myNum.Num % 2 == 0
    }).
    SelectT(func (myNum MyNum) MyNum { // <- 戻り値も interface{} でなくていい!!
        // 二乗する
        num := myNum.Num
        return MyNum{Num: num * num}
    }).
    AggregateT(func (acc, myNum MyNum) MyNum {
        // 合計する
        return MyNum{Num: acc.Num + myNum.Num}
    }).(MyNum) // Aggregate の返り値は type assersion 必要みたい。おしい

    fmt.Printf("%#v\n", result) // -> main.MyNum{Num:20}
    fmt.Printf("%#v\n", result.Num) // -> 20
}

なかなかよさそうですね。

  • map -> Select
  • filter -> Where
  • reduce -> Aggregate

と名前はちょっと違うものの、当面これを使ってみます。

webpack で実行可能な単一ファイルを作る

node.js で cli ツールを作った時に 簡単に実行する方法を考えてみました。 nexe を使う方法もありますが、ファイルサイズが大きくなるので javascript のコードのみまとめる事を想定します。

方針としては

  • ライブラリも使いたいので webpack で単一ファイルにまとめる
  • まとまったファイルに shebang をつける
  • ファイルを path の通った所に置いて実行権限をつける

て感じです。 これを node.js で実現するには以下をやります。

  • webpack で単一ファイルにまとめる
  • webpack.BannerPlugin で shebang を追加する
  • ファイルを path の通った所に置いて実行権限をつける

実行するスクリプトを作成する

いつも通り node.js の package を作ります。

$ mkdir webpack-test && cd webpack-test
$ npm init -y
$ npm install --save lodash
$ mkdir src
$ vim src/index.js

ソースを src/index.js に書いたのは後で使う webpack のデフォルト設定に合わせるためです。 src/index.js の内容は webpack の Getting Started を 参考に、こんな感じで。

const _ = require('lodash');
const main = () => {
   const message = _.join(['hello', 'world'], ' ');
   console.log(message);
};
main();

試しに実行してみます。

$ node index.js
hello world

表示されました。

webpack で単一ファイルにまとめる

webpack をインストールして実行してみます。

$ npm install --save-dev webpack webpack-cli
$ npx webpack
...
$ node ./dist/main.js
hello world

./dist/main.js が webpack によって出力されたファイルです。 中身を見ると、lodash もこのファイルに入っている事が分かります。 これで 単一ファイルにはできましたが、 実行可能にするには shebang を追加する必要があります。

実行可能にする

shebang を追加するための webpack の設定をします。 以下のようにコマンドラインでやってもいいのですが、

$ echo '#!/usr/bin/env node' | cat - ./dist/main.js > ./dist/main

今回は webpack の BannerPlugin を使い、さらにファイルに実行権限を追加するところまで自動でやります。 webpack.config.js を作り、以下のようにします。

const webpack = require('webpack');

module.exports = {
    mode: 'production', // to minify
    target: 'node',
    plugins: [
        new webpack.BannerPlugin({
            banner: '#!/usr/bin/env node', // add shebang
            raw: true,
        }),
        function() {
            this.hooks.done.tap('chmod', () => {
                fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
            });
        },
    ],
};

もう一度 webpack を実行し、 直接実行できるか試してみます。

$ npx webpack
$ ./dist/main.js
hello world

実行できました。 あとはこのファイルを PATH の通った所に配置し、 ファイル名を適当に変えれば OK です。

補足: hook の書き方

chmod する所は最初は

function() {
    this.plugin('done', () => {
        fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
    });
},

と書いていたんですが、

$ nps webpack
DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead

のようなメッセージが出たので調べたところ、 最初の例の書き方に変わったみたいです。

バージョン

$ node -v
v10.13.0
$ npx webpack -v
4.26.1

node.js でライブラリを使わずに関数型する

node.js で関数型したい時には Ramda などの ライブラリを入れるのが一般的だと思いますが、 ちょっとしたツールを書く時には Ramda 入れるのも大げさに思える時があります。

で、よくよく調べてみると、map, filter, reduce はあるし、 部分適用も Function.prototype.bind() を使えばよさそう。

あとはカリー化と関数合成ができればいいんだけど、 関数合成については reduce を使えばいける事に気付いて こんな感じで定義してみました。

const pipe = (...fns) => val => fns.reduce((acc, fn) => fn(acc), val);
const compose = (...fns) => val => fns.reduceRight((acc, fn) => fn(acc), val);
const bind = (fn, ...args) => fn.bind(null, ...args);

こんな感じで使えます。

const add = (a, b) => a + b;
const mul = (a, b) => a * b;

const add1 = bind(add, 1);
const mul2 = bind(mul, 2);

let ret = 0;
ret = pipe(
    add1,
    mul2
)(2);
console.log(ret); // -> 6

ret = compose(
    add1,
    mul2
)(2);
console.log(ret); // -> 5

カリー化はいい方法が見つかってないですが、 必要な場合は手動でやってます。

const addCurried = a => b => a + b;
const add10 = addCurried(10);
ret = add10(1)
console.log(ret); // -> 11