This is a design related question.
Lets say we have a public API called ClientAPI with a few web methods like CreateAccount, GetAccount. Depending on the customer, we use a number of different providers to fulfil these requests.
So say we have ProviderA and ProviderB and ProviderC.
ProviderA has a method signature/implementation of CreateAccount that needs (Firstname, Lastname) only and creates an account with ProviderA.
ProviderB has a method signature/implementation of CreateAccount that needs (Firstname, Lastname, Email, DOB) and creates an account with ProviderB.
ProviderC has a method signature/implementation of CreateAccount that needs (Nickname, CompanyKey, Email) and creates an account with ProviderC.
The Client doesn’t need to know or care about which provider they are. When the Client API method CreateAccount is called, the client api will work out what provider(s) it needs to call and invokes that Providers Method.
So there are two questions I have here.
1) What is the best design/pattern to implement for this model? Also bearing in mind that the number of providers will grow – we will be adding more providers.
2) Regarding passing parameters – currently the ClientAPI CreateAccount method signature is a big line of variables, and if a new provider needs a new value, the method signature has another variable added to it, which obviously breaks the old implementations etc. Is it a good practice to pass an array/list/dictionary of parameters in the method signature and into the providers below, or is there a better way?
It is indeed an interesting question. I've encountered myself few problems like this in different projects I worked on. After reading your questions, I noticed you have two different challenges:
ClientAPI
When I'm designing a service or new feature, I like to reason about design by trying to minimize the number of changes I would need to make in order to support a new functionality. In your case, it would be the addition of new authentication provider. At least three different ways to implement that come to my mind right now. In my opinion, there is no perfect solution. You will have to choose one of them based on tradeoffs. Below, I try to present few options addressing these two pain points listed above along with their advantages and disadvantages.
No matter what we do, no matter how good we are abstracting complexity using polymorphism, there is always a different type or component that distinguishes itself from its simblings by requiring a different set of information. Depending on how much effort you want to put in your design to keep it strongly typed and on how different your polymorphic abstractions are, it will require more changes when adding new features. Below there is an example of implementation that does not enforce types for all kinds of information provided by the user.
public class UserData {
private AuthType type;
private String firstname;
private String lastname;
private Map<String, String> metadata;
}
public enum AuthType {
FACEBOOK, GPLUS, TWITTER;
}
public interface AuthProvider {
void createAccount(UserData userData);
void login(UserCredentials userCredentials);
}
public class AuthProviderFactory {
public AuthProvider get(AuthType type) {
switch(type) {
case FACEBOOK:
return new FacebookAuthProvider();
case GPLUS:
return new GPlusAuthProvider();
case TWITTER:
return new TwitterAuthProvider();
default:
throw new IllegalArgumentException(String.format('Invalid authentication type %s', type));
}
}
}
// example of usage
UserData userData = new UserData();
userData.setAuthType(AuthType.FACEBOOK);
userData.setFirstname('John');
userData.setLastname('Doe');
userData.putExtra('dateOfBirth', LocalDate.of(1997, 1, 1));
userData.putExtra('email', Email.fromString('[email protected]'));
AuthProvider authProvider = new AuthProviderFactory().get(userData.getType());
authProvider.createAccount(userData);
AuthType
and AuthProviderFactory
.AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.UserData
won't be strongly typed. Some AuthProvider
that require additional parameters will have to lookup them i.e. metadata.get('email')
.UserData
I assume that the component in charge of invoking AuthProviderFactory
already knows a little bit about the type of provider it needs since it will have to fill out UserData
with all the information needed for a successful createAccount()
call. So, what about letting this component create the correct type of UserData
?
public class UserData {
private String firstname;
private String lastname;
}
public class FacebookUserData extends UserData {
private LocalDate dateOfBirth;
private Email email;
}
public class GplusUserData extends UserData {
private Email email;
}
public class TwitterUserData extends UserData {
private Nickname nickname;
}
public interface AuthProvider {
void createAccount(UserData userData);
void login(UserCredentials userCredentials);
}
public class AuthProviderFactory {
public AuthProvider get(UserData userData) {
if (userData instanceof FacebookUserData) {
return new FacebookAuthProvider();
} else if (userData instanceof GplusUserData) {
return new GPlusAuthProvider();
} else if (userData instanceof TwitterUserData) {
return new TwitterAuthProvider();
}
throw new IllegalArgumentException(String.format('Invalid authentication type %s', userData.getClass()));
}
}
// example of usage
FacebookUserData userData = new FacebookUserData();
userData.setFirstname('John');
userData.setLastname('Doe');
userData.setDateOfBirth(LocalDate.of(1997, 1, 1));
userData.setEmail(Email.fromString('[email protected]'));
AuthProvider authProvider = new AuthProviderFactory().get(userData);
authProvider.createAccount(userData);
UserData
containing strongly typed attributes.UserData
types and adding new entries AuthProviderFactory
.AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.AuthProviderFactory
uses instanceof
for selecting the proper AuthProvider
.UserData
subtypes and potentially duplication of code.UserData
revisitedWe can try removing code duplication by reintroducing the enum AuthType
to our previous design and making our UserData
subclasses a little bit more general.
public interface UserData {
AuthType getType();
}
public enum AuthType {
FACEBOOK, GPLUS, TWITTER;
}
public class BasicUserData implements UserData {
private AuthType type:
private String firstname;
private String lastname;
public AuthType getType() { return type; }
}
public class FullUserData extends BasicUserData {
private LocalDate dateOfBirth;
private Email email;
}
public class EmailUserData extends BasicUserData {
private Email email;
}
public class NicknameUserData extends BasicUserData {
private Nickname nickname;
}
public interface AuthProvider {
void createAccount(UserData userData);
void login(UserCredentials userCredentials);
}
public class AuthProviderFactory {
public AuthProvider get(AuthType type) {
switch(type) {
case FACEBOOK:
return new FacebookAuthProvider();
case GPLUS:
return new GPlusAuthProvider();
case TWITTER:
return new TwitterAuthProvider();
default:
throw new IllegalArgumentException(String.format('Invalid authentication type %s', type));
}
}
}
// example of usage
FullUserData userData = new FullUserData();
userData.setAuthType(AuthType.FACEBOOK);
userData.setFirstname('John');
userData.setLastname('Doe');
userData.setDateOfBirth(LocalDate.of(1997, 1, 1));
userData.setEmail(Email.fromString('[email protected]'));
AuthProvider authProvider = new AuthProviderFactory().get(userData.getType());
authProvider.createAccount(userData);
UserData
containing strongly typed attributes.AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.AuthProviderFactory
and creating new subtype for UserData
, new providers will require a new entry in the enum AuthType
.UserData
subtypes but now the reusability of these subtypes has increased.Im pretty sure there are several other solutions for this problem. As I mentioned above, there are no perfect solution either. You might have to choose one based on their tradeoffs and the goals you want to achieve.
I'm not very well inspired today, so I will keep updating this post if something else comes to my mind.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With