web-dev-qa-db-ja.com

不変条件に関連するコードをどこに置くか?

私はDDDを練習するためだけに小さなアプリケーションを開発しています。私の知る限り。インバリアントは、ドメインに関連する検証の包括的な用語です。したがって、たとえばucfirst名のみが必要な場合、それは不変であり、この不変が保護されていることを確認し、プロパティ自体ではなくセッターのみをパブリックにするためにプロパティセッターで検証が必要です。

私の現在の問題は、firstname、lastname、birthDate、およびおそらくいくつかのランダムなものから生成されたユーザー名が必要なことです。私が理解している限り、固有のユーザー名を持つことはドメインの問題です。ログインするか、実際のユーザーにいくつかのデータをバインドするかが非常に重要だからです。したがって、ユーザー名にpkを含むデータベーステーブルを用意し、違反制約エラーを待つだけでは不十分です。ドメインにコードを挿入して、生成されたユーザー名が一意になるようにします。私はコードがリポジトリまたはエンティティに何らかの形で入ると思います。これが実際のアプリでどのように見えるかの例を見せていただけますか?私が興味を持っているドメインとアプリサービスの実装。

私はこれについて正しくないかもしれません。すべてのエンティティには一意の識別子とリポジトリがあります。そのため、おそらくユーザー名の生成をリポジトリに入れる必要があります。それだけです。これを言い換える必要があるかもしれません。たとえば、一意の識別子がなく、一意である必要があるプロパティだけがある場合はどうなりますか?

私は小さなコードドラフトを作りました:

class CustomerService {

    // ...

    public createCustomerFromNameAndYear(name, year){
        try {
            transaction.begin();
            username = generateUsername(name, year);
            while (repo.findByUsername(username))
                username = addSomeRandomChar(username, 1);
            password = "";
            password = addSomeRandomChar(password, 6);
            customer = new Customer(username, password, name, year);
            repo.save(customer);
            transaction.commit();
            dto = new CustomerDTO(customer.getUsername(), customer.getPassword());
            return dto;
        }
        catch(exception) {
            transaction.rollback();
            throw new ExceptionWithCauses(CUSTOMER_CREATION_FAILED, exception.getMessage());
        }
    }

}

これをアプリサービスに追加すると、並行処理が発生しない限り、名前が一意になることが保証されますが、そのまれなケースでエラーメッセージを送信しても問題ありません。これが大丈夫かどうかはわかりません。不変条件がドメインコードの一部でなければならない場合、それは正しくありません。

リポジトリにcreateメソッドを持つ別の可能な解決策。

class CustomerService {

    // ...

    public createUserFromNameAndYear(name, year){
        try {
            transaction.begin();
            customer = repo.create(name, year);
            transaction.commit();
            dto = new CustomerDTO(customer.getUsername(), customer.getPassword());
            return dto;
        }
        catch(exception) {
            transaction.rollback();
            throw new ExceptionWithCauses(CUSTOMER_CREATION_FAILED, exception.getMessage());
        }
    }

}

class CustomerRepository implements iCustomerRepository {

    // ...

    public create(name, year){
        username = generateUsername(name, year);
        while (this.findByUsername(username))
            username = addSomeRandomChar(username, 1);
        password = "";
        password = addSomeRandomChar(password, 6);
        customer = new Customer(username, password, name, year);
        this.save(customer);
        return customer;
    }
}

これは良いかもしれません。

2
inf3rno

多層アプリケーションでは、アプリケーションは通常3つの層に分かれています。

  • プレゼンテーション、
  • ビジネスの論理、
  • データアクセス。

データアクセスレイヤーは1つのことを行い、それをうまく実行する必要があります。ビジネスロジックレイヤーで使用され、ビジネスロジックを含むべきではないエンティティを取得して永続化する方法を知っている必要があります。

データアクセスレイヤーのエラーが他のすべてが失敗したときの最後の手段となる可能性があるため、私はすべきではないことに注意してください-事前にチェックできなかったために一意の属性の重複レコードを挿入しようとした。

しかし、なぜシステム内に一意のユーザー名属性が存在するのでしょうか。 2つの可能性があります。

  1. データアクセス層のメカニズムはこの制約を生成し、ユーザーはそれを制御できません。
  2. 利害関係者から、システムではユーザー名は一意である必要があり、サインインに使用されると言われました-利害関係者は、満たす必要のあるビジネスルールを提示しました。

ケース1は完全に無視できます。データストレージは、フォーマットが正しい限り、実際の値を気にしない限り、何が入っているのかまったく気にしません。一意の値に制限するのは、そうするように指示した場合のみです。つまり、実際には2.ビジネスルールに直面しています。

ルールを念頭に置いて、属性が実際に一意である必要があるという制約をデータストアエンジン(サポートする必要があります)に追加しましたが、前述したように、これに依存するべきではありません。他のすべてが失敗したときのレベル。

また、ユーザー名は一意である必要があるというルールは実際にはビジネスオーナーによって作成されたため、ビジネスロジックレイヤーに属しています。

リポジトリのcreateメソッドにはINSERTコマンドのみが含まれ、チェックは含まれなくなります。誰かが一意のユーザー名を確認するのを忘れ、一意のユーザー名を確認せずにcreateインターフェースのICustomerRepositoryメソッドを使用すると、プログラマが実際に確認を無視するため、リポジトリメソッドが失敗する可能性があります。重要なルールを破った。

2
Andy

この問題には複数の解決策があります。ユーザー名の割り当てを担当する特殊な集合体の作成を暗示するものを挙げます。言語はPHPですが、それが一般的に理解できることを願っています。

したがって、理想は、UsernameAggregateがユーザー名の一意性の不変が尊重されていることを確認しているということです。コマンドAllocateUsernameを受け入れてユーザー名を割り当て、イベントを発生させるか、エラーをスローします。このコマンドは、コマンドRegisterNewUserApplicationレイヤーにディスパッチされるにディスパッチされます。

class UsernameAggregate
{
    private $allocatedUsernames = array();

    public static function getAggregateId():Guid
    {
        //singleton aggregate
        return Guid::fromFixedString(self::class);
    }

    public function handleAllocateUsername(AllocateUsername $command)
    {
        if ($this->isUsernameTaken($command->getUsername())) {
            throw new \Exception("This username is already taken");
        }

        yield new UsernameWasAllocated($command->getIdUser(), $command->getUsername());
    }

    public function applyUsernameWasAllocated(UsernameWasAllocated $event)
    {
        $this->addUsername($event->getUsername());
    }

    private function isUsernameTaken(string $username):bool
    {
        return isset($this->allocatedUsernames[$username]);
    }

    private function addUsername(string $username)
    {
        $this->allocatedUsernames[$username] = 1;
    }
}

Applicationレイヤー:

public function registerNewUser($username, $userId)
{
    $this->commandDispatcher->dispatchCommand(new AllocateUsername($username));
    $this->commandDispatcher->dispatchCommand(new RegisterNewUser($userId, $username));
}

ユーザー名が既に使用されている場合、例外がスローされ、RegisterNewUserコマンドがディスパッチされないため、不変条件が適用されます。私のコードでは、CommandDispatcherは、トランザクションの方法で、集約をロードし、コマンドをディスパッチしてイベントストアに保存します( source )。

0