Enfoque DDD para acceder a información externa

Tengo una clase de aplicación bancaria existente como se muestra a continuación. La cuenta bancaria puede ser SavingsBankAccount o FixedBankAccount. Hay una operación llamada IssueLumpSumInterest. Para FixedBankAccount, el saldo debe actualizarse solo si el propietario de la cuenta no tiene otra cuenta.

Esto exige que el objeto FixedBankAccount conozca otras cuentas del propietario de la cuenta. ¿Cómo hacer esto siguiendo el patrón SOLID / DDD / GRASP / Information Expert?

namespace ApplicationServiceForBank { public class BankAccountService { RepositoryLayer.IRepository accountRepository; ApplicationServiceForBank.IBankAccountFactory bankFactory; public BankAccountService(RepositoryLayer.IRepository repo, IBankAccountFactory bankFact) { accountRepository = repo; bankFactory = bankFact; } public void IssueLumpSumInterest(int acccountID) { RepositoryLayer.BankAccount oneOfRepositroyAccounts = accountRepository.FindByID(p => p.BankAccountID == acccountID); int ownerID = (int) oneOfRepositroyAccounts.AccountOwnerID; IEnumerable accountsForUser = accountRepository.FindAll(p => p.BankUser.UserID == ownerID); DomainObjectsForBank.IBankAccount domainBankAccountObj = bankFactory.CreateAccount(oneOfRepositroyAccounts); if (domainBankAccountObj != null) { domainBankAccountObj.BankAccountID = oneOfRepositroyAccounts.BankAccountID; domainBankAccountObj.AddInterest(); this.accountRepository.UpdateChangesByAttach(oneOfRepositroyAccounts); //oneOfRepositroyAccounts.Balance = domainBankAccountObj.Balance; this.accountRepository.SubmitChanges(); } } } public interface IBankAccountFactory { DomainObjectsForBank.IBankAccount CreateAccount(RepositoryLayer.BankAccount repositroyAccount); } public class MySimpleBankAccountFactory : IBankAccountFactory { public DomainObjectsForBank.IBankAccount CreateAccount(RepositoryLayer.BankAccount repositroyAccount) { DomainObjectsForBank.IBankAccount acc = null; if (String.Equals(repositroyAccount.AccountType, "Fixed")) { acc = new DomainObjectsForBank.FixedBankAccount(); } if (String.Equals(repositroyAccount.AccountType, "Savings")) { //acc = new DomainObjectsForBank.SavingsBankAccount(); } return acc; } } } namespace DomainObjectsForBank { public interface IBankAccount { int BankAccountID { get; set; } double Balance { get; set; } string AccountStatus { get; set; } void FreezeAccount(); void AddInterest(); } public class FixedBankAccount : IBankAccount { public int BankAccountID { get; set; } public string AccountStatus { get; set; } public double Balance { get; set; } public void FreezeAccount() { AccountStatus = "Frozen"; } public void AddInterest() { //TO DO: Balance need to be updated only if the person has no other accounts. Balance = Balance + (Balance * 0.1); } } } 

LEYENDO

  1. Problema al usar Composition para la relación “is – a”

  2. Implementación de lógica de negocios (LINQ to SQL) http://msdn.microsoft.com/en-us/library/bb882671.aspx

  3. Arquitectura de aplicaciones LINQ to SQL

  4. Exploración de la architecture N-Tier con LINQ to SQL http://randolphcabral.wordpress.com/2008/05/08/exploring-n-tier-architecture-with-linq-to-sql-part-3-of-n/

  5. ¡Confusión entre DTO (linq2sql) y objetos de Clase!

  6. Diseño impulsado por dominio (Linq a SQL): ¿cómo se eliminan las partes de un agregado?

Lo primero que noté fue el uso indebido de la fábrica de la cuenta bancaria. La fábrica, más o menos como la tiene, debe ser utilizada por el repository para crear la instancia basada en los datos recuperados del almacén de datos. Como tal, su llamada a accountRepository.FindByID devolverá una cuenta FixedBankAccount o SavingsBankAccount en función del AccountType devuelto desde el almacén de datos.

Si el interés solo se aplica a las instancias de FixedBankAccount, puede realizar una verificación de tipo para asegurarse de que está trabajando con el tipo de cuenta correcto.

 public void IssueLumpSumInterest(int accountId) { var account = _accountRepository.FindById(accountId) as FixedBankAccount; if (account == null) { throw new InvalidOperationException("Cannot add interest to Savings account."); } var ownerId = account.OwnerId; if (_accountRepository.Any(a => (a.BankUser.UserId == ownerId) && (a.AccountId != accountId))) { throw new InvalidOperationException("Cannot add interest when user own multiple accounts."); } account.AddInterest(); // Persist the changes } 

NOTA: FindById solo debe aceptar el parámetro ID y no un lambda / Func. Ha indicado con el nombre “FindById” cómo se realizará la búsqueda. El hecho de que el valor ‘accountId’ se compare con la propiedad BankAccountId es un detalle de implementación oculto dentro del método. Denomine el método “FindBy” si desea un enfoque genérico que utilice una lambda.

Tampoco pondría AddInterest en la interfaz IBankAccount si todas las implementaciones no son compatibles con ese comportamiento. Considere una interfaz separada de IInterestEarningBankAccount que expone el método AddInterest. También consideraría usar esa interfaz en lugar de FixedBankAccount en el código anterior para hacer que el código sea más fácil de mantener y ampliar si agrega otro tipo de cuenta en el futuro que admita este comportamiento.

De leer su requisito, aquí es cómo lo haría:

 //Application Service - consumed by UI public class AccountService : IAccountService { private readonly IAccountRepository _accountRepository; private readonly ICustomerRepository _customerRepository; public ApplicationService(IAccountRepository accountRepository, ICustomerRepository customerRepository) { _accountRepository = accountRepository; _customerRepository = customerRepository; } public void IssueLumpSumInterestToAccount(Guid accountId) { using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create()) { Account account = _accountRepository.GetById(accountId); Customer customer = _customerRepository.GetById(account.CustomerId); account.IssueLumpSumOfInterest(customer); _accountRepository.Save(account); } } } public class Customer { private List _accountIds; public IEnumerable AccountIds { get { return _accountIds.AsReadOnly();} } } public abstract class Account { public abstract void IssueLumpSumOfInterest(Customer customer); } public class FixedAccount : Account { public override void IssueLumpSumOfInterest(Customer customer) { if (customer.AccountIds.Any(id => id != this._accountId)) throw new Exception("Lump Sum cannot be issued to fixed accounts where the customer has other accounts"); //Code to issue interest here } } public class SavingsAccount : Account { public override void IssueLumpSumOfInterest(Customer customer) { //Code to issue interest here } } 
  1. El método IssueLumpSumOfInterest en el agregado de la cuenta requiere el agregado del cliente para ayudar a decidir si se debe emitir el interés.
  2. El agregado del cliente contiene una lista de ID de cuenta, NO una lista de agregados de cuenta.
  3. La ‘Cuenta’ de la clase base tiene un método polimórfico: la Cuenta Fija verifica que el cliente no tenga otras cuentas; SavingsAccount no realiza esta verificación.

2 minutos de respuesta de escaneo …

  • No estoy seguro de por qué hay una necesidad de 2 representaciones de una cuenta BankAccount RepositoryLayer.BankAccount y DomainObjectsForBank.IBankAccount . Oculta la capa de persistencia acoplada una … trata solo con el objeto de dominio en el servicio.
  • No pase / devuelva Nulls – Creo que es un buen consejo.
  • Los métodos de búsqueda se parecen a los métodos LINQ que seleccionan elementos de una lista de colección. Sus métodos parecen que quieren obtener la primera coincidencia y salir … en cuyo caso sus parámetros pueden ser primitivos simples (Id) vs lambdas.

La idea general parece correcta. El servicio encapsula la lógica de esta transacción, no los objetos de dominio. Si esto cambia, solo un lugar para actualizar.

 public void IssueLumpSumInterest(int acccountID) { var customerId = accountRepository.GetAccount(accountId).CustomerId; var accounts = accountRepository.GetAccountsForCustomer(customerId); if ((accounts.First() is FixedAccount) && accounts.Count() == 1) { // update interest } } 

Cosas que me parecen raras:

  • Su IBankAccount tiene un método FreezeAccount , pero supongo que todas las cuentas tendrían un comportamiento bastante similar. ¿Tal vez una clase de cuenta BankAccount está garantizada que implementa parte de la interfaz?
  • AccountStatus probablemente debería ser una enumeración? ¿Qué debería pasar si una cuenta es “Forzen”?