Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 990
Spotzer Dev Avatar asked Sep 26 '22 19:09

Spotzer Dev


1 Answers

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('[email protected]'));

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('[email protected]'));

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('[email protected]'));

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.

like image 168
Trein Avatar answered Oct 22 '22 17:10

Trein