La coesione di alta classe viola il linguaggio dei domini onnipresenti

1

Originariamente pubblicato su CodeReview, ma è stato rilevato che sarebbe stato pubblicato qui meglio. L'altra domanda è stata cancellata.

Creazione di un software di finanza personale, ma in esecuzione in un problema di architettura. Ho una classe chiamata Account che contiene Transactions e SubAccounts . Sono molto avanti nella fase di codifica, ma sto vedendo che la mia classe Account potrebbe potenzialmente diventare molto grande.

Questo è quello che ho:

public class Account
{
    public Account(string name)
    {
        Name = name;
        _subAccounts = new List<Account>();
        Transactions = new List<Transaction>();
    }

    public string Name { get; }
    public List<Transaction> Transactions { get; }

    public IEnumerable<Account> SubAccounts => _subAccounts;

    public decimal Balance => ComputeBalance();

    public void AddSubAccount(Account account)
    {
        Debug.Assert(account != null);

        _subAccounts.Add(account);
    }

    public Account FindSubAccount(string name)
    {
        // logic to find account
        return account;
    }

    public void RemoveSubAccount(Account account)
    {
        Debug.Assert(account != null);
        _subAccounts.Remove(account);
    }

    public void AddTransaction(Transaction transaction)
    {
        Transactions.Add(transaction);
    }

    public void FindTransactionByID(TransactionID id)
    {
        // logic to find transaction
        return transaction;
    }

    public void RemoveTransaction(Transaction transaction)
    {
        Transactions.Remove(transaction);
    }

    private decimal ComputeBalance()
    {
        decimal balance = 0M;

        foreach (var transaction in Transactions)
        {
            balance += transaction.Amount;
        }

        return balance;
    }

    private readonly List<Account> _subAccounts;
}

public class Transaction
{
    public Transaction(string desc, decimal amount, Account debit, Account credit)
    {
        Description = desc;
        Amount = amount;
        Credit = credit;
        Debit = debit;
    }

    public string Description { get; }
    public decimal Amount { get; }
    public Account Credit { get; }
    public Account Debit { get;  }
}

Il problema che sta iniziando a comparire è che la mia classe Account sembra molto segregata tra 3 parti: subaccount, transazioni e calcolo di un saldo. Tuttavia, in termini di parlare di contabilità, un account finanziario ha tutti questi attributi.

La mia domanda è: cosa fare per evitare che una classe diventi troppo grande (troppe responsabilità) quando si cerca di attenersi al linguaggio del dominio ubiquitario?

Per risolvere il problema, ho avuto l'idea di creare una struttura dati ad albero che usasse AccountNodes che mantiene puntatori ad altri AccountNodes (per simulare SubAccounts) e Account . Ciò separerebbe la struttura gerarchica fisica dall'account.

    
posta keelerjr12 29.09.2018 - 20:50
fonte

4 risposte

1

Il tuo concetto di classe Account va bene. Ovviamente hai una limitazione tecnica, in particolare con la lista Transazioni, che potrebbe ovviamente diventare troppo grande per essere popolata e conservata in memoria.

Tuttavia, è possibile risolvere questa limitazione tecnica senza modificare drasticamente il design della classe iniettando un servizio di dominio per le transazioni e alterando leggermente le firme dei metodi per utilizzare i parametri di ricerca. vale a dire

public class Account
{
    public Account(ITransactionService trans)

    public IEnumerable<Transaction> GetTransactionsByDate(DateTime start, DateTime end)
    {
        return trans.GetTransactionsByDate(this.Id, start,end);
    }
}

In questo modo hai effettivamente creato un nuovo contesto limitato per Transazione. stiamo consentendo alle Transazioni di esistere al di fuori degli account e di unire i due contesti con un servizio di dominio

    
risposta data 29.09.2018 - 21:04
fonte
1

La tua classe Account è semplicemente un Repository . Tutto ciò che contiene è metodi CRUD, per la maggior parte. L'unico metodo in quella classe che fa qualsiasi lavoro reale è il metodo ComputeBalance .

Per correggere il problema, rimuovi i metodi CRUD da Account (relegandoli a un repository) e inserisci un DTO nel costruttore che contiene i dati necessari per calcolare il saldo.

Questo separa la preoccupazione su dove e come ottenere i dati nel tuo livello di accesso ai dati, a cui appartiene.

    
risposta data 29.09.2018 - 21:18
fonte
1

Dominio delle finanze personali

Nel dominio semplificato di finanza personale un Account di solito rappresenta un conto entrate e uscite , ad esempio un conto in contanti (il tuo portafoglio) o un conto bancario.

In questo dominio, Account potrebbe essere una radice aggregata:

  • Può essere diviso in sotto-conti (conto corrente, conto carta di pagamento, conto deposito, ecc.). Alcune banche scrivono per esempio sullo stesso estratto conto, sulle operazioni di una carta di pagamento e sulle operazioni dei conti correnti. Altre banche usano un suffisso subaccount e il suffisso ha senso solo con l'account principale.

  • Le transazioni sono singole variazioni di valori nei (sotto) conti. Ogni singola riga è identificata in modo univoco solo in relazione al numero di conto.

In questo contesto, il tuo modello potrebbe essere valido. AddSubAccount() e FindTransactionPerID() non sarebbero operazioni di repository, ma accesso a entità aggregate tramite la radice aggregata.

Tuttavia ci sono diverse incongruenze con Transaction che suggeriscono di riconsiderare seriamente il tuo modello di dominio.

Domanda di design principale: relazione tra transazioni e account

Osservando più da vicino il tuo Transaction , sembra che ciascuna transazione sia correlata a due account, con un solo importo. Ciò suggerisce che le transazioni non dovrebbero appartenere a un aggregato di account, ma dovrebbe essere un aggregato indipendente memorizzato nel proprio TransactionRepository .

Inoltre, sembra che gli account secondari siano essi stessi account e possano essere visualizzati direttamente nelle transazioni. Questo suggerisce che ogni sotto-account è un account indipendente a cui è possibile accedere direttamente. Quindi dovrebbero essere aggiunti e trovati di nuovo da AccountRepository

Un altro suggerimento sarebbe considerare un modello di transazione più potente , che consentirebbe di avere più importi e conti in un'unica transazione . Ciò potrebbe ad esempio avere una sola spesa sul conto bancario, ma assegnarla a più conti di spesa (ad esempio cibo, bevande, vestiti). Ma forse quest'ultimo è per la prossima versione; -)

    
risposta data 30.09.2018 - 17:19
fonte
1

Mi piace l'idea di spostare una parte della logica in una struttura dati separata. Per non reinventare la ruota, userei la semantica fornita da ICollection<T> . Questo ha anche il vantaggio di poter applicare tecniche conosciute come il looping con foreach o l'interrogazione con LINQ.

Entrambe, le raccolte di conti e transazioni hanno una logica comune come poter essere ricercate dall'ID. Pertanto, vorrei creare un'interfaccia speciale per queste cose, che a sua volta implementa ICollection<T> :

public interface IDomainCollection<T> : ICollection<T>
{
    T FindById(int id);
    //TODO: Add other common logic
}

Una parte sostanziale di questa logica può essere implementata in una classe base astratta:

public abstract class DomainCollection<T> : IDomainCollection<T>
{
    protected readonly List<T> _innerList = new List<T>();

    public abstract T FindById(int id);

    // ICollection<T> members
    public int Count => _innerList.Count;
    public bool IsReadOnly => false;
    public void Add(T item) => _innerList.Add(item);
    public void Clear() => _innerList.Clear();
    public bool Contains(T item) => _innerList.Contains(item);
    public void CopyTo(T[] array, int arrayIndex) => _innerList.CopyTo(array, arrayIndex);
    public bool Remove(T item) => _innerList.Remove(item);
    public IEnumerator<T> GetEnumerator() => _innerList.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_innerList).GetEnumerator();
}

Ora le due raccolte richieste possono essere implementate facilmente:

public class AccountCollection : DomainCollection<Account>
{
    public Account FindByName(string name) // Specific to accounts
    {
        throw new NotImplementedException();
    }

    public override Account FindById(int id)
    {
        throw new NotImplementedException();
    }
}

public class TransactionCollection : DomainCollection<Transaction>
{
    public override Transaction FindById(int id)
    {
        throw new NotImplementedException();
    }
}

La logica della classe Account viene ridotta alla logica di bilanciamento:

public class Account
{
    public Account(string name)
    {
        Name = name;
    }

    public string Name { get; }

    public TransactionCollection Transactions { get; } = new TransactionCollection();

    public AccountCollection SubAccounts { get; } = new AccountCollection();

    public decimal Balance => ComputeBalance();

    private decimal ComputeBalance()
    {
        decimal balance = 0M;
        foreach (var transaction in Transactions) {
            balance += transaction.Amount;
        }
        return balance;
    }
}

La semantica per i subaccount e le transazioni cambia da

account.AddSubAccount(newAccount);
Account subAccount = account.FindSubAccount(name);
Transaction transaction = account.FindTransactionByID(id);

a

account.SubAccounts.Add(newAccount);
Account subAccount = account.SubAccounts.FindByName(name);
Transaction transaction = account.Transactions.FindById(id);

Hai ancora il pieno controllo di ciò che fanno queste collezioni. È possibile aggiungere asserzioni di debug ed eseguire ulteriore logica, se necessario.

    
risposta data 30.09.2018 - 22:01
fonte

Leggi altre domande sui tag