注:阅读本文需要具备的知识:
lambda
表达式。
痛点
先举个例子:
用户的登录账号有手机号码和邮箱等, 所以现在有 AccountTypeEnum
枚举,在注册过程的最后一步 -- 插入数据, 我们需要将注册账号 set 到对应的字段, 即: 手机 → setPhone(phone), 邮箱 → setEmail(email);
一般情况, 我们可以通过 switch(account) 的方式, 根据不同的账号类型把账号 set 到对应字段. 这样做没错, 但很麻烦, 后续的维护成本也很高, 比如:
- 所有跟账号类型有关的逻辑, 都会有一个 switch(account) 代码块;
- 以后再加一种 account type, 所有 switch(account) 代码块都必须修改;
解决方案
面向操作编程可以屏蔽不同类型的差异, 只要在业务逻辑开始之前, 根据 account type
路由到对应的 Operator
, 比如: phone → PhoneOperator, 接下来的所有与 account type
有关的操作, 只需要跟 PhoneOperator
打交道即可.
面向操作编程其实是本人根据上述的解决方案起的名字,不具权威性,若不喜可以忽略,这里主要是介绍一种解决思路,或有更好的欢迎在评论留言。
实现
Operator
public interface Operator<K> {
/**
* Operator的名称, 同一类型的Operator的路由器{@link OperatorRouter}能够根据该值路由到当前的Operator
* @return route key
*/
K getName();
}
该接口很简单,但确实最核心的一个,之后的所有扩展方法都会在该接口的实现类中定义。该只有一个方法getName()
,该方法的返回结果就是实现类的名称,但其实是用于路由器OperatorRouter
的路由。
OperatorRouter
public abstract class OperatorRouter<K, O extends Operator> {
/**
* 存放同一类型的{@link Operator}
*/
private Map<K, O> operatorMap = Collections.emptyMap();
/**
* 根据 route key 路由到目标{@link Operator}
* @param routeKey
* @return
*/
public O route(K routeKey) {
O o = operatorMap.get(routeKey);
if (o == null) {
handleBadRoute(routeKey);
}
return o;
}
/**
* 处理路由结果为空的情况. {@link #route(Object)}
* @param routeKey
*/
protected abstract void handleBadRoute(K routeKey);
/**
* 返回{@link Operator}的子类的{@link Class}
* @return {@link O#getClass()}
*/
protected abstract Class<O> getOperatorClass();
/**
* 在初始化{@link OperatorRouter}时, 会对所有 {@link Operator}进行校验, 确保初始化完成后的{@link OperatorRouter},
* 其管理的 {@link Operator} 都是可用的, 校验逻辑默认为直接放行, 当子类需要对其管理的 {@link Operator} 进行校验时, 可重写该方法.
*
* @param operator
*/
protected void checkOperator(O operator) {}
void setOperatorMap(Map<K, O> operatorMap) {
this.operatorMap = operatorMap;
}
}
OperatorRouter
为同一类型的Operator
实现类的路由器,其中主要有一个属性,和两个抽象方法。属性operatorMap
用于存放所有的Operator
实现类;第一个抽象方法handleBadRoute(K routeKey)
很好理解,就是处理路由结果为空的情况,可以看到带一个参数routeKey
,可以在处理的时候知道哪个路由出现问题,比如在日志打印的时候会用到;第二个方法Class<O> getOperatorClass()
下文会介绍。
到这里主要的类就介绍完了,这两个类一般会放在公共包中,然后在项目中实现或继承。但是还差一步,就是必须要在项目初始化的时候收集同类型的Operator
然后通过OperatorRouter#setOperatorMap
注入路由器中。如果项目使用的是Spring
系的框架,实现类都会交由容器进行管理,即注入容器成为一个Bean
,这里给出一种思路:OperatorAutoConfiguration
。
OperatorAutoConfiguration
@Configuration
public class OperatorAutoConfiguration {
@Autowired(required = false)
public void initOperatorRouter(Map<String, OperatorRouter> routerMap, ApplicationContext applicationContext) {
if (null != routerMap && false == routerMap.isEmpty()) {
routerMap.values().forEach(router -> {
Class<Operator> operatorClass = router.getOperatorClass();
Map<String, Operator> beans = applicationContext.getBeansOfType(operatorClass);
Map<Object, Operator> tmpMap = new HashMap(8);
beans.forEach((beanName, operator) -> {
router.checkOperator(operator);
tmpMap.put(operator.getName(), operator);
});
router.setOperatorMap(Collections.unmodifiableMap(tmpMap));
});
}
}
}
然后在公共包的resource
包下新建目录META-INF
,再新建文件spring.factories
,文件内容为(包路径需要替换为OperatorAutoConfiguration
的包路径):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
包路径.OperatorAutoConfiguration
注:spring.factories的作用,如果不知道的可以自行百度或Google,springboot的实现原理很大程度就是基于这个。
实战
定义AccountTypeEnum
public enum AccountTypeEnum {
/** 邮件 */
EMAIL("email", "邮箱"),
/** 手机 */
PHONE("phone", "手机"),
;
private String code;
private String name;
/**
* 允许返回空
*
* @param code code
* @return {@link AccountTypeEnum}
*/
public static AccountTypeEnum parseOfNullable(String code) {
if (code != null) {
for (AccountTypeEnum e : values()) {
if (e.code.equals(code)) {
return e;
}
}
}
return null;
}
/**
* 允许返回空
*
* @param code code
* @return name
*/
public static String getNameNullable(String code) {
AccountTypeEnum e = parseOfNullable(code);
if (e != null) {
return e.name;
}
return null;
}
// 省略getter、构造方法,如果使用lombok插件,可以使用注解@Getter、@AllArgsConstructor
}
定义用户类
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long userId;
/**
* 邮箱
*/
private String email;
/**
* 手机
*/
private String phone;
// 省略其他属性、getter/setter
}
定义AccountTypeOperator
public interface AccountTypeOperator extends Operator<AccountTypeEnum> {
/**
*
* @return
*/
BiConsumer<SysUser, String> getAccountSetter();
/**
*
* @return
*/
Function<SysUser, String> getAccountGetter();
/**
* 校验账号不存在. 若存在, 则抛异常
* @param sysUser
* @param account
*/
void checkAccountNotExists(SysUser sysUser, String account);
/**
* 校验账号存在. 若不存在, 则抛异常
* @param sysUser
* @param account
*/
void checkAccountExists(SysUser sysUser, String account);
default String getAccount(SysUser sysUser) {
return getAccountGetter().apply(sysUser);
}
default void setAccount(SysUser sysUser, String account) {
getAccountSetter().accept(sysUser, account);
}
}
EmailAccountTypeOperator
@Component
public class EmailAccountTypeOperator implements AccountTypeOperator {
@Override
public AccountTypeEnum getName() {
return AccountTypeEnum.EMAIL;
}
@Override
public BiConsumer<SysUser, String> getAccountSetter() {
return (SysUser::setEmail);
}
@Override
public Function<SysUser, String> getAccountGetter() {
return SysUser::getEmail;
}
@Override
public void checkAccountNotExists(SysUser sysUser, String account) {
// 校验邮箱是否已被使用. 若被使用, 则抛异常
}
@Override
public void checkAccountExists(SysUser sysUser, String account) {
// 校验邮箱是否已被使用. 若没找到对应记录, 则抛异常
}
}
PhoneAccountTypeOperator
@Component
public class PhoneAccountTypeOperator implements AccountTypeOperator {
@Override
public AccountTypeEnum getName() {
return AccountTypeEnum.PHONE;
}
@Override
public BiConsumer<SysUser, String> getAccountSetter() {
return (SysUser::setPhone);
}
@Override
public Function<SysUser, String> getAccountGetter() {
return SysUser::getPhone;
}
@Override
public void checkAccountNotExists(String account) {
// 校验手机号码是否已被使用. 若被使用, 则抛异常
}
@Override
public void checkAccountExists(String account) {
// 校验手机号码是否已被使用. 若没找到对应记录, 则抛异常
}
}
AccountTypeOperatorRouter
@Component
public class AccountTypeOperatorRouter extends OperatorRouter<AccountTypeEnum, AccountTypeOperator> {
@Override
protected void handleBadRoute(AccountTypeEnum routeKey) {
// 提前抛异常,避免空指针异常
}
@Override
public Class<AccountTypeOperator> getOperatorClass() {
return AccountTypeOperator.class;
}
}
继承OperatorRouter
并实现2个抽象方法,其中getOperatorClass()
的作用就是返回当前路由器管理的Operator
子类的Class
,这里为AccountTypeOperator
,方便从Spring
容器中获取所有AccountTypeOperator
实现类实例,然后放到OperatorRouter#operatorMap
中。
AccountUtil
public class AccountUtil {
/**
* 邮件
*/
public static final String EMAIL_PATTERN = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
private static final Pattern emailPattern = Pattern.compile(EMAIL_PATTERN);
/**
* 手机
*/
public static final String PHONE_PATTERN = "^1[3|4|5|7|8][0-9]{9}$";
private static final Pattern phonePattern = Pattern.compile(PHONE_PATTERN);
/**
* 检测账号类型
*
* @param account
* @return
*/
public static AccountTypeEnum detectAccountType(String account) {
if (StrUtil.isBlank(account)) {
return null;
}
if (checkEmail(account)) {
return AccountTypeEnum.EMAIL;
}
if (checkPhone(account)) {
return AccountTypeEnum.PHONE;
}
return null;
}
public static boolean checkEmail(String account) {
return emailPattern.matcher(account).matches();
}
public static boolean checkPhone(String account) {
return phonePattern.matcher(account).matches();
}
}
UserService
@Service
public class UserService {
@Autowired
private AccountTypeOperatorRouter accountTypeOperatorRouter;
// 省略其他依赖
/**
* 使用面向Operation编程实现
* @param request
* @return
*/
public SysUser register(RegisterRequest request) {
AccountTypeEnum accountType = AccountUtil.detectAccountType(request.getAccount());
AccountTypeOperator operator = accountTypeOperatorRouter.route(accountType);
// 检验邮箱或手机号码未被注册
operator.checkAccountNotExists(request.getAccount());
// 其他校验
SysUser newUser = new SysUser();
operator.getAccountSetter().accept(newUser, request.getAccount());
// 借助 java8 的接口默认方法进一步封装
// operator.setAccount(newUser, request.getAccount());
newUser.setPassword(request.getPassword());
// 插入数据, 进行注册
return newUser;
}
/**
* 使用switch实现
* @param request
* @return
*/
public SysUser registerThroughSwitch(RegisterRequest request) {
AccountTypeEnum accountType = AccountUtil.detectAccountType(request.getAccount());
// 检验邮箱或手机号码未被注册
switch (accountType) {
case EMAIL:
// 校验邮箱未被使用
break;
case PHONE:
// 校验手机号码未被使用
break;
default:
// do something
}
// 其他校验
SysUser newUser = new SysUser();
switch (accountType) {
case EMAIL:
newUser.setEmail(request.getAccount());
break;
case PHONE:
newUser.setPhone(request.getAccount());
break;
default:
// do something
}
newUser.setPassword(request.getPassword());
// 插入数据, 进行注册
return newUser;
}
@Data
public static class RegisterRequest {
/**
* 账号. 邮箱或手机号码
*/
private String account;
/**
* 密码
*/
private String password;
// 省略其他属性和getter/setter
}
// 省略其他方法
}
可以看出,如果使用switch
,代码十分臃肿,而且每一个switch
都有一个default
分支需要处理,而且以后如果再加一种账号类型,那么必须要改所有相关的switch
代码块,而这明显违背开闭原则 (OCP
)。相反,面向Operation编程,则很好的遵守了该原则,只需要再添加一个对应的AccountTypeOperator
实现类即可。其他的好处,各位可以自行体会。
对于面向Operation编程,这篇文章权当抛砖引玉,希望更多码友指教,欢迎在评论留下您独特的理解和看法。