Dove inserire il codice relativo agli invarianti?

2

Sto sviluppando una piccola applicazione, solo per praticare DDD. Per quanto ne so. invarianti sono il termine generale di convalida relativo al dominio. Quindi, per esempio, se voglio avere solo i nomi ucfirst, allora è un invariante e ho bisogno della validazione nel setter della proprietà per assicurarmi che questo invariante sia protetto e rendere pubblico solo il setter, non la proprietà stessa.

Il mio problema attuale è che ho bisogno di nomi utente, che vengono generati da nome, cognome, data di nascita e probabilmente alcuni elementi casuali, se questi non sono abbastanza unici. Per quanto ne so, avere un nome utente univoco è un problema relativo al dominio, perché è molto importante se si desidera effettuare il login o semplicemente associare alcuni dati a un utente reale. Quindi avere una tabella di database con pk sul nome utente e attendere la violazione dell'errore di vincolo non è sufficiente, dovrei inserire del codice nel dominio, il che assicura che il nome utente generato sia unico. Immagino che il codice entri in qualche modo nel repository o nell'entità. Puoi mostrarmi un esempio di come potrebbe apparire in una vera app? Il dominio e l'implementazione del servizio app sono ciò di cui sono curioso.

Potrei non avere ragione su questo. Ogni entità ha un identificatore univoco e ha un repository, quindi probabilmente dovrei inserire la generazione del nome utente nel repository, e questo è tutto. Potrei aver bisogno di riformulare questo per esempio cosa succede se non abbiamo un identificatore univoco, solo una proprietà che dovrebbe essere unica?

Ho creato una piccola bozza di codice:

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());
        }
    }

}

Se inserisco questo nel servizio app, assicurerà che il nome sia univoco a meno che non si verifichi una concomitanza, ma l'invio di un messaggio di errore è ok in quel raro caso. Non sono sicuro che sia ok. Non è corretto se gli invarianti devono essere parti del codice di dominio.

Un'altra possibile soluzione per avere un metodo di creazione nel repository.

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;
    }
}

Potrebbe essere meglio.

    
posta inf3rno 14.02.2017 - 16:18
fonte

2 risposte

2

In un'applicazione multilivello un'applicazione viene generalmente suddivisa in tre livelli:

  • presentazione,
  • logica aziendale,
  • accesso ai dati.

Un livello di accesso ai dati dovrebbe fare una cosa e farlo bene, dovrebbe sapere come recuperare e mantenere le entità utilizzate dal livello della logica aziendale e non dovrebbe contenere alcuna logica aziendale.

Nota, che sto dicendo non dovrebbe, perché gli errori nel tuo livello di accesso ai dati potrebbero essere il tuo meccanismo di ultima istanza, quando tutto il resto fallisce - come se stavi cercando di inserire un record duplicato di un attributo unico perché non sei riuscito per controllare in anticipo.

Ma perché esattamente esiste un attributo nome utente univoco all'interno del tuo sistema? Ci sono due possibilità:

  1. Il meccanismo del livello di accesso ai dati produce questo vincolo e non hai alcun controllo su di esso.
  2. I tuoi stakeholder ti hanno detto che nel tuo sistema un nome utente DEVE essere unico e verrà utilizzato per l'accesso - le parti interessate hanno presentato una regola aziendale che deve essere soddisfatta.

Possiamo ignorare completamente il caso numero 1, poiché gli archivi di dati non si preoccupano affatto di ciò che viene inserito, a condizione che il formato sia corretto e che non si preoccupino del valore effettivo. Ti limitano a valori unici solo se gli dici di farlo. Significa che stai affrontando la 2. situazione: una regola aziendale.

Tenendo presente la regola, hai aggiunto un vincolo al tuo motore di archiviazione dati (dovrebbe supportare) che un attributo DEVE essere davvero unico, ma come ho già detto, non devi fare affidamento su questo, è solo lì per fallire al livello più basso quando tutto il resto fallisce.

E poiché la regola che un nome utente DEVE essere univoco è stata effettivamente prodotta dai proprietari di attività commerciali, appartiene al livello della logica aziendale, è il suo posto legittimo.

Il tuo metodo di repository create dovrebbe contenere solo il comando INSERT e nessun controllo. E se qualcuno dimentica di verificare la presenza di un nome utente univoco e utilizza il metodo create dell'interfaccia ICustomerRepository senza cercare un nome utente univoco, è probabile che il metodo del repository possa fallire, perché il programmatore che ignora il controllo ha effettivamente infranto una regola importante. / p>     

risposta data 14.02.2017 - 20:16
fonte
0

Esistono diverse soluzioni a questo problema. Ne darò uno che implica la creazione di un aggregato specializzato che è responsabile dell'assegnazione del nome utente. Il linguaggio è PHP, ma spero che sia generalmente comprensibile.

Quindi l'idea è che UsernameAggregate verifichi che l'invarianza di univocità del nome utente sia rispettata. Accetta il comando AllocateUsername per allocare un nome utente e genera un evento o genera un errore. Questo comando viene inviato prima il comando RegisterNewUser viene inviato, nel livello Application .

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;
    }
}

Nel livello Application :

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

Se il nome utente è già in uso, viene generata un'eccezione e il comando RegisterNewUser non viene mai inviato, rafforzando così l'invarianza. Nel mio codice, CommandDispatcher è responsabile del caricamento dell'aggregato, del comando di dispatching e del salvataggio degli eventi nell'archivio degli eventi, in modo transazionale ( fonte ).

    
risposta data 14.02.2017 - 21:48
fonte

Leggi altre domande sui tag