环绕执行代码简化
1 概述
在业务开发中,经常会碰到一些类似的业务逻辑,它们都要走相似的步骤,具体的入参出参和执行过程不同。因而在代码落地时,会有很多重复,譬如发送HTTP请求,读取EXCEL文件等等。
在java8推出来后,针对环绕执行模式(即资源处理时,常见的模式是打开一个资源,做一些处理,然后关闭资源。这些准备和清理资源阶段总是很类似,并且会围绕执行处理的那些重要代码。)重复代码的问题,可以通过泛型解决入参出参差异的问题,通过lambda表达式(函数式编程)传递方法解决执行过程不同的问题。这样环绕代码只需写一次。
2 简单示例1,筛选list集合中的优惠券
筛选金额大于100的优惠券,一般会有如下代码
public static List<Coupon> doCheck(List<Coupon> coupons){
if(CollectionUtils.isEmpty(coupons)){
return null;
}
List<Coupon> selected = new ArrayList<>();
for(Coupon coupon: coupons){
if(coupon.getMoney() > 100){
selected.add(coupon);
}
}
return selected;
}
如果另有需求,要调整发券金额筛选条件,或者以发券时间为筛选条件,或者是金额和时间共同作为筛选条件等等,该怎么办呢?
A为每个不同逻辑都去重复写for循环,此办法势必会带来大量的重复代码,而且可读性不强
B将各种筛选条件作为方法的参数传入,此时则需要区分不同的筛选模式和筛选参数,会使得整个筛选逻辑特别复杂,可读性更差,后续新增或修改筛选条件很难维护。
能不能将整个筛选逻辑抽象出来呢?这里引入java.util.function中的Predicate,它接收一个泛型对象参数,返回boolean,用它接收具体的筛选逻辑。
Predicate<Coupon> predicate = new Predicate<Coupon>(){
@Override
public boolean test(Coupon coupon) {
if(coupon.getMoney() > 100){
return true;
}
return false;
}
};
因为Predicate是函数式接口(一般指只定义了一个抽象方法的接口),在java8中,可以简写为
Predicate<Coupon> predicate = (Coupon coupon)->coupon.getMoney() > 100;
然后将环绕代码调整为
public static List<Coupon> doCheck(List<Coupon> coupons, Predicate<coupon> checkIn){
if(CollectionUtils.isEmpty(coupons)){
return null;
}
List<Coupon> selected = new ArrayList<>();
for(Coupon coupon: coupons){
if(checkIn.test(coupon)){
selected.add(coupon);
}
}
return selected;
}
将整个筛选框架定下来后,后续每次调用筛选逻辑,都只需要调整具体的筛选条件比较部分代码了,譬如
doCheck(coupons, (coupon)->coupon.getMoney>100);
doCheck(coupons, (coupon)->coupon.getMoney>100 && coupon.getDeadline<’2019-09-09’);
当然对list等数据的筛选,可以使用stream处理。这里举例只是为了说明问题。
3 简单示例2,写数据到excel
先定义函数接口
package com.excel;
import java.util.List;
import org.apache.poi.ss.usermodel.Sheet;
@FunctionalInterface
public interface WriteSheetContent<T> {
void wirte(Sheet sheet, List<T> data);
}
再定义执行写excel的调用方法
package com.excel;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.List;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.util.CollectionUtils;
public class WriteExcel {
public static <T> File write(List<T> datas, String filePath, String sheetName, WriteSheetContent<T> consume){
if(CollectionUtils.isEmpty(datas)){
return null;
}
File file = null;
OutputStream os= null;
Workbook workbook = null;
try {
file = new File(filePath);
if(! file.getParentFile().exists()){
boolean mkdir = file.getParentFile().mkdirs();
if (!mkdir) {
throw new Exception("mkdir root path error");
}
}
workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet(sheetName);
consume.wirte(sheet, datas);
os = new FileOutputStream(file);
workbook.write(os);
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
if(os != null){
os.close();
}
if(workbook != null){
workbook.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return file;
}
}
最后就是具体的生成sheet的过程
package com.excel;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.ss.usermodel.Row;
import com.lambda.Coupon;
public class WriteExcelTest {
public static void main(String[] args){
String filePath = "E:\\tmp\\haha.xlsx";
List<Coupon> coupons = new ArrayList<>();
Coupon a = new Coupon();
a.setId(1);
a.setMoney(100);
a.setDesc("发券拉");
coupons.add(a);
WriteExcel.write(coupons, filePath, "优惠券", (sheet, datas)->{
int column = 0;
Row headRow = sheet.createRow(0);
headRow.createCell(column++).setCellValue("优惠券ID");
headRow.createCell(column++).setCellValue("金额");
headRow.createCell(column++).setCellValue("描述");
for(Coupon data: datas){
column = 0;
Row row = sheet.createRow(1);
row.createCell(column++).setCellValue(data.getId());
row.createCell(column++).setCellValue(data.getMoney());
row.createCell(column++).setCellValue(data.getDesc());
}
});
}
}
其中类WriteExcel中的方法是完全复用的,不会因为数据或excel样式的不同需要重新定义。
类似的有发送Http请求时,解析http返回数据可以通过lambda表达式做。
简单应用
java方法传递,是以类为载体的。lambda函数作为参数传递,通常会拿来和匿名类做比较,它们有如下几点不同:
A lambda表达式没有成员变量,只能处理无状态的逻辑
B 匿名类块中的变量有单独的作用域,可以和外部类重名;lambda能直接引用外部类中的变量(注意this的指向)
1 延迟计算
函数式接口可以将方法作为参数传递,使得能在具体使用时,进行结果计算,而不是提前计算好,传递结果变量。
譬如,在记录日志时,在开启debug模式时打印日志。
if(log.isDebugEnabled()){
if(resultInfo.getCode() == 0){
log.debug("请求XX接口成功:"+ GsonUtils.toString(resultInfo.getData()));
}
}
if(log.isDebugEnabled()){
if(CollectionUtils.isNotEmpty(coupons)){
StringBuilder sb = new StringBuilder();
for(Coupon coupon: coupons){
}
log.debug("使用优惠券:"+ sb.toString());
}
}
在抽象上述逻辑时(减少业务代码中大量的log.isDebugEnabled()判断),会有
public class LogHelper {
public static void debug(Logger log, String message){
if(!log.isDebugEnabled()){
return;
}
log.debug(message);
}
}
此时,在调用LogHelper.debug时,就需要提前将message计算好。那么进一步有如下
@FunctionalInterface
public interface MessageGenerate {
String generate();
}
-----------------------------
import org.slf4j.Logger;
public class LogHelper {
public static void debug(Logger log, MessageGenerate cxt){
if(!log.isDebugEnabled()){
return;
}
String message = cxt.generate();
log.debug(message);
}
}
-----------------------------
List<Coupon> coupons = new ArrayList();
LogHelper.debug(log, ()->{
if(CollectionUtils.isEmpty(coupons)){
return "";
}
StringBuilder sb = new StringBuilder();
for(Coupon coupon: coupons){
}
return sb.toString();
});
虽然在调用MessageGenerate.generate(无参数,引用外部变量特殊做法)时,组织了生成message的代码,但是它只在记录日志时才会执行。
2 使用外部变量简化代码
在使用数据连接时,譬如redis连接时,jedis中有很多方法,但参数类型和个数,返回结果等都不同。很难通过lambda统一参数和返回结果。一般会有如下代码:
public String set(String key, String value) {
ShardedJedis jRedis = null;
try {
jRedis = shardedJedisPool.getResource();
return jRedis.set(key, value);
} catch (Exception e) {
log.error("Redis set err , key = {}", key, e);
} finally {
if (null != jRedis) {
jRedis.close();
}
}
return null;
}
public String get(String key) {
ShardedJedis jedis = null;
try {
jedis = shardedJedisPool.getResource();
return jedis.get(key);
} catch (Exception e) {
log.error("Redis get err , key = {}", key, e);
} finally {
if (null != jedis) {
jedis.close();
}
}
return null;
}
这里可以不考虑封装jedis的方法,把方法的执行交给jedis对象。
public <R> R handle(Function<ShardedJedis, R> function){
ShardedJedis jedis = null;
try {
jedis = shardedJedisPool.getResource();
return function.apply(jedis);
} catch (Exception e) {
log.error("Redis handle err", e);
} finally {
if (null != jedis) {
jedis.close();
}
}
return null;
}
String redisKey = "key", redisValue="value";
String s = redisUtils.handle((jedis)->jedis.set(redisKey, redisValue));
3 设计模式
在遵循“开放闭合”原则下,采用各种模式组织代码,譬如工厂模式,策略模式,模板模式等,都会将差异化部分封装在新类(其中很多都是接口或抽象类的子类)或新方法中。此时和容易想到将这些类或方法用lambda函数替代。
譬如在工厂模式中就不需要定义很多接口的子类了。当然比较复杂逻辑,为了保证代码的可读性和可维护性,还是需要针对具体情况做具体拆分。
//定义接口
public interface RefundStrategy{
boolean refund(Order order);
}
//调用
RefundStrategy alipayRefund = order->{ //do refund };
alipayRefund.refund(order);