函数式编程小试-记一次代码重构

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

背景

有个需求是在设备上绑定service和解除对service的绑定。

设计:

数据库设计是一张设备表,表上用一个列unbind_service来代表解除绑定的services。

那么我最开始写了如下代码:

private boolean bindService(String appkey, String deviceId, String service) {
    DeviceDO deviceDO = new DeviceDO();
    deviceDO.setAppkey(appkey);
    deviceDO.setDeviceId(deviceId);

    DeviceDO dbDeviceDO = this.findDevice(appkey, deviceId);
    if(dbDeviceDO==null){
        return this.insertDevice(deviceDO);
    }else {
        Set<String> services = unbindServiceToSet(dbDeviceDO.getUnbindService());
        services.remove(service);

        String toSaveService = mapUnbindService(services);

        dbDeviceDO.setUnbindService(toSaveService);
        int count =  sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
                SqlBeanUtil.getStatementPartList(
                        deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));

        return count>=1 ? true: false;
    }
}


public boolean unbindService(String appkey, String deviceId, String service){

    DeviceDO deviceDO = new DeviceDO();
    deviceDO.setAppkey(appkey);
    deviceDO.setDeviceId(deviceId);

    DeviceDO dbDeviceDO = this.findDevice(appkey, deviceId);
    if(dbDeviceDO==null){
        return this.insertDevice(deviceDO);
    }else {
        Set<String> services = unbindServiceToSet(dbDeviceDO.getUnbindService());
        services.add(service);

        String toSaveService = mapUnbindService(services);

        dbDeviceDO.setUnbindService(toSaveService);
        int count =  sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
                SqlBeanUtil.getStatementPartList(
                        deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));

        return count>=1 ? true: false;
    }

}

写完之后一看,发现unbindService和bindService方法都是先检查数据库中是否存在对应的设备,没有的话,就插入一条新记录。已经存在对应设备了,就更新unbindService,再update 设备。

所以可以合并一下,新的代码如下:

private boolean bindService(String appkey, String deviceId, String service) {
    return bindOrUnbindService(appkey, deviceId, service, false);
}

private boolean unbindService(String appkey, String deviceId, String service) {
    return bindOrUnbindService(appkey, deviceId, service, true);
}

private boolean bindOrUnbindService(String appkey, String deviceId, String service, boolean isUnbind) {
    DeviceDO deviceDO = new DeviceDO();
    deviceDO.setAppkey(appkey);
    deviceDO.setDeviceId(deviceId);

    DeviceDO dbDeviceDO = deviceSDK.findDevice(appkey, deviceId);
    if (dbDeviceDO == null) {
        if(isUnbind){
            deviceDO.setUnbindService(service);
        }
        return deviceSDK.insertDevice(deviceDO);
    } else {
        Set<String> services = unbindService(dbDeviceDO.getUnbindService());
        if (isUnbind) {
            services.add(service);
        } else {
            //it's bindding
            services.remove(service);
        }

        String toSaveService = mapUnbindService(services);

        dbDeviceDO.setUnbindService(toSaveService);
        int count = sqlExecutor.update(dbDeviceDO, Lists.newArrayList(ColumnConstants.GMT_CREATE),
                SqlBeanUtil.getStatementPartList(
                        deviceDO, ColumnConstants.APPKEY, ColumnConstants.DEVICE_ID));

        return count >= 1 ? true : false;
    }
}

好了,再看看bindOrUnbindService方法,多线程并发情况下,很有可能产生问题。那么最好给它加上事务或者全局锁。写上事务的话,sqlExecutor写起来会比较麻烦。所以我偷懒点,选择使用全局锁,使用redis做全局锁,redis 客户端 RedissonClient就有对锁的支持。于是上述代码又被修改成:

public boolean bindService(String appkey, String deviceId, String service){

    boolean success = false;

    if(redisClient!=null){
        String lockName = "bind_service_"+appkey+deviceId;
        RLock lock = redisClient.getLock(lockName);
        if(lock!=null){
            try {
                if(lock.tryLock(1000, TimeUnit.MILLISECONDS)){
                    success = bindServiceInternal(appkey, deviceId, service);
                    String key = getUnbindServiceCacheKey(appkey, deviceId);
                    redisClient.getBucket(key).expire(0, TimeUnit.MILLISECONDS);
                }
            } catch (InterruptedException e) {
                DeviceSDKLogger.ERROR_LOGGER.error("bindService lock failed", e);
            }finally {
                lock.unlock();
            }
        }
    }else {
        success = bindServiceInternal(appkey, deviceId, service);
    }

    return success;

}

public boolean unbindService(String appkey, String deviceId, String service){

    boolean success = false;

    if(redisClient!=null){
        String lockName = "bind_service_"+appkey+deviceId;
        RLock lock = redisClient.getLock(lockName);
        if(lock!=null){
            try {
                if(lock.tryLock(1000, TimeUnit.MILLISECONDS)){
                    success = unbindServiceInternal(appkey, deviceId, service);
                    String key = getUnbindServiceCacheKey(appkey, deviceId);
                    redisClient.getBucket(key).expire(0, TimeUnit.MILLISECONDS);
                }
            } catch (InterruptedException e) {
                DeviceSDKLogger.ERROR_LOGGER.error("unbindService lock failed", e);
            }finally {
                lock.unlock();
            }
        }
    }else {
        success = unbindServiceInternal(appkey, deviceId, service);
    }

    return success;

}

上述代码看上去95%是重复的,而且直接依赖了redis。重复代码java8之前就没辙了,在java8之后,我们可以使用函数式编程。在这里,我先把锁的逻辑抽象出来。

/**
* @author jingjing.zhijj
* @create 2018/6/20 下午4:00
*/
public interface LockSupport {
    /**
     * 执行一个函数,LockSupport实现保证全局排他
     * @param lockKey 全局锁的key
     * @param function 需要执行的函数
     * @param <R> 执行函数的返回类型
     * @return 待执行函数的执行结果
     */
    <R> R execFunction(String lockKey, FunctionNeedLock<R> function);
}

使用一个新的接口有好处,以后可以换锁的实现。

虽然java8在java.util.function默认提供了几十接口,但是没有我需要的,所以我重新定义一下。

/**
* @author jingjing.zhijj
* @create 2018/6/20 下午3:46
*/
public interface FunctionNeedLock<R> {
    /**
     * 一个无参的Function
     * @return
     */
    R apply();
}

这个接口中其实名字是否是apply无所谓,只要方法的入参和返回值类型一致就可以。

可能有人会有疑问,bindService方法有3个参数,为何FuncitonNeedLock接口没有参数?哦,这个可以使用闭包解决。新的代码如下:

@Override
public boolean bindService(String appkey, String deviceId, String service) {
    String lockName = "bind_service_" + appkey + deviceId;
    return lockSupport.execFunction(lockName, () -> {
                return bindServiceInternal(appkey, deviceId, service);
            }
    );
}

@Override
public boolean unbindService(String appkey, String deviceId, String service) {
    String lockName = "bind_service_" + appkey + deviceId;
    return lockSupport.execFunction(lockName, () -> {
                return unbindServiceInternal(appkey, deviceId, service);
            }
    );
}

锁的实现代码如下:

/**
* @author jingjing.zhijj
* @create 2018/6/20 下午4:00
*/
public class RedisLockSupport implements LockSupport {
    private RedissonClient redissonClient;
    @Override
    public <R> R execFunction(String lockKey, FunctionNeedLock<R> function) {
        R result = null;
        if (redissonClient != null) {
            RLock lock = redissonClient.getLock(lockKey);
            if (lock != null) {
                try {
                    if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
                        result = function.apply();
                    }
                } catch (InterruptedException e) {
                    DeviceSDKLogger.ERROR_LOGGER.error("failed execFunction", e);
                } finally {
                    lock.unlock();
                }
            }
        } else {
            result = function.apply();
        }

        return result;
    }

    public void setRedissonClient(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
}

再运行一次单元测试,发现跑不过了,

@Test
public void testBindServiceImpl(){
    String appkey = "myappkey";
    String deviceId = "xxxxxxxxxxxxxxx";


    boolean success = bindService.bindService(appkey, deviceId, "abc");
    Set<String> unbindServiceSet = bindService.getUnbindService(appkey, deviceId);
    Assert.assertTrue(!unbindServiceSet.contains("abc"));

    success = bindService.unbindService(appkey, deviceId, "abc");
    unbindServiceSet = bindService.getUnbindService(appkey, deviceId);
    Assert.assertTrue(unbindServiceSet.contains("abc"));
}

原来是没有处理缓存失效。。以下代码把失效逻辑补回来。

@Override
public boolean bindService(String appkey, String deviceId, String service) {
    String lockName = "bind_service_" + appkey + deviceId;
    boolean success =  lockSupport.execFunction(lockName, () -> {
                return bindServiceInternal(appkey, deviceId, service);
            }
    );
    if(success){
        cacheProvider.remove(getUnbindServiceCacheKey(appkey, deviceId));
    }

    return success;
}

@Override
public boolean unbindService(String appkey, String deviceId, String service) {
    String lockName = "bind_service_" + appkey + deviceId;
    boolean success = lockSupport.execFunction(lockName, () -> {
                return unbindServiceInternal(appkey, deviceId, service);
            }
    );
    if(success){
        cacheProvider.remove(getUnbindServiceCacheKey(appkey, deviceId));
    }
    return success;
}

结语:
函数式编程还是能大大减少重复逻辑吧。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
5月前
|
Java
编程中最难的就是命名?这几招教你快速上手(4)
编程中最难的就是命名?这几招教你快速上手
36 0
编程中最难的就是命名?这几招教你快速上手(4)
|
17天前
|
设计模式 传感器 数据处理
探索设计模式的魅力:为什么你应该了解装饰器模式-代码优化与重构的秘诀
装饰器模式是一种设计模式,它允许在运行时向对象添加额外的职责,而无需修改其代码。这种模式提供了一种动态扩展对象功能的方法,同时保持了对象的单一职责原则。本文介绍了装饰器模式的基本概念、原理、优势、适用场景、实现方法、最佳实践和注意事项。通过装饰器模式,可以将多个行为组合成一个更复杂的行为,而无需使用继承或大量的接口实现。装饰器模式适用于需要对一个对象进行一系列的增强处理的情况,而这些增强处理可以以一种松耦合的方式进行组合。通过使用装饰器模式,可以提高代码的可维护性、可扩展性和灵活性,使系统更加灵活和易于维护
43 1
探索设计模式的魅力:为什么你应该了解装饰器模式-代码优化与重构的秘诀
|
1月前
|
程序员
代码之禅:从技术细节到哲学思考
【2月更文挑战第16天】 在数字世界的庞大宇宙中,每行代码都如同星辰,独立而明亮。本文将探索编程不仅是技术实现的手段,更是一种深层的哲学思考。我们将透过编程语言的语法和结构的表象,挖掘背后蕴含的思维模式与创造力的火花。这不仅仅是对编程实践的一次反思,更是一次关于人与机器、逻辑与直觉、控制与自由之间辩证关系的深入探讨。
9 0
|
5月前
|
Java 程序员 编译器
编程中最难的就是命名?这几招教你快速上手(1)
编程中最难的就是命名?这几招教你快速上手(1)
35 0
编程中最难的就是命名?这几招教你快速上手(1)
|
5月前
|
关系型数据库
编程中最难的就是命名?这几招教你快速上手(3)
编程中最难的就是命名?这几招教你快速上手
27 0
|
12月前
|
Java 关系型数据库 程序员
编程中最难的就是命名?这几招教你快速上手
编程中最难的就是命名?这几招教你快速上手
596 1
|
设计模式 前端开发 JavaScript
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
|
存储 Java 数据库连接
写了这么久代码你了解Java面向对象的设计原则吗?(三)
写了这么久代码你了解Java面向对象的设计原则吗?
88 0
写了这么久代码你了解Java面向对象的设计原则吗?(三)
|
设计模式 XML JavaScript
写了这么久代码你了解Java面向对象的设计原则吗?(一)
写了这么久代码你了解Java面向对象的设计原则吗?
94 0
写了这么久代码你了解Java面向对象的设计原则吗?(一)
|
存储 XML Java
写了这么久代码你了解Java面向对象的设计原则吗?(二)
写了这么久代码你了解Java面向对象的设计原则吗
177 0
写了这么久代码你了解Java面向对象的设计原则吗?(二)

相关实验场景

更多