(一)让代码更优雅系列(函数式接口+泛型)

发布于:2024-05-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、往事随风

记得刚毕业开始写代码那会,经常看到别人或者自己的一些重复代码,毕竟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 类库造成严重的影响。权衡利弊,因此最终还是利用 接口作为 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吧~