代码优化之代码重复


背景

//可维护性是大型项目成熟度的一个重要指标,而提升可维护性非常重要的一个手段就是减少代码重复
1. 如果多处重复代码实现完全相同的功能,很容易修改一处忘记修改另一处,造成 Bug;
2. 有一些代码并不是完全重复,而是相似度很高,修改这些类似的代码容易改(复制粘贴)错,把原本有区别的地方改为了一样。

工厂模式+模板方法模式实现对修改关闭对扩展开放

目标需求

//开发一个购物车下单功能,针对不同用户进行不同处理
1. 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
2. VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
3. 内部用户可以免运费,无商品折扣。
//目标实现三种类型的购物车业务逻辑,把入参Map对象(key时商品ID,value是商品数量),转换为出参购物车类型cart。

初始代码实现

//普通用户购物车处理逻辑

//购物车
@Data
public class Cart {
    //商品清单
    private List items = new ArrayList<>();
    //总优惠
    private BigDecimal totalDiscount;
    //商品总价
    private BigDecimal totalItemPrice;
    //总运费
    private BigDecimal totalDeliveryPrice;
    //应付总价
    private BigDecimal payPrice;
}
//购物车中的商品
@Data
public class Item {
    //商品ID
    private long id;
    //商品数量
    private int quantity;
    //商品单价
    private BigDecimal price;
    //商品优惠
    private BigDecimal couponPrice;
    //商品运费
    private BigDecimal deliveryPrice;
}

//普通用户购物车处理
public class NormalUserCart {
    public Cart process(long userId, Map items) {
        Cart cart = new Cart();

        //把Map的购物车转换为Item列表
        List itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);

        //处理运费和商品优惠
        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        //计算商品总价
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算运费总价
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总优惠
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //应付总价=商品总价+运费总价-总优惠
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }
}

//VIP用户购物车逻辑:与普通购物车逻辑不同在于VIP用户能享受同类商品多买的折扣,执行额外处理多买折扣部分;
public class VipUserCart {


    public Cart process(long userId, Map items) {
        ...


        itemList.stream().forEach(item -> {
            //运费为商品总价的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
            //购买两件以上相同商品,第三件开始享受一定折扣
            if (item.getQuantity() > 2) {
                item.setCouponPrice(item.getPrice()
                        .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                       .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
            } else {
                item.setCouponPrice(BigDecimal.ZERO);
            }
        });


        ...
        return cart;
    }
}

//免运费、无折扣内部用户:只是处理商品折扣和运费时的逻辑差异
public class InternalUserCart {


    public Cart process(long userId, Map items) {
        ...

        itemList.stream().forEach(item -> {
            //免运费
            item.setDeliveryPrice(BigDecimal.ZERO);
            //无优惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        ...
        return cart;
    }
}
//通过代码量可以发现,三种购物车70%代码重复。
//因为不同类型用户计算运费和优惠的方式不同,
//但整个购物车初始化、统计总价、总运费、总优惠和支付价格的逻辑一样。
//代码重复本身不可怕,可怕的时漏改或改错,比如VIP用户购物车商品总价计算bug
//不应该是把所有 Item 的 price 加在一起,而是应该把所有 Item 的 price*quantity 加在一起。
//这时,可能会只修改 VIP 用户购物车的代码,而忽略了普通用户、内部用户的购物车中,重复的逻辑实现也有相同的 Bug。

//根据不同用户类型使用不同购物车,使用三个if实现不同类型用户调用不同购物车的process方法
@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
    //根据用户ID获得用户类型
    String userCategory = Db.getUserCategory(userId);
    //普通用户处理逻辑
    if (userCategory.equals("Normal")) {
        NormalUserCart normalUserCart = new NormalUserCart();
        return normalUserCart.process(userId, items);
    }
    //VIP用户处理逻辑
    if (userCategory.equals("Vip")) {
        VipUserCart vipUserCart = new VipUserCart();
        return vipUserCart.process(userId, items);
    }
    //内部用户处理逻辑
    if (userCategory.equals("Internal")) {
        InternalUserCart internalUserCart = new InternalUserCart();
        return internalUserCart.process(userId, items);
    }

    return null;
}
//这里若是后续添加更多用户类型和购物车类,将会重复的购物车逻辑和if逻辑

模板方法模式与工厂模式改造

//在父类中实现了购物车处理的流程模板,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。
//由于父类的逻辑不完整无法单独工作,因此需要定义为抽象类。
//AbstractCart 抽象类实现了购物车通用的逻辑,额外定义了两个抽象方法让子类去实现。
//其中,processCouponPrice 方法用于计算商品折扣,processDeliveryPrice 方法用于计算运费。
public abstract class AbstractCart {
    //处理购物车的大量重复逻辑在父类实现
    public Cart process(long userId, Map items) {

        Cart cart = new Cart();

        List itemList = new ArrayList<>();
        items.entrySet().stream().forEach(entry -> {
            Item item = new Item();
            item.setId(entry.getKey());
            item.setPrice(Db.getItemPrice(entry.getKey()));
            item.setQuantity(entry.getValue());
            itemList.add(item);
        });
        cart.setItems(itemList);
        //让子类处理每一个商品的优惠
        itemList.stream().forEach(item -> {
            processCouponPrice(userId, item);
            processDeliveryPrice(userId, item);
        });
        //计算商品总价
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总运费
cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算总折扣
cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //计算应付价格
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }

    //处理商品优惠的逻辑留给子类实现
    protected abstract void processCouponPrice(long userId, Item item);
    //处理配送费的逻辑留给子类实现
    protected abstract void processDeliveryPrice(long userId, Item item);
}

//普通用户的购物车 NormalUserCart,实现的是 0 优惠和 10% 运费的逻辑
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(item.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()))
                .multiply(new BigDecimal("0.1")));
    }
}

//VIP 用户的购物车 VipUserCart,直接继承了 NormalUserCart,只需要修改多买优惠策略
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

    @Override
    protected void processCouponPrice(long userId, Item item) {
        if (item.getQuantity() > 2) {
            item.setCouponPrice(item.getPrice()
                    .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
                    .multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
        } else {
            item.setCouponPrice(BigDecimal.ZERO);
        }
    }
}

//内部用户购物车 InternalUserCart 是最简单的,直接设置 0 运费和 0 折扣即可:
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {
    @Override
    protected void processCouponPrice(long userId, Item item) {
        item.setCouponPrice(BigDecimal.ZERO);
    }

    @Override
    protected void processDeliveryPrice(long userId, Item item) {
        item.setDeliveryPrice(BigDecimal.ZERO);
    }
}

//定义三个购物车子类,在@Service注解中对Bean进行命名。
//使用Spring IOC容器,通过Bean名称直接获取到AbstractCart,调用其process方法实现通用。
//工厂模式(只不过是借助Spring容器实现罢了)
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
    String userCategory = Db.getUserCategory(userId);
    AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
    return cart.process(userId, items);
}
//利用工厂模式 + 模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险。这就是设计模式中的开闭原则:对修改关闭,对扩展开放。

 注解+反射去除重复代码

目标需求

1. 按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串
2. 因为每一种参数都有固定长度,未达到长度时需要做填充处理:
  2.1 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左;
  2.2 数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右;
  2.3 货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行左填充。
3. 对所有参数做 MD5 操作作为签名(为了方便理解,Demo 中不涉及加盐处理)

 初始实现

// 创建用户和支付方法
public class BankService {

    //创建用户方法
    public static String createUser(String name, String identity, String mobile, int age) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        //字符串靠左,多余的地方填充_
        stringBuilder.append(String.format("%-10s", name).replace(' ', '_'));
        //字符串靠左,多余的地方填充_
        stringBuilder.append(String.format("%-18s", identity).replace(' ', '_'));
        //数字靠右,多余的地方用0填充
        stringBuilder.append(String.format("%05d", age));
        //字符串靠左,多余的地方用_填充
        stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_'));
        //最后加上MD5作为签名
        stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
        return Request.Post("http://localhost:45678/reflection/bank/createUser")
                .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
                .execute().returnContent().asString();
    }
    
    //支付方法
    public static String pay(long userId, BigDecimal amount) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        //数字靠右,多余的地方用0填充
        stringBuilder.append(String.format("%020d", userId));
        //金额向下舍入2位到分,以分为单位,作为数字靠右,多余的地方用0填充
        stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
        //最后加上MD5作为签名
        stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
        return Request.Post("http://localhost:45678/reflection/bank/pay")
                .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
                .execute().returnContent().asString();
    }
}
//三种标准数据类型的处理逻辑有重复,稍有不慎就会出现Bug;
//处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复;
//实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错;
//代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错概率极大。

注解+反射改造

// 接口逻辑与逻辑实现的剥离,定义接口参数
@Data
public class CreateUserAPI {
    private String name;
    private String identity;
    private String mobile;
    private int age;
}
//自定义注解为接口和所有参数增加一些元数据 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Inherited public @interface BankAPI { String desc() default ""; String url() default ""; }
//自定义注解@BankAPIField描述接口的每一个字段规范,包含参数的次序、类型和长度三个属性 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented @Inherited public @interface BankAPIField { int order() default -1; int length() default -1; String type() default ""; }
//使用注解,定义Create UserAPI类描述创建用户接口,增加@BankAPI注解补充接口URL和描述等元数据; //通过每个字段增加@BankAPIField注解来补充参数顺序、类型和长度等元数据: @BankAPI(url = "/bank/createUser", desc = "创建用户接口") @Data public class CreateUserAPI extends AbstractAPI { @BankAPIField(order = 1, type = "S", length = 10) private String name; @BankAPIField(order = 2, type = "S", length = 18) private String identity; @BankAPIField(order = 4, type = "S", length = 11) //注意这里的order需要按照API表格中的顺序 private String mobile; @BankAPIField(order = 3, type = "N", length = 5) private int age; }
//支付API @BankAPI(url = "/bank/pay", desc = "支付接口") @Data public class PayAPI extends AbstractAPI { @BankAPIField(order = 1, type = "N", length = 20) private long userId; @BankAPIField(order = 2, type = "M", length = 10) private BigDecimal amount; } //继承的AbstractAPI类是一个空实现,因为没有公共数据可以抽象到基类
//通过注解实现对API参数的描述,反射如何配合注解实现动态的接口参数组装? //1. 从类上获取BankAPI注解得到URL属性后续远程调用 //2. 使用stream快速实现获取类中所有带BankAPIField注解的字段,并把字段按照order属性排序,然后设置私有字段反射可访问; //3. 实现反射获取注解的值后,根据BankAPIField得到参数类型,按照三种标准进行格式化,将所有参数的格式化逻辑集中于一处; //4. 实现参数加签和请求调用 private static String remoteCall(AbstractAPI api) throws IOException { //从BankAPI注解获取请求地址 BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class); bankAPI.url(); StringBuilder stringBuilder = new StringBuilder(); Arrays.stream(api.getClass().getDeclaredFields()) //获得所有字段 .filter(field -> field.isAnnotationPresent(BankAPIField.class)) //查找标记了注解的字段 .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //根据注解中的order对字段排序 .peek(field -> field.setAccessible(true)) //设置可以访问私有字段 .forEach(field -> { //获得注解 BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class); Object value = ""; try { //反射获取字段值 value = field.get(api); } catch (IllegalAccessException e) { e.printStackTrace(); } //根据字段类型以正确的填充方式格式化字符串 switch (bankAPIField.type()) { case "S": { stringBuilder.append(String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_')); break; } case "N": { stringBuilder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0')); break; } case "M": { if (!(value instanceof BigDecimal)) throw new RuntimeException(String.format("{} 的 {} 必须是BigDecimal", api, field)); stringBuilder.append(String.format("%0" + bankAPIField.length() + "d", ((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue())); break; } default: break; } }); //签名逻辑 stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); String param = stringBuilder.toString(); long begin = System.currentTimeMillis(); //发请求 String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url()) .bodyString(param, ContentType.APPLICATION_JSON) .execute().returnContent().asString(); log.info("调用银行API {} url:{} 参数:{} 耗时:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin); return result; } //所有处理参数排序、填充、加签、请求调用核心逻辑汇聚在remoteCall方法;
//BankService中每个接口实现调用remoteCall即可,剩下的只是参数组装。 //创建用户方法 public static String createUser(String name, String identity, String mobile, int age) throws IOException { CreateUserAPI createUserAPI = new CreateUserAPI(); createUserAPI.setName(name); createUserAPI.setIdentity(identity); createUserAPI.setAge(age); createUserAPI.setMobile(mobile); return remoteCall(createUserAPI); } //支付方法 public static String pay(long userId, BigDecimal amount) throws IOException { PayAPI payAPI = new PayAPI(); payAPI.setUserId(userId); payAPI.setAmount(amount); return remoteCall(payAPI); } //总结:涉及类结构的通用处理可以按照这个模式减少重复代码 //反射实现在不知道类结构时按照固定的逻辑处理类的成员 //注解实现成员补充元数据的能力 //所以我们利用反射实现通用逻辑的时候,可以从外部获得更多关心的数据。

属性拷贝工具消除重复代码

//实体间的转换复制
//手动写实体间的赋值代码容易出错
//对于复杂的业务系统,实体几百正常
//如描述一个订单中的几十个属性,要把DTO转为DO,复制其中大部分字段
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
orderDO.setAcceptDate(orderDTO.getAcceptDate());
orderDO.setAddress(orderDTO.getAddress());
orderDO.setAddressId(orderDTO.getAddressId());
orderDO.setCancelable(orderDTO.isCancelable());
orderDO.setCommentable(orderDTO.isComplainable()); //属性错误
orderDO.setComplainable(orderDTO.isCommentable()); //属性错误
orderDO.setCancelable(orderDTO.isCancelable());
orderDO.setCouponAmount(orderDTO.getCouponAmount());
orderDO.setCouponId(orderDTO.getCouponId());
orderDO.setCreateDate(orderDTO.getCreateDate());
orderDO.setDirectCancelable(orderDTO.isDirectCancelable());
orderDO.setDeliverDate(orderDTO.getDeliverDate());
orderDO.setDeliverGroup(orderDTO.getDeliverGroup());
orderDO.setDeliverGroupOrderStatus(orderDTO.getDeliverGroupOrderStatus());
orderDO.setDeliverMethod(orderDTO.getDeliverMethod());
orderDO.setDeliverPrice(orderDTO.getDeliverPrice());
orderDO.setDeliveryManId(orderDTO.getDeliveryManId());
orderDO.setDeliveryManMobile(orderDO.getDeliveryManMobile()); //对象错误
orderDO.setDeliveryManName(orderDTO.getDeliveryManName());
orderDO.setDistance(orderDTO.getDistance());
orderDO.setExpectDate(orderDTO.getExpectDate());
orderDO.setFirstDeal(orderDTO.isFirstDeal());
orderDO.setHasPaid(orderDTO.isHasPaid());
orderDO.setHeadPic(orderDTO.getHeadPic());
orderDO.setLongitude(orderDTO.getLongitude());
orderDO.setLatitude(orderDTO.getLongitude()); //属性赋值错误
orderDO.setMerchantAddress(orderDTO.getMerchantAddress());
orderDO.setMerchantHeadPic(orderDTO.getMerchantHeadPic());
orderDO.setMerchantId(orderDTO.getMerchantId());
orderDO.setMerchantAddress(orderDTO.getMerchantAddress());
orderDO.setMerchantName(orderDTO.getMerchantName());
orderDO.setMerchantPhone(orderDTO.getMerchantPhone());
orderDO.setOrderNo(orderDTO.getOrderNo());
orderDO.setOutDate(orderDTO.getOutDate());
orderDO.setPayable(orderDTO.isPayable());
orderDO.setPaymentAmount(orderDTO.getPaymentAmount());
orderDO.setPaymentDate(orderDTO.getPaymentDate());
orderDO.setPaymentMethod(orderDTO.getPaymentMethod());
orderDO.setPaymentTimeLimit(orderDTO.getPaymentTimeLimit());
orderDO.setPhone(orderDTO.getPhone());
orderDO.setRefundable(orderDTO.isRefundable());
orderDO.setRemark(orderDTO.getRemark());
orderDO.setStatus(orderDTO.getStatus());
orderDO.setTotalQuantity(orderDTO.getTotalQuantity());
orderDO.setUpdateTime(orderDTO.getUpdateTime());
orderDO.setName(orderDTO.getName());
orderDO.setUid(orderDTO.getUid());
//需要赋值的属性个数;重复赋值;赋值错误等问题

//使用BeanUtils改造 把DTO赋值到DO
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;

//工具包hutool:https://hutool.cn/
//映射工具:MapStruct:基于 JSR 269 的 Java 注解处理器实现(你可以理解为,它是编译时的代码生成器),使用的是纯 Java 方法而不是反射进行属性赋值,并且做到了编译时类型安全
//IDEA中可以安装 插件MapStruct Support,实现映射配置自动完成、跳转到定义等功能。

相关