使用多个提供程序的客户端应用程序使用什么设计/模式?

时间:2015-09-09 08:27:48

标签: design-patterns architecture adapter strategy-pattern

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

假设我们有一个名为ClientAPI的公共API,其中包含一些Web方法,如CreateAccount,GetAccount。根据客户的不同,我们会使用许多不同的提供商来满足这些要求。

所以说我们有ProviderA和ProviderB以及ProviderC。

ProviderA具有CreateAccount的方法签名/实现,只需要(名字,姓氏)并使用ProviderA创建帐户。

ProviderB具有CreateAccount的方法签名/实现,需要(名字,姓氏,电子邮件,DOB)并使用ProviderB创建帐户。

ProviderC有一个CreateAccount的方法签名/实现,需要(Nickname,CompanyKey,Email)并使用ProviderC创建一个帐户。

客户无需知道或关心他们是哪个提供商。调用客户端API方法CreateAccount时,客户端api将确定需要调用的提供程序并调用该提供程序方法。

所以我在这里有两个问题。

1)为此模型实施的最佳设计/模式是什么?还要记住,提供商的数量会增加 - 我们将增加更多的提供商。

2)关于传递参数 - 当前ClientAPI CreateAccount方法签名是一大串变量,如果新的提供者需要一个新值,方法签名会添加另一个变量,这显然会破坏旧的实现等。将方法签名中的参数数组/列表/字典传递到下面的提供程序中是一种好习惯,还是有更好的方法?

2 个答案:

答案 0 :(得分:1)

这确实是一个有趣的问题。在我工作的不同项目中,我遇到过这样的问题。在看完你的问题之后,我发现你有两个不同的挑战:

  1. ClientAPI
  2. 正确选择提供者
  3. 每个提供商所需的可变数量和参数类型。
  4. 当我设计服务或新功能时,我喜欢通过尝试最小化为了支持新功能而需要进行的更改次数来推理设计。在您的情况下,它将添加新的身份验证提供程序。现在至少有三种不同的实现方式。在我看来,没有完美的解决方案。你必须根据权衡选择其中一个。下面,我尝试提出几个选项来解决上面列出的这两个痛点以及它们的优点和缺点。

    类型放松

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

    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填写成功所需的所有信息{ {1}}致电。那么,让这个组件创建正确类型的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个子类型的爆炸,但现在这些子类型的可重用性已经增加。

    摘要

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

    我今天的灵感并不是很好,所以如果有其他事情发生,我会不断更新这篇文章。

答案 1 :(得分:0)

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

添加需要新参数的新提供程序将始终破坏API:

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

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

  1. 您的CreateAccount()函数将委托作为单个参数。
  2. 一旦CreateAccount()知道将使用哪个提供程序,将调用委托来收集所需的参数,并且只收集它们。
  3. 它可能更优雅一点,但是如果您添加新的提供程序并且客户端在代理人询问时您还没有准备好提供新参数时仍会遇到运行时问题...除非您的API已初始化使用客户端支持的提供程序列表。然后,您将添加新的提供程序,而您的客户端只有在准备好后才会启用它。