什么 design/pattern 用于使用多个提供程序的客户端应用程序?

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

这是一个设计相关的问题。

假设我们有一个名为 ClientAPI 的 public API,带有一些网络方法,如 CreateAccount、GetAccount。根据客户的不同,我们使用许多不同的供应商来满足这些要求。

假设我们有 ProviderA、ProviderB 和 ProviderC。

ProviderA 有一个 CreateAccount 方法 signature/implementation 只需要 (Firstname, Lastname) 并创建 ProviderA 的帐户。

ProviderB 有一个 CreateAccount 方法 signature/implementation 需要(名字、姓氏、电子邮件、出生日期)并使用 ProviderB 创建一个帐户。

ProviderC 有一个 CreateAccount 方法 signature/implementation 需要(昵称、公司密钥、电子邮件)并使用 ProviderC 创建一个帐户。

客户不需要知道或关心他们是哪个提供商。当调用客户端 API 方法 CreateAccount 时,客户端 api 将计算出它需要调用的提供程序并调用该提供程序方法。

所以这里有两个问题。

1) 这个模型最好的 design/pattern 是什么?还要记住,提供者的数量将会增加——我们将增加更多的提供者。

2) 关于传递参数——目前ClientAPI CreateAccount 方法签名是一大行变量,如果新的提供者需要一个新值,方法签名会添加另一个变量,它显然打破了旧的实现等。在方法签名中传递一个 array/list/dictionary 参数并传递给下面的提供者是一个好习惯,还是有更好的方法?

这确实是一个有趣的问题。在我从事的不同项目中,我遇到过一些这样的问题。阅读您的问题后,我注意到您面临两个不同的挑战:

  1. ClientAPI
  2. 正确选择提供商
  3. 每个提供商所需的参数数量和类型可变。

当我设计一项服务或新功能时,我喜欢通过尽量减少为支持新功能而需要进行的更改数量来进行设计推理。在您的情况下,这将是添加新的身份验证提供程序。我现在想到至少三种不同的实施方式。在我看来,没有完美的解决方案。您将不得不根据权衡选择其中之一。下面,我尝试提出一些解决上面列出的这两个痛点的选项以及它们的优点和缺点。

打字放松

无论我们做什么,无论我们使用多态性将复杂性抽象得多么好,总会有一种不同的类型或组件通过需要一组不同的信息来区别于它的同类。根据您希望在设计中投入多少精力以保持其强类型以及多态抽象的不同程度,在添加新功能时需要进行更多更改。下面是一个不强制用户提供的各种信息的类型的实现示例。

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);

优点

  • 只需向 AuthTypeAuthProviderFactory 添加新条目即可支持新提供程序。
  • 每个 AuthProvider 都确切地知道它需要什么来执行公开的操作(createAccount(),等等)。逻辑和复杂性封装得很好。

缺点

  • UserData 中的少数参数不是强类型的。一些需要额外参数的 AuthProvider 将不得不查找它们,即 metadata.get('email').

输入UserData

我假设负责调用 AuthProviderFactory 的组件已经知道它需要的提供者类型,因为它必须填写 UserData 所需的所有信息createAccount() 调用成功。那么,让这个组件创建正确类型的 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);

优点

  • 包含强类型属性的 UserData 的特殊形式。
  • 只需创建新的 UserData 类型并添加新的条目 AuthProviderFactory
  • 即可支持新的提供商
  • 每个 AuthProvider 都确切地知道它需要什么来执行公开的操作(createAccount(),等等)。逻辑和复杂性封装得很好。

缺点

  • AuthProviderFactory 使用 instanceof 选择合适的 AuthProvider
  • UserData 子类型的激增和潜在的代码重复。

已键入 UserData 已重访

我们可以通过将枚举 AuthType 重新引入我们之前的设计并使我们的 UserData 子类更通用一些来尝试消除代码重复。

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);

优点

  • 包含强类型属性的 UserData 的特殊形式。
  • 每个 AuthProvider 都确切地知道它需要什么来执行公开的操作(createAccount(),等等)。逻辑和复杂性封装得很好。

缺点

  • 除了向 AuthProviderFactory 添加新条目并为 UserData 创建新子类型外,新提供者还需要在枚举 AuthType.
  • 中添加新条目
  • 我们仍然有 UserData 子类型的爆炸式增长,但现在这些子类型的可重用性有所提高。

总结

我很确定这个问题还有其他几种解决方案。正如我上面提到的,也没有完美的解决方案。您可能必须根据他们的权衡和您想要实现的目标来选择一个。

我今天的灵感不是很好,所以我会继续更新这个 post 如果我想到其他事情。

根据您的描述,当客户调用 CrateAccount() API 时,他还不知道将使用哪个提供商。因此,如果您想要一个简单的解决方案,您的 CreateAccount() API 必须需要它最终可能需要的所有信息。

添加需要新参数的新提供者总是会破坏 API :

  • 如果向函数添加新参数,它会在编译时中断(这是检测问题的最简单方法)
  • 如果您使用 dictionary/map,它会在运行时中断,因为您会错过所需的信息。

但是,如果您处于面向对象的上下文中,则可以使用 callback/delegate 设计模式:

  1. 您的 CreateAccount() 函数会将委托作为单个参数。
  2. 一旦 CreateAccount() 知道将使用哪个提供者,将调用委托来收集所需的参数,并且仅收集它们。

它可能更优雅一点,但如果您添加一个新的提供者并且您的客户在委托要求时还没有准备好提供新参数,您仍然会遇到运行时问题...除非您的API 使用您的客户端支持的提供商列表进行初始化。然后您将添加新的提供商,而您的客户只会在他准备好后启用它。