Entità convalidata più volte utilizzando criteri diversi ogni volta

0

Vedi il codice qui sotto:

public void GetOffers(List<Offer> offers)
        {
            if (Validate() == true)
            {
                OfferFactory offerFactory = new OfferFactory();
                foreach (Offer offer in offers)
                {
                    IValidator iValidator = offerFactory.getOffer(offer);
                    if (iValidator.Validate(this).IsValid==true)
                    {
                        _assignedOffers.Add(offer);
                    }
                }
            }
            else
            {
                throw new InvalidOperationException();
            }
        }

Questo metodo è contenuto in una classe chiamata: Richiedente, cioè il candidato è validato contro molte (cinque offerte). Ad esempio:

1) If the age is above 35 and less than 50 then they are entitled to offer 1
2) If the age is less than 35 and the gender is male then they are entitled to offer 2 
etc

È una cattiva idea farlo da un punto di vista architettonico?

Sto usando Fluent Validation. La chiamata a convalidare; convalida l'oggetto dell'applicazione, ovvero assicura che il primo nome contenga un valore e che il cognome contenga un valore ecc.

Il codice quindi scorre in tutte le offerte per vedere se il membro ha diritto ad esse.

    
posta w0051977 11.07.2017 - 21:28
fonte

2 risposte

2

Il Domain Driven Design è basato su concetti orientati agli oggetti, quindi dobbiamo iniziare da lì. Sappiamo che abbiamo Applicant e abbiamo Offer . Il Applicant può essere valido per un Offer , ma non per uno diverso.

La domanda che dobbiamo porci è "chi è responsabile della determinazione dell'ammissibilità?" Poiché le regole di idoneità sono uniche per ogni offerta, renderei il Offer responsabile per determinare se un Applicant è idoneo o meno.

Se siamo d'accordo, allora abbiamo la seguente API che deve esistere per Offer :

  • bool IsEligible(Applicant applicant)

Il Applicant non dovrebbe preoccuparsi di come Offer determina la sua eleggibilità, solo il risultato. Quindi qualsiasi codice di supporto deve essere incapsulato nella classe Offer . Non è nel dominio Applicant .

Successivamente, dobbiamo determinare come applicare le offerte. Puoi fare tutti i tipi di argomenti che sono tutti corretti sul fatto che l'applicazione di Offer s a Applicant debba essere eseguita da Applicant o da una terza parte che non abbiamo ancora discusso. Per questa risposta, utilizzerò la soluzione che hai fornito nella tua domanda: Applicant è responsabile.

Ora abbiamo bisogno di un metodo che tenga tutte le offerte disponibili e tenti di applicarle:

  • void AssignOffers(IEnumerable<Offer> availableOffers)

NOTA: ho cambiato il nome perché non hai ottenuto nulla. Stai modificando lo stato interno di Applicant e applicando loro le offerte.

Il contenuto di tale metodo dovrebbe essere simile a questo:

public void AssignOffers(IEnumerable<Offer> availableOffers)
{
    if (!IsValid)
    {
        throw new IllegalStateException("Can only apply offers to valid applicants");
    }

    _assignedOffers.AddRange(availableOffers
        .Where(offer => offer.IsEligible(this));
}

Ho una proprietà che determina se questo candidato è valido o meno, ma puoi mantenerlo come metodo. Potrebbe essere appropriato se la logica è abbastanza complessa.

Dall'altro lato abbiamo la nostra classe Offer che abbiamo bisogno di una sorta di implementazione per il nostro nuovo metodo. Se le classi di offerta vengono create per ciascuna offerta, è possibile utilizzare il codice scritto a mano. Sarebbe più facile iniziare con certezza. Tuttavia, diciamo che abbiamo bisogno di generare la logica di validazione in modo dinamico e di avere una fabbrica per farlo. Quella fabbrica dovrebbe essere nominata per ciò che crea. In questo caso è un ValidationFactory che può generare il validatore delle regole di ammissibilità dal tuo Offer . L'implementazione sarebbe simile a questa:

public bool IsEligible(Applicant applicant)
{
    IValidator rules = _validationFactory.CreateForOffer(this);

    return rules.Validate(applicant).IsValid;
}

Ciò mantiene chiari i domini e le responsabilità. Mantiene anche i dettagli di implementazione dal filtraggio del resto del codice.

Ci sono alcune cose da tenere a mente con DDD:

  • I nomi contano. Classi e metodi dovrebbero essere nominati per quello che fanno. Le classi sono sostantivi e i metodi sono i verbi.
  • L'intento dovrebbe essere chiaro. Non utilizzare il verbo Get quando il metodo restituisce void .
  • Le responsabilità dovrebbero essere chiare.

Il risultato finale dovrebbe essere un codice che sia relativamente facile da comprendere e succinto.

    
risposta data 11.07.2017 - 23:09
fonte
1

La "convalida" di solito si applica a dati che sono fuori controllo, più comunemente dati immessi da un utente finale, ma in alcune situazioni è possibile convalidare anche i dati di back-end se vengono recuperati da un servizio di terze parti. Sarebbe inusuale "convalidare" i dati che controlli.

Ecco come vorrei refactoring questo:

  1. Le informazioni sul richiedente come nome e data di nascita sarebbero convalidate tramite script lato client per fornire un feedback immediato.

  2. La convalida sul lato client sarebbe ripetuta sul lato server per rilevare eventuali manipolazioni di dati che potrebbero essere eseguite con Javascript disattivato o tramite acquisizione / modifica della richiesta utilizzando uno strumento come Paros. Mentre è "pulito" per mettere quel tipo di convalida nel livello aziendale, la realtà è che spesso è conveniente farlo nel livello di presentazione, poiché ci sono strumenti che consentono di replicare la convalida di JavaScript con la convalida lato server ( ad es. Page.IsValid ) . Ma potresti farlo in entrambi i posti.

  3. Una volta che il richiedente è stato completamente convalidato, il sito chiede al livello aziendale di produrre un elenco di offerte valide per l'utente. Sarebbe meglio generare queste offerte endogenamente ... in altre parole, prendere i criteri del richiedente come input e quindi produrre la lista. Scorrere l'elenco completo di tutte le offerte possibili e convalidarle una per una sarebbe meno efficiente, soprattutto se il sistema contiene molte offerte per le quali l'utente non è idoneo.

Per ottenere l'elenco delle offerte, il modo più ovvio sarebbe con una chiamata simile a questa:

List<Offer> offers = offerFactory.GetOffers(applicant.DateOfBirth,
                                            applicant.OtherCriteria,
                                            applicant.Etc);

Tuttavia, se ci sono un sacco di campi, potresti essere tentato di passare invece l'intero richiedente, in questo modo:

List<Offer> offers = offerFactory.GetOffers(applicant);

Ciò solleva la domanda su quale input si aspetta il metodo GetOffers . Potrebbe aspettarsi un Applicant , suppongo, come questo:

class OfferFactory
{
    public List<Offer> GetOffers(Applicant applicant)
    {
        //Code goes here
    }
}

Ma ciò crea una dipendenza dalla classe Applicant concreta. Potrebbe essere meglio passare un'interfaccia. Possiamo interpretare ogni candidato con criteri di applicazione, quindi possiamo definire la classe Richiedente con un'interfaccia come questa:

public interface IApplicationCriteria
{
    DateTime DateOfBirth;
}

class Applicant: IApplicationCriteria
{
    public DateTime DateOfBirth { get; set; }
    public string   FirstName   { get; set; } //Not in the interface
    public string   LastName    { get; set; } //Not in the interface
}

Quindi la nostra implementazione GetOffers() accetta l'interfaccia che contiene solo i dati di cui ha bisogno:

public List<Offer> GetOffers(IApplicationCriteria criteria)
{
    //Code goes here
}

Ma lo chiameresti comunque allo stesso modo:

List<Offer> offers = offerFactory.GetOffers(applicant);

Tuttavia, passando solo l'interfaccia, creiamo una migliore separazione delle preoccupazioni. Ciò potrebbe essere utile se, ad esempio, un giorno decidiamo che un Applicant non ha FirstName e LastName , ma invece ha FullName . La chiamata di GetOffers() non dovrebbe interessare, e possiamo dire che non gli interessa al momento della compilazione.

Nel frattempo, GetOffers() utilizza i criteri per generare una query nella tabella delle offerte

class OfferFactory
{
    private readonly SqlConnection _connection;

    public OfferFactory(SqlConnection connection)
    {
        _connection = connection; //Injected
    }

    public List<Offer> GetOffers(IApplicationCriteria criteria)
    {
        var offers = new List<Offer>();
        SqlCommand cmd = new SqlCommand("GetOffers");
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.AddParameterWithvalue("@DateOfBirth", criteria.DateOfBirth);
        cmd.Connection = _connection;  
        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            offers.Add(new Offer {
                                     OfferID   = reader["OfferID"]   as string,
                                     OfferName = reader["OfferName"] as string,
                                     Price     = reader["Price"]     as decimal
                                 }
                      );
        }

        return offers;
    }
}

In questo modo non chiami Convalida ripetutamente su dati che sai essere già appropriati per il richiedente.

Se davvero volevi creare il maggior numero di livelli possibile, puoi spostare l'accesso ai dati su un altro livello, ma in un caso come questo, dove stai selezionando un sottoinsieme di dati, sarebbe preferibile mantenere livello aziendale "stupido" e lascia che il database trovi i record per te; solo ha gli indici e i dati necessari per trovare le offerte giuste senza guardare tutte le offerte che non sono valide.

Come apparirebbe lo sproc? Dipende dalla struttura dei dati, ma potrebbe andare lungo le linee di questo:

CREATE PROC GetOffers(@DateOfBirth DateTime)
AS
BEGIN
    SET NOCOUNT ON
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED

    DECLARE @Age int
    DECLARE @OfferCount int

    --Compute the applicant's age
    SELECT @Age = DATEDIFF(hour, @dateOfBirth, GETDATE())/8766.0

    --Obtain the number of offers allowed
    SELECT @OfferCount = MAX(OfferCount)
    FROM   OfferAgeRules
    WHERE  MinAge <= @Age
    AND    MaxAge >= @Age

    --Get the offers
    SELECT TOP (@OfferCount)
             o.OfferID,
             o.OfferName,
             o.Price
    FROM     Offers o
    WHERE    o.MaxAge >= @Age
    AND      o.MinAge <= @Age
    AND      o.OfferStartDate < GETDATE()
    AND      o.OfferGoodUntil > GETDATE()
    ORDER BY o.Priority
END
    
risposta data 11.07.2017 - 23:13
fonte

Leggi altre domande sui tag