Ray.MediaQuery

# ドメインとインフラ層のDIP

この記事は [BEAR.Sunday Advent Calendar 2021](https://qiita.com/advent-calendar/2021/bear-sunday)の記事です。
以前の記事、[Ray.Di](https://qiita.com/koriym/items/55101291b72cda899871)で[Dependency Inversion Principle(DIP)、依存性逆転の原則](https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E9%80%86%E8%BB%A2%E3%81%AE%E5%8E%9F%E5%89%87)を紹介しました。

その原則に従えば、ドメインから特定の永続化コンポーネント、特定ORMの実装に依存することはどうでしょうか？

ドメイン層からインフラ層の実装に依存するのではなく、その逆、ドメイン層のインターフェイスにインフラ層の実装を合わせる事を特定の状況下でより効率的に行うためのパッケージがこの[Ray.MediaQuery](https://github.com/ray-di/Ray.MediaQuery)です。

# ドメインのインターフェイス

まずドメインはインフラ層の実装や仕様に対して無知に、自由にインターフェイスを設計します。例えば`todo`アイテムを追加するインターフェイスというものを考えてみましょう。

```php
interface TodoAddInterface
{
    public function add(string $id, string $title): void;
}
```

この永続化をデータベースで行うなら`DbQuery`属性を与えます。

```php
interface TodoAddInterface
{
    #[DbQuery('user_add')]
    public function add(string $id, string $title): void;
}
```

この永続化をWebAPIで行うなら`WebQuery`属性を与えます。

```php
interface TodoAddInterface
{
    #[WebQuery('user_add')]
    public function add(string $id, string $title): void;
}
```

ドメインのコードはインフラに対しての関心を持っていないのでメソッドシグネチャは不変で、属性だけが変わっています。

# 実装はノーコード

次にこのインタフェーイスを実装するクラスを用意するのはユーザーではなく`Ray.MediaQuery`の仕事です。このインターフェイスを利用するために必要なのはインジェクトする事だけです。

```php
class Todo
{
    public function __construct(
        private TodoAddInterface $todoAdd
    ) {}
}
```

インジェクトされたで"インフラストラクチャ操作オブジェクト"を利用します。

```php
    public function add(string $id, string $title): void
    {
        $this->todoAdd->add($id, $title);
    }
```

ドメインは操作の詳細に興味を持っていません。

## ランタイム

これがどのように実現されているかに興味がありますか？ Ray.DiとRay.AopそれにNullObjectを生成する技術で実現されています。簡単に説明します。

### 1. データーソースの特定

属性で指定された`user_add`を手がかりとして、`DbQuery`の場合は`user_add.sql`、`WebQuery`の場合は`https://{host}/{path/to/user_add}`を特定します。

### 2. 束縛

メソッドの引数名と値を束縛します。

`DbQuery`の場合、以下の引数`id`、`title`が

```php
public function add(string $id, string $title): void
```

"データーソースの特定で特定されたSQL"にバインドされ実行されます。複数のSQLを記述すればトランザクション実行されます。

```sql
INSERT INTO user (id, name) VALUES (:id, :name);
```

`WebQuery`の場合は`id={$id}&title={$title}`というようなクエリーまたはペイロードが作られ、初期化の時に指定したメソッドでリクエストされます。

## ジェネレートされるクラス

`ランタイム`で説明したクラスをジェネレートしてインジェクトすれば実現できそうですね。それも可能ですが冗長です。必要になってそのコードを読むのも大変そうです。

実際は実行コードをジェネレートする代わりに、Nullオブジェクトをジェネレートして、それに対して特定のインターセプターを束縛しています。

実際にジェネレートされているのはこのようなNullクラス（メソッドの中身がないクラス）です。

```php
class TodoAddInterfaceNull implements TodoAddInterface
{
    #[DbQuery('todo_add')]
    public function __invoke(string $id, string $title): void
    {
    }
}
```

これは別途作成した[koriym/null-object](https://github.com/koriym/Koriym.NullObject)で実現しています。これに対して[DbQueryInterceptor](https://github.com/ray-di/Ray.MediaQuery/blob/aa2fd925425039386fd7437fc43a262a2508b20e/src/DbQueryInterceptor.php#L40-L51)や[WebQueryInterceptor](https://github.com/ray-di/Ray.MediaQuery/blob/aa2fd925425039386fd7437fc43a262a2508b20e/src/WebQueryInterceptor.php#L39-L49)を束縛しています。

DBの場合

```php
    public function invoke(MethodInvocation $invocation)
    {
        $method = $invocation->getMethod();
        /** @var DbQuery $dbQury */
        $dbQury = $method->getAnnotation(DbQuery::class);
        $pager = $method->getAnnotation(Pager::class);
        $values = $this->paramInjector->getArgumentes($invocation);
        if ($pager instanceof Pager) {
            return $this->getPager($dbQury->id, $values, $pager);
        }

        $fetchStyle = $this->getFetchMode($dbQury);

        return $this->sqlQuery($dbQury->id, $values, $fetchStyle, $dbQury->entity);
    }
```

Web APIの場合

```php
    public function invoke(MethodInvocation $invocation)
    {
        $method = $invocation->getMethod();
        /** @var WebQuery $webQuery */
        $webQuery = $method->getAnnotation(WebQuery::class);
        /** @var array<string, string> $values */
        $values = $this->paramInjector->getArgumentes($invocation);
        $request = $this->webApiList[$webQuery->id];

        return $this->webApiQuery->request($request['method'], $request['path'], $values);
    }
```

デバッカーでトレースしてみると行っている事はとても単純だと言う事に気付かれるでしょう。

## 着想

テスト可能性(testability)や、ソフトウエアの継続進化を考えるとドメインコードからインフラストラクチャ"コンポーネント"への関心が取り除かれる事がとても魅力的な事に思えてきます。

Ray.Di、Ray.Aop、それにそれぞれで実績にあるcodegen（コード生成）の力を合わせれば、この問題がとてもエレガントに解決できるのではないかと考えました。インターフェイスからオブジェクトを生成する技術はモックとかではありますが、インフラ層に対するアクセスでこれを実現しているものはないと思います。Javaでは[Domaがほぼ同じことを実現](https://doma.readthedocs.io/en/latest/basic/#examples)しているそうです。（[kawanami](https://github.com/kawanamiyuu)さんに教えていただきました）

Ray.MeidaQueryで解決できないような複雑なアクセスは他に専用のクラスを作成することができます。この実装よりもっと素晴らしい`MySuperRayMediaQuery`に入れ替えることもできます。いずれにしてもオリジナルコード本体に変更はありません。依存コンポーネントの後方互換性破壊に振り回される事もありません。

コードのオーナーシップをより高い次元で得る事になるのではないでしょうか。