What design/pattern to use for a Client application using multiple providers?

自作多情 提交于 2020-01-05 02:10:49

问题


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:

  1. Proper selection of provider by the ClientAPI
  2. 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 and AuthProviderFactory.
  • 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. Some AuthProvider 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 entries AuthProviderFactory.
  • 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 uses instanceof for selecting the proper AuthProvider.
  • 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 for UserData, new providers will require a new entry in the enum AuthType.
  • 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 :

  1. Your CreateAccount() function will take a delegate as a single parameter.
  2. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!