(一)让代码更优雅系列(函数式接口+泛型)
一、往事随风
记得刚毕业开始写代码那会,经常看到别人或者自己的一些重复代码,毕竟ctrl+c、ctrl+v大法还是能“提升”一点点工作效率的,但是经历了一年左右项目上的“洗礼”,就发现项目很多地方越来越臃肿,可维护性和可读性自然是不忍直视。
慢慢地,我发现原来这些重复代码在项目里有很多地方都是很“形似”的,所以为何不抽取成一个方法?然后在不同的业务场景直接调用就好了,从而实现自己理解的所谓“代码复用性”。
又经历了相当一段时间后,发现仅仅是将代码抽取出来就是代码复用吗?从表面上看,确实是的,但经过参考优秀的开源框架如春天(Spring)后发现其实这种理解是相当表象的,缺少“灵魂”和“思考”。
这个系列就是结合了本人粗浅的代码经历和阅读源码后的感悟,让大家可以在日常工作中丰富自己的“武器库”,以后工作中面对复杂场景时候可以多一份从容,拥有多一种优雅选择!
二、常见的业务场景
相信各位只要工作中用到过缓存的话,都会遇到过这么一种业务场景,这里就拿获取商品详情展示来说:
- 商品详情数据在缓存中存在的话,则直接返回结果
- 商品详情数据在缓存中不存在的话,则查询数据库,得到结果后放回缓存中
这样简单获取操作后,只要缓存没过期,就都会从缓存获取商品详情。
这种从缓存中获取结果,没有的话,数据库里获取结果,再设置回缓存中去 的逻辑,在项目里实在是太司空见惯了,而大部分人都会直接把这种获取逻辑嵌入到每一处业务场景里,这样我们在写代码的时候就不能专注于业务开发了,还要一直关注处理着缓存的逻辑。
获取商品详情伪代码:
@Resource
private RedissonClient redissonClient;
@Resource
private ProductService productService;
public ProductVO queryProductById(Long id) {
RBucket<Object> productFromCache = redissonClient.getBucket("id");
if (productFromCache != null) {
return (ProductVO) productFromCache.get();
}
ProductVO productVO = productService.queryById(id);
productFromCache.set(productVO, 1, TimeUnit.DAYS);
return productVO;
}
看见没有,这只是不掺杂任何其他逻辑的代码结构,如果稍微复杂点,其实整体的代码可读性就没那么高了,那么我们有没办法让缓存相关的代码与业务代码分离呢?(其实这个场景还可以通过spring提供的@Cacheable去解耦,但是很多开发者也没用到)
有,下面请让我娓娓道来。
三、函数式接口 + 泛型
3.1 函数式接口(Functional Interface)
其实函数式接口(Functional Interface)是Java8对一类特殊类型的接口的统称。这种接口只会议定义了唯一的抽象方法,所以一开始也称为SAM(Single Abstract Method)类型的接口。
那么怎么判断当前的接口是否为函数式接口呢?可以参考接口上是否定义注解:@FunctionalInterface
JDK 8之前已有的 JDK 中提供的支持函数式编程的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
相信用过JDK8的小伙伴们知道有个包:java.util.function
Predicate:传入一个参数,返回一个布尔结果, 方法为boolean test(T t)
Consumer:传入一个参数,无返回值,纯消费。 方法为void accept(T t)
Function:传入一个参数,返回一个结果,方法为R apply(T t)
Supplier:无参数传入,返回一个结果,方法为T get()
UnaryOperator:一元操作符, 继承Function,传入参数的类型和返回类型相同。
BinaryOperator:二元操作符, 传入的两个参数的类型和返回类型相同, 继承 BiFunction
举个栗子:
@FunctionalInterface
public interface ProductFactory {
String print(String message);
}
那么我可以这样定义这个实现类:
ProductFactory productFactory = message -> {
log.info("test");
return "my name";
};
那可能有同学会问,这种方式我们用匿名类lambda表达式不是也可以实现吗,为什么要还要用这个函数式接口呢,还得额外再定义接口?
Java 推出 @FunctionalInterface 注解的原因是在 Java Lambda 的实现中,开发组不想再为 Lambda 表达式单独定义一种特殊的 Structural 函数类型,称之为箭头类型(arrow type),依然想采用 Java 既有的类型系统(class, interface, method等)。
增加一个结构化的函数类型会增加函数类型的复杂性,破坏既有的 Java 类型,并对成千上万的 Java 类库造成严重的影响。权衡利弊,因此最终还是利用 SAM 接口作为 Lambda 表达式的目标类型。JDK 中已有的一些接口本身就是函数式接口,如 Runnable。JDK 8 中又增加了 java.util.function 包,提供了常用的函数式接口。
函数式接口代表的一种契约,一种对某个特定函数类型的契约。在它出现的地方,实际期望一个符合契约要求的函数。Lambda 表达式不能脱离上下文而存在,它必须要有一个明确的目标类型,而这个目标类型就是某个函数式接口。
简单的理解就是定义一个符合我们在接口里定义的目标,而不是随机发散,方便我们统一使用和管理。
3.2 泛型
泛型这个只要是真正做过项目的都知道,它主要是可以定义通用的“外壳”,每个不同的场景即“内在特殊”的,我们可以用泛型来定义。
比如项目里应用层常用到的对外暴露的模型:
@Data
public class ResultBean<T> implements Serializable {
private String code = ExceptionEnum.SUCCESS.getCode();
/**
* 消息内容
*/
private String msg = "success";
/**
* 数据内容
*/
private T data;
四、优雅转身
好了,关键时刻到了,基于第二点里面的业务场景,我们如何利用第三点去实现呢?
首先可以定义好缓存用到的相关实体(无非就是过期时间和实际存储的对象):
public class RedisBusinessConfig {
private Integer expiresIn;
private TimeUnit timeUnit;
private Object data;
然后定义一个函数式接口,用来规范最终返回的缓存实体对象:
@FunctionalInterface
public interface CacheFactory<F extends RedisBusinessConfig> {
F getCache();
}
然后就是核心工具类的编写:
/**
* 从缓存中获取结果,没有的话,获取结果,再设置回缓存中去
*
* @param cacheFactory
* @param redisKey
* @return
*/
public <T, F extends RedisBusinessConfig> T getFromRedis(CacheFactory<F> cacheFactory, String redisKey) {
Object cache = redissonClient.getBucket(redisKey).get();
if (cache != null) {
log.info("fetch from redis, key: {}, cache: {}", redisKey, GsonUtils.toJson(cache));
return (T) cache;
}
F result = cacheFactory.getCache();
if (result == null) {
return null;
}
redissonClient.getBucket(redisKey).set(result.getData(), result.getExpiresIn(), result.getTimeUnit());
return (T) result.getData();
}
最后看看项目里是怎么用的吧:
public CpBlackList getCpBlackListByType(String type) {
return redisUtils.getFromRedis(() -> {
CpBlackList cpBlackList = cpBlackListService.getByType(type);
if (cpBlackList != null) {
return new RedisBusinessConfig()
.setExpiresIn(7)
.setTimeUnit(TimeUnit.DAYS)
.setData(cpBlackList);
}
return null;
}, String.format(RedisConstants.BLACK_LIST, type));
}
显然,我们使用的时候,可以将缓存和业务代码剥离,明显会优雅一点。
五、感想
当我们在项目里发现很多类似的写法或者结构的时候,一定要多思考怎么抽象、复用和沉淀,如果一直只是意味地CC+CV,虽然能暂时性地提升一点效率,但是未来肯定不如多思考得到的多,毕竟还是那句话:想要写得少,就得想得多;想要想得少,那就老老实实地CC+CB吧~
转载自:https://juejin.cn/post/7363459456146243618