【问题标题】:What design/pattern to use for a Client application using multiple providers?使用多个提供程序的客户端应用程序使用什么设计/模式?
【发布时间】:2015-09-14 20:57:34
【问题描述】:

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

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

假设我们有 ProviderA、ProviderB 和 ProviderC。

ProviderA 有一个 CreateAccount 方法签名/实现,它只需要(Firstname, Lastname)并使用 ProviderA 创建一个帐户。

ProviderB 有一个 CreateAccount 方法签名/实现,需要(Firstname、Lastname、Email、DOB)并使用 ProviderB 创建一个帐户。

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

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

所以我有两个问题。

1) 为这个模型实现的最佳设计/模式是什么?还要记住,提供者的数量将会增加——我们将增加更多的提供者。

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

【问题讨论】:

    标签: design-patterns architecture adapter strategy-pattern


    【解决方案1】:

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

    1. ClientAPI 正确选择提供程序
    2. 每个提供程序所需的参数数量和类型不定。

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

    类型放松

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

    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 子类型仍然激增,但现在这些子类型的可重用性有所提高。

    总结

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

    我今天的灵感不是很好,所以如果我有其他想法,我会继续更新这篇文章。

    【讨论】:

      【解决方案2】:

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

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2018-05-31
        • 1970-01-01
        • 1970-01-01
        • 2011-07-22
        • 2023-04-02
        • 1970-01-01
        • 1970-01-01
        • 2013-12-31
        • 2018-05-05
        相关资源
        最近更新 更多