Accesso controllato all'elenco delle entità e accesso get / set nel modello di dominio

2

Durante la progettazione di un dominio, è possibile utilizzare entrambi gli approcci seguenti per accedere all'elenco di entità figlio all'interno di un'entità padre.

1. Ottieni / Imposta

Il seguente approccio è semplice e semplice da implementare, ma chiunque può sovrascrivere la raccolta degli ordini.

public User 
{
    public IList<Order> Orders { get; set; }
}

o

2. Accesso controllato

Il seguente è un po 'complicato, ma fornisce l'accesso controllato alla lista degli ordini.

public User 
{
    private IList<Order> _orders;

    public User()
    {
        _orders = new List<Order>();
    }

    public IList<Order> Orders
    {
        get
        {
            return new new ReadOnlyCollection<Order>(_orders);
        }
    }

    public void Add(Order order)
    {  
        order.User = this;
        _orders.Add(order);
    }

    public void Remove(Order order)
    {
        if (_orders.Contains(order)
        { 
            order.User = null;
            _orders.Remove(order);
        }
    }
}

Il secondo approccio ha chiaramente i suoi benefici, ma aumenta anche il numero di linee nel codice (immagina che poche delle collezioni siano nell'entità). Quindi la domanda è: ne vale la pena?

    
posta Low Flying Pelican 07.05.2015 - 13:34
fonte

2 risposte

2

Domanda importante: nel primo esempio, dove viene pubblicato il codice che impone l'invarianza di business?

Il primo approccio sembra semplice e diretto perché tutte le cose difficili sono state spostate da qualche altra parte!

Quindi, in ordine:

public User 
{
    public IList<Order> Orders { get; set; }
}

Questo è negativo, perché il vincolo sull'elenco di utenti è stato spostato all'esterno dell'entità.

user.getOrders().add(invariantViolatingOrder);

Il tuo secondo approccio è migliore, ma ha ancora problemi

public User 
{
    public IList<Order> Orders
    {
        get
        {
            return new new ReadOnlyCollection<Order>(_orders);
        }
    }
}

Questo protegge la raccolta dal cambiamento, ma non protegge gli Ordini da modifiche in modo tale da violare l'invarianza.

user.getOrders().getOrder(2).invariantViolatingChange();

L'entità ordine può imporre un invariante del proprio stato, ma non può imporre un invariante dello stato combinato. (Esempio di esempio: questo particolare utente ha solo $ 20 dollari di ordini. Devi controllare l'intera collezione per verificare che sia corretto cambiare questo ordine).

Il modo più semplice per garantire che un invariante sia applicato in ogni punto che cambia lo stato di un'entità, è assicurarsi che ci sia solo un posto dove lo stato può essere cambiato, e applicarlo lì. In termini OO, questo è "incapsulamento".

public User 
{
    public IList<Order> Orders
    {
        get
        {
            // By returning a copy of the state, we ensure that nothing
            // outside the user can change its state.
            IList<Order> readOnly = new List<Order>();

            for (Order o : _orders) {
                // Turtles all the way down: this depends on the order
                // providing copies of its own state.
                readOnly.add(o.clone());
            }

            return readOnly ;
        }
    }
}

Un'altra possibilità è trattare lo stato interno di un'entità come un tipo di valore immutabile. Ottenere lo stato immutable va perfettamente bene, perché non c'è modo che lo stato immutabile possa essere utilizzato per violare l'invarianza di business.

Ma ancora una volta, questa seconda soluzione è le tartarughe fino in fondo; devi stare attento a includere le entità in un oggetto valore.

    
risposta data 30.01.2016 - 20:30
fonte
1

Questa è una delle ragioni per cui viene utilizzata l'interfaccia IEnumerable. Ciò consente alla proprietà getter pubblica di restituire una raccolta su cui è possibile iterare ma non può aggiungere o rimuovere elementi. La raccolta di lettura / scrittura sottostante potrebbe essere un IList.

public class User
{
    public User()
    {
        orders = new List<Order>();
    }

    private IList<Order> orders;

    public IEnumerable<Order> Orders
    {
        get { return orders; }
    }

    public Order AddOrder(Product p)
    {
        Order o = new Order(this, p);

        orders.Add(o);

        return o;
    }
}

Ora puoi controllare attentamente come gli ordini vengono aggiunti forzando i client del tuo codice a utilizzare il metodo "AddOrder". Inoltre, non è possibile aggiungere accidentalmente un ordine per un altro utente a questo utente. Puoi persino arrivare al punto di contrassegnare il costruttore dell'Ordine interno in modo che il codice all'esterno di questo assembly non sia in grado di creare ordini senza un utente:

public class Order
{
    internal Order(User user, Product product)
    {
        User = user;
        Product = product;
    }

    public Product Product { get; private set; }
    public User User { get; private set; }
}

E un esempio di utilizzo:

User user = usersRepository.Find(123);
Product product = productsRepository.Find(321);

// Compiler won't allow this because the Order constructor is marked "internal"
Order order = new Order(user, product);

// This is allowed because the User class is in the same Assembly as the Order class
Order order = user.AddOrder(product);

Questo approccio evita anche di provare ad aggiungere un ordine a un utente in cui l'oggetto Order fa riferimento a un utente diverso:

User user1 = usersRepository.Find(123);
User user2 = usersRepository.Find(1003);
Product product = productsRepository.Find(30);

// Create the "order" object for "user2"
Order order = new Order(user2, product);

// Add the "order" object to "user1" -- oops!
user1.Orders.Add(order);

Il codice sopra compila bene, ma si traduce in un errore di runtime che potrebbe essere piuttosto difficile da rintracciare.

Mi rendo conto che questo è andato oltre la tua domanda iniziale, ma penso che guardare il sistema più grande e gli altri casi di utilizzo sia essenziale per fornire una risposta.

Secondo me, non hai bisogno di una "raccolta di sola lettura" o di un clone della collezione esistente. Se la classe dell'ordine implementa correttamente l'incapsulamento, nessuno può modificare i dati per un ordine senza che le regole aziendali vengano applicate dall'oggetto Ordine stesso, quindi non è necessario clonare oggetti in memoria.

  1. Crea un campo privato come IList o ICollection
  2. Crea una proprietà getter pubblica che restituisce la raccolta di lettura / scrittura come oggetto IEnumerable
  3. Ogni oggetto nella raccolta deve implementare correttamente l'incapsulamento, quindi anche se qualche altro oggetto modifica i dati, i dati vengono comunque modificati in base alle regole aziendali
risposta data 01.03.2016 - 03:38
fonte

Leggi altre domande sui tag