这是一个与设计相关的问题。
假设我们有一个名为ClientAPI的公共API,其中包含一些web方法,如CreateAccount, GetAccount。根据客户的不同,我们使用许多不同的提供者来满足这些请求。假设我们有ProviderA ProviderB ProviderC。
ProviderA有一个CreateAccount的方法签名/实现,它只需要(Firstname, Lastname),并使用ProviderA创建一个帐户。
ProviderB有一个CreateAccount的方法签名/实现,它需要(Firstname, Lastname, Email, DOB)并使用ProviderB创建一个帐户。
ProviderC有一个CreateAccount的方法签名/实现,它需要(Nickname, CompanyKey, Email)并使用ProviderC创建一个帐户。
客户端不需要知道或关心它们是哪个提供者。当调用客户端API方法CreateAccount时,客户端API将计算出它需要调用哪些提供程序并调用该提供程序方法。
我这里有两个问题。
1)这个模型的最佳设计/模式是什么?同时要记住,供应商的数量将会增长——我们将增加更多的供应商。2)关于传递参数-目前的ClientAPI CreateAccount方法签名是一大行变量,如果一个新的提供者需要一个新的值,方法签名有另一个变量添加到它,这显然打破了旧的实现等。在方法签名中传递参数的数组/列表/字典并传入下面的提供程序是一个好的做法,还是有更好的方法?
这确实是一个有趣的问题。我在不同的项目中也遇到过类似的问题。看完你的问题,我注意到你有两个不同的挑战:
-
ClientAPI
对供应商的正确选择 - 每个提供程序所需参数的可变数量和类型。
当我在设计一项服务或新功能时,我喜欢通过尽量减少为了支持新功能而需要进行的更改来对设计进行推理。在您的示例中,可能是添加新的身份验证提供程序。我现在想到了至少三种不同的实现方法。在我看来,没有完美的解决方案。你必须在权衡的基础上做出选择。下面,我将尝试提出一些解决上述两个痛点的方法,以及它们的优缺点。
类型放松
无论我们做什么,无论我们使用多态性抽象复杂性有多好,总是有不同的类型或组件通过需要不同的信息集来区分它自己与它的简单性。这取决于你想在设计中投入多少努力来保持强类型,以及你的多态抽象有多不同,在添加新特性时需要更多的更改。下面是一个实现示例,它没有对用户提供的所有类型的信息强制执行类型。
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);
优势- 可以通过简单地向
AuthType
和AuthProviderFactory
添加新条目来支持新的提供者。 - 每个
AuthProvider
都确切地知道它需要什么来执行暴露的操作(createAccount()
等)。逻辑和复杂性被很好地封装。
缺点-
UserData
中的一些参数不是强类型的。一些需要额外参数的AuthProvider
将不得不查找它们,即metadata.get('email')
。
输入UserData
UserData
中的一些参数不是强类型的。一些需要额外参数的AuthProvider
将不得不查找它们,即metadata.get('email')
。UserData
我假设负责调用AuthProviderFactory
的组件已经知道它需要的提供者类型,因为它必须用成功调用createAccount()
所需的所有信息填充UserData
。那么,让这个组件创建正确的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
重新访问
AuthProviderFactory
使用instanceof
选择合适的AuthProvider
。UserData
亚型的爆炸和潜在的重复代码。UserData
重新访问我们可以尝试通过在之前的设计中重新引入enum 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
创建新子类型之外,新的提供者还需要在enum AuthType
中添加新条目。 - 我们仍然有大量的
UserData
亚型,但现在这些亚型的可重用性增加了。
<标题> 总结
AuthProviderFactory
添加新条目和为UserData
创建新子类型之外,新的提供者还需要在enum AuthType
中添加新条目。UserData
亚型,但现在这些亚型的可重用性增加了。我很确定这个问题还有其他的解决方案。正如我上面提到的,也没有完美的解决方案。您可能必须根据它们的权衡和您想要实现的目标来选择一个。
我今天没有很好的灵感,所以我会继续更新这篇文章,如果我想到了什么。
标题>给定您的描述,当客户端调用createaccount () API时,他还不知道将使用哪个提供者。因此,如果你想要一个直接的解决方案,你的CreateAccount() API必须要求它最终可能需要的所有信息。
添加需要新参数的新提供者总是会破坏API:
- 如果你给函数添加一个新参数,它将在编译时中断(这是检测问题的最简单方法)
- 如果你使用字典/映射,它会在运行时中断,因为你会错过所需的信息。
然而,如果你是在一个面向对象的上下文中,你可以使用回调/委托设计模式:
- 你的CreateAccount()函数将接受一个委托作为单个参数。
- 一旦CreateAccount()知道将使用哪个提供者,将调用委托来收集所需的参数,并且只收集它们。
它可能更优雅一点,但如果你添加了一个新的提供者,并且你的客户端在委托要求时没有准备好提供新的参数,你仍然会有运行时问题…除非你的API是用客户端支持的提供商列表初始化的。然后,您将添加新的提供程序,并且您的客户端只会在准备好后启用它。