Answer To ‘How Would You Design This Adapter?’
Today I’ll answer the question I posed yesterday about how we might want to design a reusable adapter retrieving data in two different ways.
Let’s get into it.
OK, our adapter, XyzCustomerSource, was already able to retrieve a Customer by OrgId and CustomerId from the XYZ REST API via the GetCustomer() method:
public class XyzCustomerSource : ICustomerSource { // ... public async Task<Customer> GetCustomer(CustomerIdentifier id) { // Code to get a customer from the XYZ CRM REST API by OrgId and CustomerId. } }
This method would work well in a system—we called this ‘System 1’—where OrgId and CustomerId were known and available to callers of XyzCustomerSource.
But what if we had another system—a ‘System 2’—and the concept of CustomerId does not exist there? Instead, Customers are identified by CustomerNumber. And luckily, the XYZ API also allows us to retrieve Customers by OrgId and CustomerNumber.
So much for a quick recap.
My question was:
How would you change the design of XyzCustomerSource to handle single Customer retrieval by CustomerId or CustomerNumber? It would help if you provided some high-level pseudo-code for the crucial parts of XyzCustomerSource.
I can think of a couple of options. I’ll start with the less elegant and less encapsulated option first:
Obvious, Inelegant Solution:
Here we might go ahead and forgo the use of CustomerIdentifier and set up two methods on ICustomerSource and XyzCustomerSource, one using OrgId and CustomerId and the other OrgId and CustomerNumber, depending on which combination we know we have in a given situation.
ICustomerSource then becomes:
public interface ICustomerSource { Task<Customer> GetCustomer(Guid orgId, Guid customerId); Task<Customer> GetCustomer(Guid orgId, string customerNumber); }
and XyzCustomerSource will implement both GetCustomer() overloads:
public class XyzCustomerSource : ICustomerSource { // ... public async Task<Customer> GetCustomer(Guid orgId, Guid customerId) { // Code to get a customer from the XYZ CRM REST API by OrgId and CustomerId. } public async Task<Customer> GetCustomer(Guid orgId, string customerNumber) { // Code to get a customer from the XYZ CRM REST API by OrgId and CustomerNumber. } }
The outlined approach will work but has a significant disadvantage:
High-level business logic classes, the callers of the GetCustomer() overloads must become aware of much detail. Specifically, callers would need to become aware of two points:
1.) Which GetCustomer() method to call—Before, there was only one method, and now there are two.
2.) What the different customer identifiers are—OrgId, CustomerId, CustomerNumber. The CustomerIdentifier class abstraced away this detail but not anymore. And here, we may run into trouble in the future: Now CustomerId is a Guid, but if at some point it became a string, we would need to change the signature and all the invocations of the GetCustomer() overload using CustomerId. That could be a task involving changes to many files.
The Better, Elegant Solution:
Let’s keep ICustomerSource the same as we had at the beginning:
public interface ICustomerSource { Task<Customer> GetCustomer(CustomerIdentifier id); }
Inside CustomerIdentifier we will have either CustomerId or CustomerNumber. These identifiers are mutually exclusive.
That implies we could find out which of OrgId/CustomerId or OrgId/CustomeNumber combination callers passed into XyzCustomerSource.GetCustomer(). Accordingly, logic inside GetCustomer() decides how to retrieve a Customer from the XYZ API:
public class XyzCustomerSource : ICustomerSource { // ... public async Task<Customer> GetCustomer(CustomerIdentifier id) { if (HasCustomerId(id)) return await GetByCustomerId(id); return await GetByCustomerNumber(id); } }
where
private bool HasCustomerId(CustomerIdentifier id) { return id.CustomerId.HasValue; }
and
private async Task<Customer> GetByCustomerId(CustomerIdentifier id) { // Code to get a customer from the XYZ CRM REST API by OrgId and CustomerId. } private async Task<Customer> GetByCustomerNumber(CustomerIdentifier id) { // Code to get a customer from the XYZ CRM REST API by OrgId and CustomerNumber. }
This design has a couple of advantages:
1.) Avoidance of Feature Envy—XyzCustomerSource is the correct place to encapsulate the logic about which call to make to the XYZ CRM API given the available information. Why should the caller know this? It seems too much inappropriate detail for a high-level caller to have to know. XyzCustomerSource is a better place for this information.
2.) High-level callers can remain ignorant about which details—The smarts now live in the XyzCustomerSource adapter. How cool is this? Callers invoke XyzCustomerSource.GetCustomer() with a bunch of customer identifiers, and this adapter all by itself works out which call to make. Now we have a reusable class! In both systems, System 1 and System 2, we can make identical calls—the only difference is that the CustomerIdentifier parameter will contain different data.
Whenever you pull off this degree of reusability with classes, you have done well, and it’s time to feel a little bit chuffed with your efforts. I know I do whenever I manage it.
Leave a Reply
Want to join the discussion?Feel free to contribute!