7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BEAR.SundayAdvent Calendar 2021

Day 17

Ray.MediaQuery

Last updated at Posted at 2021-12-16

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

この記事は BEAR.Sunday Advent Calendar 2021の記事です。
以前の記事、Ray.DiDependency Inversion Principle(DIP)、依存性逆転の原則を紹介しました。

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

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

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

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

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

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

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

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

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

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

実装はノーコード

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

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

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

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

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

ランタイム

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

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

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

2. 束縛

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

DbQueryの場合、以下の引数idtitle

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

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

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

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

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

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

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

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

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

これは別途作成したkoriym/null-objectで実現しています。これに対してDbQueryInterceptorWebQueryInterceptorを束縛しています。

DBの場合

    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の場合

    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がほぼ同じことを実現しているそうです。(kawanamiさんに教えていただきました)

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

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

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?