Membro Value Objects utilizzato per convalidare un altro oggetto valore

0

Vedi il codice qui sotto:

    public sealed class UKCurrency : ICurrency
    {
        private static readonly int _decimalPlaces=2; 
            private static readonly decimal[] _denominations = new decimal[] {
                50.00M, 20.00M, 10.00M,
                5.00M,  2.00M,  1.00M,
                0.50M,  0.20M,  0.10M,
                0.05M,  0.02M,  0.01M,
            };

            public IEnumerable<decimal> Denominations
            {
                get { foreach (var denomination in _denominations) yield return denomination; }
            }
    }

    public sealed class DenominationCounter
    {
        private readonly decimal _cost;

        public decimal Cost
        {
            get { return _cost; }
        }

        public ICurrency Currency
        {
            get { return _currency; }
        }

            public DenominationCounter(decimal cost, ICurrency currency)
            {
                if (currency == null)
                    throw new ArgumentNullException("Currency cannot be null", "ICurrency");
                if (cost < 0)
                    throw new ArgumentException("Cost cannot be negative", "Cost");
                if (decimal.Round(cost, currency.DecimalPlaces) != cost)
                    throw new ArgumentException(string.Concat("Cost has too many decimal places.  It should only have: ", currency.DecimalPlaces), "Cost");
                _cost = cost;
                _currency = currency;
            }

public IEnumerable<System.Collections.Generic.KeyValuePair<decimal, int>> CalculateDenominations()
        {
            var target = _cost;
            foreach (var denomination in _currency.AvailableDenominations)
            {
                var numberRequired = target / denomination;
                if (numberRequired >= 1)
                {
                    int quantity = (int)Math.Floor(numberRequired);
                    yield return new KeyValuePair<decimal, int>(denomination, quantity);
                    target = target - (quantity * denomination);
                }
            }
        }
    }

Il costruttore DenominationCounter genera un'eccezione se il costo ha il numero sbagliato di cifre decimali.

Si noti che la classe UKCurrency viene utilizzata per convalidare DenominationCounter come mostrato di seguito:

if (decimal.Round(cost, currency.DecimalPlaces) != cost)

È normale procedere con la convalida in questo modo:

1) Un membro Value Objects viene utilizzato per convalidare un'entità

2) Un membro Value Objects viene utilizzato per convalidare un altro oggetto valore

Lo chiedo perché non ho mai visto una convalida approcciata in questo modo e sto cercando di seguire il principio del minimo stupore in questi giorni.

    
posta w0051977 15.02.2018 - 12:04
fonte

3 risposte

0

Non vedo alcuna esigenza per il tuo oggetto DenominationCounter . Lo scopo è fornire un IEnumerable<(decimal, int)> (ho semplificato il tuo tipo di dati per utilizzare una tupla) tramite CalculateDenominations .

Ma quel metodo è completamente deterministico in natura. Non stai ottenendo nulla di utile (oltre le discussioni sull'ossessione primitiva) avendo istanze.

Inoltre, stai creando una soluzione non coesa alle denominazioni avendo più classi responsabili della gestione di ciò che dovrebbe essere una singola responsabilità. Non usare solo il polimorfismo dei sottotipi solo per il gusto di farlo.

Io renderei l'intero lotto una singola classe statica, per massimizzare la coesione, per rimuovere tutte le complicazioni attorno ai costruttori, per rimuovere la necessità di duplicare la funzionalità in ogni implementazione di ICurrency.Denomination etc:

public enum Currency { GBP, EUR, USD, ... }

public static class Denominations
{
    private static readonly decimal[] GBPDenominations = new decimal[]
    {
        50.00M, 20.00M, 10.00M,
        5.00M,  2.00M,  1.00M,
        0.50M,  0.20M,  0.10M,
        0.05M,  0.02M,  0.01M,
    };

    private static readonly decimal[] EURDenominations = ...

    private static Dictionary<Currency, decimal[]> CurrencyDenominations =
        new Dictionary<Currency, decimal[]>
        {
            [Currency.GBP] = GBPDenominations,
            [Currency.EUR] = EURDenominations,
            ...
        };


    public static IEnumerable<(decimal denomination, int quantity)>
        CalculateDenominations(Currency currency, decimal cost)
    {
        var remainingCost = cost;
        foreach (var denomination in CurrencyDenominations[currency])
        {
            var numberRequired = remainingCost / denomination;
            if (numberRequired >= 1)
            {
                var quantity = (int)Math.Floor(numberRequired);
                yield return (denomination, quantity);
                remainingCost = remainingCost - (quantity * denomination);
            }
        }
    }
}
    
risposta data 15.02.2018 - 12:44
fonte
1

Non hai due diversi oggetti valore. Hai un oggetto valore e una matrice fantasia. Probabilmente hai bisogno di combinare un sacco di UKCurrency e DenominationCounter. Non è sbagliato per un oggetto valore convalidare che le sue dipendenze siano valide anche se sono altri oggetti valore, ma entrambi gli oggetti devono essere preziosi indipendentemente l'uno dall'altro.

Un oggetto valore ha bisogno di un valore che sia effettivamente comparabile (anche se non si esegue mai o non si implementa mai un confronto), UKCurrency non ha questo. Ogni istanza di UKCurrency è esattamente la stessa, e anche peggio se esistesse CanadianCurreny sarebbe funzionalmente identica a UKCurrency.

Prova a indicare cosa fanno i tuoi oggetti / che valore forniscono in 1-2 frasi. Se non puoi farlo o quello che hai detto non sembra utile, allora qualcosa deve cambiare.

    
risposta data 15.02.2018 - 15:28
fonte
1

OK Quindi! ecco la mia comprensione del tuo approccio

  1. Nel tuo dominio hai un tipo di valore speciale es. Valuta in Denominazioni
  2. Vuoi impedire la costruzione di un tipo di valore che sarebbe impossibile per definizione, ovvero mezzo pezzo da 10p.
  3. Stai modellando i tipi di valore come tipi di riferimento immutabili. piuttosto che usare i tipi di valore. Per ragioni.
  4. Alcune delle informazioni richieste per convalidare il 'tipo di valore' sono contenute in un'altra classe. ad esempio le possibili denominazioni di una determinata valuta.

Cose cattive:

  1. Il lancio di costruttori è generalmente negativo. L'eccezione alla regola è rappresentata da tipi di valori impossibili, ad esempio il 31 di febbraio
  2. Tutti i tuoi esempi potrebbero essere refactored per eseguire la convalida in un metodo piuttosto che in un costruttore.

Se sei sposato con il tuo modo di fare le cose, allora non hai alternative alla tua attuale soluzione. Sì, sembra un po 'strano, ma è funzionale.

Se vuoi una soluzione in cui non hai quella particolare combinazione di validazione, perché non eliminare completamente le eccezioni del costruttore?

public class ImmutableListOfCoins 
{
    private IEnumerable<KeyValuePair<decimal, uint>> listOfCoins {get; private set;}
    public ImmutableListOfCoins(IEnumerable<KeyValuePair<decimal, uint>> listOfCoins) 
    {
         ....
    }
}

public static ImmutableListOfCoins CalculateDenominations(decimal cost)
    {
        var target = _cost;
        if (decimal.Round(cost, currency.DecimalPlaces) != cost)
        {
            throw new ArgumentException("Cost has too many decimal places.");
        }
        var loc = new Dictionary<decimal,uint>();

        foreach (var denomination in _currency.AvailableDenominations)
        {
            var numberRequired = target / denomination;
            if (numberRequired >= 1)
            {
                var quantity = (uint)Math.Floor(numberRequired);
                loc.Add(new KeyValuePair<decimal, uint>(denomination, quantity));
                target = target - (quantity * denomination);
            }
        }
        return new ImmutableListOfCoins(loc);
    }
    
risposta data 15.02.2018 - 15:49
fonte

Leggi altre domande sui tag