问题
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?
回答1:
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:
- Proper selection of provider by the
ClientAPI
- Variable number and type of arguments needed by each provider.
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.
Type relaxation
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('john.doe@gmail.com'));
AuthProvider authProvider = new AuthProviderFactory().get(userData.getType());
authProvider.createAccount(userData);
Advantages
- New providers can be supported by simply adding new entries to
AuthType
andAuthProviderFactory
. - Each
AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.
Disadvantages
- Few parameters in
UserData
won't be strongly typed. SomeAuthProvider
that require additional parameters will have to lookup them i.e.metadata.get('email')
.
Typed 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('john.doe@gmail.com'));
AuthProvider authProvider = new AuthProviderFactory().get(userData);
authProvider.createAccount(userData);
Advantages
- Specialized forms of
UserData
containing strongly typed attributes. - New providers can be supported by simply creating new
UserData
types and adding new entriesAuthProviderFactory
. - Each
AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.
Disadvantages
AuthProviderFactory
usesinstanceof
for selecting the properAuthProvider
.- Explosion of
UserData
subtypes and potentially duplication of code.
Typed UserData
revisited
We 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('john.doe@gmail.com'));
AuthProvider authProvider = new AuthProviderFactory().get(userData.getType());
authProvider.createAccount(userData);
Advantages
- Specialized forms of
UserData
containing strongly typed attributes. - Each
AuthProvider
knows exactly what it needs in order to perform the exposed operations (createAccount()
, etc). The logic and complexity are well encapsulated.
Disadvantages
- Besides adding new entries to
AuthProviderFactory
and creating new subtype forUserData
, new providers will require a new entry in the enumAuthType
. - We still have explosion of
UserData
subtypes but now the reusability of these subtypes has increased.
Summary
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.
回答2:
Given your description, when a client calls the CrateAccount() API, he doesn't know yet what provider will be used. So, if you want a straightforward solution, your CreateAccount() API must require all info it may eventually need.
Adding a new provider requiring a new parameter will always break the API :
- if you add a new parameter to the function, it will break at compile time (which is the easiest way to detect the issue)
- if you use a dictionary/map, it will break at runtime, since you will miss the required info.
However, if you are in an object oriented context, you could use a callback/delegate design pattern :
- Your CreateAccount() function will take a delegate as a single parameter.
- Once CreateAccount() knows which provider will be used, the delegate will be called to collect the required parameters, and only them.
It may be a little bit more elegant, but you will still have runtime issues if you add a new provider and that your clients are not ready to provide the new parameters when asked by the delegate... Unless your API is initialized with the list of providers supported by your client. You would then add the new provider, and your client would enable it only once he's ready.
来源:https://stackoverflow.com/questions/32474587/what-design-pattern-to-use-for-a-client-application-using-multiple-providers