I was asked to refactor a class, to make it adhere to SOLID principles, with testability and readability. I thought I had done a decent job, only my feedback was:
a) Unnecessary introduction of AccountExists() method [I thought it increased readability]
b) Breach of SOLID principles using "switch", "new" keywords
c) Backward compatibility not maintained [I was told I should not change the method signature of the MakePayment method]
Here is my refactored code:
I've left out most of the interface definitions because they are obvious, all methods implemented are defined on the interface:
public class NewPaymentService : IPaymentService
{
private readonly IAccountDataStore _accountDataStore;
private readonly IPaymentValidatorFactory _paymentValidatorFactory;
public NewPaymentService(
IAccountDataStore accountDataStore,
IPaymentValidatorFactory paymentValidatorFactory)
{
_accountDataStore = accountDataStore;
_paymentValidatorFactory = paymentValidatorFactory;
}
public MakePaymentResult MakePayment(MakePaymentRequest request)
{
if (!_accountDataStore.AccountExists(request.DebtorAccountNumber))
{
return new MakePaymentResult()
{
Success = false,
FailureReason = "Account does not exist"
};
}
var account = _accountDataStore.GetAccount(request.DebtorAccountNumber);
var paymentValidator = _paymentValidatorFactory.GetPaymentValidator(request.PaymentScheme);
bool isValid = paymentValidator.ValidatePayment(request, account);
if (!isValid)
{
return new MakePaymentResult()
{
Success = false,
FailureReason = "Payment is invalid"
};
}
account.Balance -= request.Amount;
_accountDataStore.UpdateAccount(account);
return new MakePaymentResult()
{
Success = true
};
}
}
public class PaymentValidatorFactory : IPaymentValidatorFactory
{
public IPaymentValidator GetPaymentValidator(PaymentScheme paymentScheme)
{
switch (paymentScheme)
{
case PaymentScheme.Bacs:
return new BacsPaymentValidator();
case PaymentScheme.FasterPayments:
return new FasterPaymentValidator();
case PaymentScheme.Chaps:
return new ChapsPaymentValidator();
default:
throw new ArgumentException($"No payment validator available for {paymentScheme}");
}
}
}
public class FasterPaymentValidator : IPaymentValidator
{
public bool ValidatePayment(MakePaymentRequest request, Account account)
{
if (!account.AllowedPaymentSchemes.HasFlag(AllowedPaymentSchemes.FasterPayments))
{
return false;
}
if (account.Balance < request.Amount)
{
return false;
}
return true;
}
}
public class AccountDataStore : IAccountDataStore
{
public bool AccountExists(string accountNumber)
{
throw new System.NotImplementedException();
}
public Account GetAccount(string accountNumber)
{
// Access database to retrieve account, code removed for brevity
return new Account();
}
public void UpdateAccount(Account account)
{
// Update account in database, code removed for brevity
}
}
public class BackupAccountDataStore : IAccountDataStore
{
public bool AccountExists(string accountNumber)
{
throw new System.NotImplementedException();
}
public Account GetAccount(string accountNumber)
{
// Access backup data base to retrieve account, code removed for brevity
return new Account();
}
public void UpdateAccount(Account account)
{
// Update account in backup database, code removed for brevity
}
}
Here is the old method I was asked to refactor:
public class OldPaymentService : IPaymentService
{
public MakePaymentResult MakePayment(MakePaymentRequest request)
{
var dataStoreType = ConfigurationManager.AppSettings["DataStoreType"];
Account account = null;
if (dataStoreType == "Backup")
{
var accountDataStore = new BackupAccountDataStore();
account = accountDataStore.GetAccount(request.DebtorAccountNumber);
}
else
{
var accountDataStore = new AccountDataStore();
account = accountDataStore.GetAccount(request.DebtorAccountNumber);
}
var result = new MakePaymentResult();
switch (request.PaymentScheme)
{
case PaymentScheme.Bacs:
if (account == null)
{
result.Success = false;
}
else if (!account.AllowedPaymentSchemes.HasFlag(AllowedPaymentSchemes.Bacs))
{
result.Success = false;
}
break;
case PaymentScheme.FasterPayments:
if (account == null)
{
result.Success = false;
}
else if (!account.AllowedPaymentSchemes.HasFlag(AllowedPaymentSchemes.FasterPayments))
{
result.Success = false;
}
else if (account.Balance < request.Amount)
{
result.Success = false;
}
break;
case PaymentScheme.Chaps:
if (account == null)
{
result.Success = false;
}
else if (!account.AllowedPaymentSchemes.HasFlag(AllowedPaymentSchemes.Chaps))
{
result.Success = false;
}
else if (account.Status != AccountStatus.Live)
{
result.Success = false;
}
break;
}
if (result.Success)
{
account.Balance -= request.Amount;
if (dataStoreType == "Backup")
{
var accountDataStore = new BackupAccountDataStore();
accountDataStore.UpdateAccount(account);
}
else
{
var accountDataStore = new AccountDataStore();
accountDataStore.UpdateAccount(account);
}
}
return result;
}
}
public class AccountDataStore
{
public Account GetAccount(string accountNumber)
{
// Access database to retrieve account, code removed for brevity
return new Account();
}
public void UpdateAccount(Account account)
{
// Update account in database, code removed for brevity
}
}
public class BackupAccountDataStore
{
public Account GetAccount(string accountNumber)
{
// Access backup data base to retrieve account, code removed for brevity
return new Account();
}
public void UpdateAccount(Account account)
{
// Update account in backup database, code removed for brevity
}
}
I cannot understand how the feedback was so critical. AccountExists()
, it may be an extra database call, but it is better than returning null, which the reader has to assume means that the account doesn't exist.
I inverted the dependencies so that IAccountDataStore
type could be defined in a configuration startup class that does the DI, rather than having to check the app settings in the method.
I can't see how using 'new' to instantiate a DTO (MakePaymentResult
) is a violation of SOLID, not using 'new' is meant to apply to behaviour implementations. And they said not to change the signature, how else can you return a MakePaymentResult
?!
I used switch in a Factory class to get the right IPaymentValidator
. That's the only place it was used, and I thought a Factory class was exactly the kind of place where you'd convert an enum to an implementation, for using the strategy pattern.
Can you give me any feedback on the feedback? What have I missed so badly?
AccountExists
does improve readability - a check fornull
would be just as clear. The rest of the feedback seems... misguided. I mean, that's pretty much a textbook implementation of the factory pattern. I'm surprised you didn't get feedback about multiple returns being bad given the first code's extra complexity just to have one return. I don't see how you've broken backwards compact either - you've only added a property to the return type? \$\endgroup\$