Incapsulamento e duplicazione di convalida dell'input

1

Considera il seguente esempio (molto semplificato):

public class Basket
{
    private readonly List<BasketItem> _items = new List<BasketItem>();
    public IReadOnlyCollection<BasketItem> Items => _items;

    // _items.Sum(item=>item.TotalAmount)
    public decimal TotalAmount { get; private set; } 
    // ...other properties ...

    public void ChangeItemQuantity(BasketItem item, decimal quantity)
    {
        if (item == null) throw new ArgumentNullException(nameof(item));
        ///////////////////////////////////
        // code duplication
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        ///////////////////////////////////

        item.ChangeQuantity(quantity);

        // recalculates basket totals, items.Sum(item=>item.TotalAmount)
        RecalculateTotals();  
    }
}

public class BasketItem
{
    public decimal Quantity { get; private set; }
    public decimal TotalAmount { get; private set; } // quantity * price
    // ...other properties ...

    public void ChangeQuantity(decimal quantity)
    {
        ///////////////////////////////////
        // code duplication
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        ///////////////////////////////////

        Quantity = quantity;

        RecalculateTotals(); // recalculates item total (price * quantity)
    }
}

Come puoi vedere, incapsulo molto le cose (sto davvero considerando di allontanarmi da OOP).

Il problema che vedo con questo codice è che eseguo la stessa validazione in 2 punti. In realtà è la stessa convalida in 3 posizioni perché ce n'è una aggiuntiva ai confini (livello API / UI).

Potresti dirmi per favore, è la convalida in Basket.ChangeItemQuantity ridondante?

    
posta Konrad 16.11.2018 - 10:55
fonte

2 risposte

5

Could you tell me please, is the validation in Basket.ChangeItemQuantity redundant?

Sì, è ridondante. Ma questo è l'ultimo dei tuoi problemi.

Semanticamente è strano per un oggetto sapere quanti ne sono in un cestino. È strano sapere che è anche in un cestino. Questo dice che potrei chiedere a una patata di completare il mio ordine. I tuoi articoli sanno semplicemente molto.

Forse sto leggendo molto nei nomi e quello che vuoi veramente sapere è se avere più livelli di convalida della stessa cosa è sempre male. Non è. Ammesso che stia accadendo nei posti giusti.

Quando attraversi confini significativi, la convalida è una buona idea. Quando stai raccogliendo i dati degli utenti, sei a un livello elevato. Quando si costruiscono strutture dati persistenti è un altro. Va bene mettere la convalida della stessa cosa sulle porte attraverso queste mura. Forse è ridondante ma mi aiuta a leggere il tuo codice. So subito quali sono le tue aspettative senza dover scavare in altri luoghi per sapere cosa è permesso.

In effetti, ogni volta che si parla o si ascolta qualcosa che non applica completamente il proprio modello, è consigliabile valutare la possibilità di imporre il modello su di esso.

Mantenere il codice duplicato è ancora fastidioso. Fare la pace con il fatto che il codice identico, facendo un lavoro diverso, che potrebbe cambiare per una ragione diversa, ha una ragione perfetta per esistere.

Detto questo, potrebbe trattarsi di un caso di ossessione primitiva . Continui a utilizzare decimal quando ciò che desideri è una quantità positiva. Puoi interrompere la diffusione di decimal e il codice di convalida in giro attaccandoli entrambi in una classe Quantity . Ora tutto ciò che devi controllare è null . Sigh.

    
risposta data 16.11.2018 - 11:37
fonte
1

Direi che il codice ridondante è un problema più grande. Perché la tua classe Basket ha un metodo "ChangeItemQuantity" quando l'elemento che passi come argomento ha lo stesso identico metodo con la stessa logica di business? Per essere chiari, un nome di metodo che si verifica più volte potrebbe non essere un problema, ma il codice di business logic che si verifica più volte è sicuramente uno. Questo è il codice ridondante che deve essere mantenuto. Un'altra domanda è: dove si trova il tuo metodo "RecalculateTotals"? È anche ridondante?

Ma non era questa la domanda, forse il tuo codice in generale ha più senso di questo estratto.

Im termini di convalida, per essere pedante devi convalidare un valore / input specifico in ogni luogo in cui non puoi garantire che sia stato convalidato prima. Nel tuo esempio, il metodo "ChangeQuantity" del tuo BasketItem potrebbe essere chiamato direttamente dal codice client senza passare attraverso il metodo "ChangeQuantity" del tuo carrello. Quindi si deve supporre che i parametri di input del metodo BasketItem non siano ancora stati validati.

Se BasketItem fosse una classe privata all'interno della classe Basket pubblica e l'unico percorso di esecuzione a BasketItem.ChangeQuantity () gous attraverso Basket.ChangeQuantity () dove i dati sono convalidati, direi che non è necessario convalidarlo di nuovo , perché puoi essere sicuro che è stato convalidato prima. Ma in questo caso si potrebbe obiettare che forse in un successivo refactoring, la classe privata potrebbe essere spostata all'esterno e diventare pubblica.

Per essere chiari, questa è una visione veramente pedante che non può essere seguita per tutto il tempo. Inoltre, tutto questo codice di validazione è piuttosto gonfiore e rende l'illegalità del metodo più illeggibile. Tutto quello che voglio dire è che le assunzioni su altri moduli / classi dal modulo / classe corrente sono spesso una brutta cosa nel software orientato agli oggetti e dovrebbero essere fatte a caso, perché non si può mai essere sicuri che l'assunto sia sbagliato (ora o forse in futuro).

    
risposta data 16.11.2018 - 12:51
fonte