网站首页 > 博客文章 正文
软件工程师和码农最大的区别就是平时写代码时习惯问题,码农很喜欢写重复代码而软件工程师会利用各种技巧去干掉重复的冗余代码。
业务同学抱怨业务开发没有技术含量,用不到设计模式、Java 高级特性、OOP,平时写代码都在堆 CRUD,个人成长无从谈起。
其实,我认为不是这样的。设计模式、OOP 是前辈们在大型项目中积累下来的经验,通过这些方法论来改善大型项目的可维护性。反射、注解、泛型等高级特性在框架中大量使用的原因是,框架往往需要以同一套算法来应对不同的数据结构,而这些特性可以帮助减少重复代码,提升项目可维护性。
在我看来,可维护性是大型项目成熟度的一个重要指标,而提升可维护性非常重要的一个手段就是减少代码重复。那为什么这样说呢?
- 如果多处重复代码实现完全相同的功能,很容易修改一处忘记修改另一处,造成 Bug
- 有一些代码并不是完全重复,而是相似度很高,修改这些类似的代码容易改(复制粘贴)错,把原本有区别的地方改为了一样。
今天,我就从业务代码中最常见的三个需求展开,聊聊如何使用 Java 中的一些高级特性、设计模式,以及一些工具消除重复代码,才能既优雅又高端。通过今天的学习,也希望改变你对业务代码没有技术含量的看法。
# 1. 利用工厂模式 + 模板方法模式,消除 if…else 和重复代码
假设要开发一个购物车下单的功能,针对不同用户进行不同处理:
- 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
- VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
- 内部用户可以免运费,无商品折扣。
我们的目标是实现三种类型的购物车业务逻辑,把入参 Map 对象(Key 是商品 ID,Value 是商品数量),转换为出参购物车类型 Cart。
先实现针对普通用户的购物车处理逻辑:
//购物车
@Data
public class Cart {
//商品清单
private List<Item> 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<Long, Integer> items) {
Cart cart = new Cart();
//把Map的购物车转换为Item列表
List<Item> 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<Long, Integer> 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<Long, Integer> 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<Long, Integer> items) {
Cart cart = new Cart();
List<Item> 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);
}
}
抽象类和三个子类的实现关系图,如下所示:
是不是比三个独立的购物车程序简单了很多呢?接下来,我们再看看如何能避免三个 if 逻辑。
或许你已经注意到了,定义三个购物车子类时,我们在 @Service 注解中对 Bean 进行了命名。既然三个购物车都叫 XXXUserCart,那我们就可以把用户类型字符串拼接 UserCart 构成购物车 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);
}
试想, 之后如果有了新的用户类型、新的用户逻辑,是不是完全不用对代码做任何修改,只要新增一个 XXXUserCart 类继承 AbstractCart,实现特殊的优惠和运费处理逻辑就可以了?
这样一来,我们就利用工厂模式 + 模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险。这就是设计模式中的开闭原则:对修改关闭,对扩展开放。
# 2. 利用注解 + 反射消除重复代码
是不是有点兴奋了,业务代码居然也能 OOP 了。我们再看一个三方接口的调用案例,同样也是一个普通的业务逻辑。
假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON,而是需要我们把参数依次拼在一起构成一个大字符串。
- 按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。
- 因为每一种参数都有固定长度,未达到长度时需要做填充处理:
- 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左;数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右;货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行左填充。
- 对所有参数做 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;
- 处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复;
- 实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错;
- 代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错的概率极大。
那应该如何改造这段代码呢?没错,就是要用注解和反射!
使用注解和反射这两个武器,就可以针对银行请求的所有逻辑均使用一套代码实现,不会出现任何重复。
要实现接口逻辑和逻辑实现的剥离,首先需要以 POJO 类(只有属性没有任何业务逻辑的数据类)的方式定义所有的接口参数。比如,下面这个创建用户 API 的参数:
@Data
public class CreateUserAPI {
private String name;
private String identity;
private String mobile;
private int age;
}
有了接口参数定义,我们就能通过自定义注解为接口和所有参数增加一些元数据。如下所示,我们定义一个接口 API 的注解 BankAPI,包含接口 URL 地址和接口说明:
@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 "";
}
接下来,注解就可以发挥威力了。
如下所示,我们定义了 CreateUserAPI 类描述创建用户接口的信息,通过为接口增加 @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;
}
另一个 PayAPI 类也是类似的实现:
@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;
}
这 2 个类继承的 AbstractAPI 类是一个空实现,因为这个案例中的接口并没有公共数据可以抽象放到基类。
通过这 2 个类,我们可以在几秒钟内完成和 API 清单表格的核对。理论上,如果我们的核心翻译过程(也就是把注解和接口 API 序列化为请求需要的字符串的过程)没问题,只要注解和表格一致,API 请求的翻译就不会有任何问题。
以上,我们通过注解实现了对 API 参数的描述。接下来,我们再看看反射如何配合注解实现动态的接口参数组装:
- 第 3 行代码中,我们从类上获得了 BankAPI 注解,然后拿到其 URL 属性,后续进行远程调用。
- 第 6~9 行代码,使用 stream 快速实现了获取类中所有带 BankAPIField 注解的字段,并把字段按 order 属性排序,然后设置私有字段反射可访问。
- 第 12~38 行代码,实现了反射获取注解的值,然后根据 BankAPIField 拿到的参数类型,按照三种标准进行格式化,将所有参数的格式化逻辑集中在了这一处。
- 第 41~48 行代码,实现了参数加签和请求调用。
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);
}
其实,许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码。
反射给予了我们在不知晓类结构的时候,按照固定的逻辑处理类的成员;而注解给了我们为这些成员补充元数据的能力,使得我们利用反射实现通用逻辑的时候,可以从外部获得更多我们关心的数据。
# 3. 利用属性拷贝工具消除重复代码
最后,我们再来看一种业务代码中经常出现的代码逻辑,实体之间的转换复制。
对于三层架构的系统,考虑到层之间的解耦隔离以及每一层对数据的不同需求,通常每一层都会有自己的 POJO 作为数据实体。比如,数据访问层的实体一般叫作 DataObject 或 DO,业务逻辑层的实体一般叫作 Domain,表现层的实体一般叫作 Data Transfer Object 或 DTO。
这里我们需要注意的是,如果手动写这些实体之间的赋值代码,同样容易出错。
对于复杂的业务系统,实体有几十甚至几百个属性也很正常。就比如 ComplicatedOrderDTO 这个数据传输对象,描述的是一个订单中的几十个属性。如果我们要把这个 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()); //对象错误
如果不是代码中有注释,你能看出其中的诸多问题吗?
如果原始的 DTO 有 100 个字段,我们需要复制 90 个字段到 DO 中,保留 10 个不赋值,最后应该如何校验正确性呢?数数吗?即使数出有 90 行代码,也不一定正确,因为属性可能重复赋值。
有的时候字段命名相近,比如 complainable 和 commentable,容易搞反(第 7 和第 8 行),或者对两个目标字段重复赋值相同的来源字段(比如第 28 行)
明明要把 DTO 的值赋值到 DO 中,却在 set 的时候从 DO 自己取值(比如第 20 行),导致赋值无效。
这段代码并不是我随手写出来的,而是一个真实案例。有位同学就像代码中那样把经纬度赋值反了,因为落库的字段实在太多了。这个 Bug 很久都没发现,直到真正用到数据库中的经纬度做计算时,才发现一直以来都存错了。
修改方法很简单,可以使用类似 BeanUtils 这种 Mapping 工具来做 Bean 的转换,copyProperties 方法还允许我们提供需要忽略的属性:
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;
很多同学在工作一段时间之后可能都有这样的困境,大家觉得自己总是在写业务代码,技术上感觉好像没有多大的长进,不知不觉就成为了CURD Boy或者Girl,自己想要去改变但是又不知道该从何处进行入手。有的同学会去学习如何做架构、有的同学可能会去学习各种新技术还有的同学甚至转产品经理来试图解除困境。但是我觉得找到跨出这种困境的途经反而还是要从我们每天写的代码入手。即便当前每天做着CRUD的事情,但是我们自己不能把自己定义为只会CURD的工具人。
那么我们到底如何从代码层面入手改变困境呢?我们可以回过头看看自己以前写的代码,或者是当前正在实现的各种各样的需求,反问自己以下5个问题。
1、有没有使用设计模式优化代码结构?
2、有没有利用一些高级特性来简化代码实现?
3、有没有借助框架的能力来扩展应用能力?
4、自己设计的业务模型够不够抽象?
5、代码扩展性强不强,需求如果有变化模块代码能不能做到最小化修改?
通过这样的反问和思考,我们可以不断自我审视自己写的代码。通过在代码上的深耕细作,我们所负责的模块的质量就会比别人更高,出现Bug的概率就会更低,稳定性就会更高,那么未来负责更多业务模块的机会也就会更多。因此本文主要从优化日常工作中经常遇到的重复代码入手,和大家探讨下如何通过一些技巧来消除平台中的重复代码。
为什么要消除重复代码
在程序猿的日常工作中,不仅要跟随业务侧的发展不断开发新的需求,同时也需要维护老的已有平台。无论是开发新需求还是维护老系统,我们都会遇到同样一个问题,系统中总是充斥着很多重复的代码。可能是由于工期很赶没时间优化,也有可能是历史原因欠下的技术债。无论是什么原因,系统中大量的重复代码非常影响平台整体的可维护性。大神们的谆谆教导Don’t Repeat Yourself 言犹在耳。那么平台中的重复代码会带来怎样的稳定性风险呢?
系统维护成本高
如果项目中出现大量的重复代码,说明系统中这部分业务逻辑并没有进行很好的抽象,因此会导致后期的代码维护面临很多问题。无论是修改原有逻辑还是新增业务逻辑可能需要在不同的文件中进行修改,项目维护成本相当高。另外后期维护的同学看到同样的逻辑写了多遍,不明白这到底是代码的坏味道还是有什么特殊的业务考虑,这也在无形中增加了后期维护者的代码逻辑理解难度。
程序Bug概率高
大家都知道重复代码意味着业务逻辑相同或者相似,假如这些相同或者相似的代码出现了Bug,在修复的过程中就需要修改很多地方,导致一次上线变更的内容比较多,存在一定的风险,毕竟线上问题70%-80%都是由于新的变更引起的。另外如果重复的地方比较多,很有可能出现漏改的情况。因此重复的代码实际就是隐藏在工程中的老炸弹,可能一直相安无事,也可能不知道什么时候就会Bom一声给你惊喜,因此我们必须要进行重复代码消除。
如何优雅的消除重复代码
在消除重复代码之前,我们首先需要确定到底什么是重复代码,或者说重复代码的特征到底是什么。有的同学可能会说,这还不简单嘛,重复代码不就是那些一模一样的但是散落在工程不同地方的代码嘛。当然这句话也没错,但是不够全面,重复代码不仅仅指那些不同文件中的完全相同的代码,还有一些代码业务流程相似但是并不是完全相同,这类代码我们也把它称之为重复代码。
所以总结了重复代码的几个特性:
1、代码结构完全相同,比如工程中好几个地方都有读取配置文件的逻辑,代码都是相同的,那么我们可以把不同地方读取配置文件的逻辑放到一个工具类中,这样今后再有读取配置文件的需要的时候可以直接调用工具类中方法即可,不需要再重复写相同的代码,这也是我们日常工作中最常见的使用方式。
2、代码逻辑结构相似,在项目中经常遇到虽然代码并不是完全相同,但是逻辑结构却非常相似。比如电商平台在进行营销活动的时候,常常通过邀请的方式来进行用户红包领取的活动,但是对于新老用户的红包赠予规则是不同的,同时也会根据邀请用户的数量的不同给予不同的红包优惠。但是无论新老用户都会经历根据用户类型获取红包计算规则,根据规则计算减免的红包,最后付款的时候减去红包数额这样一个业务逻辑。虽然表面看上去代码并不相同,但是实际上逻辑基本是一样的,因此也属于重复代码。
下面就和大家分享几种比较实用的消除重复的代码的技巧:
统一参数校验
当我们进行项目开发的时候,会编写一些类的实现方法,不可避免的会进行一些参数校验或者业务规则校验,因此会在实现方法中写一些判断参数是否有效或者返回结果是否有效的的的代码。
java复制代码public OrderDTO queryOrderById(String id) {
if(StringUtils.isEmpty(id)) {
return null;
}
OrderDTO order = orderBizService.queryOrder(id);
if(Objects.isNull(Order)) {
return null;
}
...
}
public List<UserDTO> queryUsersByType(List<String> type) {
if(StringUtils.isEmpty(id)) {
return null;
}
...
}
这种参数校验的方式,很多人会喜欢使用@Valid这种注解来进行参数有效性的判断,但是我觉得还是不够方便,它只能进行一些参数的校验,并不能进行业务结果的有效性判断。那么对于这种校验类的代码如何才能消除重复if...else...判断代码呢?因此我一般会统一定义一个Assert断言来进行参数或者业务结果的校验,当然也可以使用Spring框架提供的Assert抽象类来进行判断,但是它抛出的异常是IllegalArgumentException,我习惯抛出自己定义的全局统一的异常信息,这样可以通过全局的异常处理类来进行统一处理。因此我们首先定义一个业务断言类,主要针对biz层出现的参数以及业务结果进行断言,这样可以避免重复写if...else...判断代码。
java复制代码public class Assert {
public static void notEmpty(String param) {
if(StringUtils.isEmpty(param)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "param is empty or null");
}
}
public static void notNull(Object o) {
if (Objects.isNull(o)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "object is null");
}
}
public static void notEmpty(Collection collection) {
if(CollectionUtils.isEmpty(collection)) {
throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "collection is empty or null");
}
}
}
优化后的代码如下所示,是不是看上去更加清爽了。
java复制代码public OrderDTO queryOrderById(String id) {
Assert.notEmpty(id);
OrderDTO order = orderBizService.queryOrder(id);
Assert.notNull(order);
...
}
public List<UserDTO> queryUsersByType(List<String> type) {
Assert.notEmpty(type);
...
}
统一异常处理
以下这类Controller代码在项目中是不是很常见?大家可以翻翻自己的项目工程代码,可能很多工程中Cotroller层都充斥着这样的try{}catch{}逻辑处理,相当于每个接口实现都要进行异常处理,看起来非常冗余写起来也麻烦。实际上我们可以通过定义统一的全局异常处理器来进行优化,避免重复的进行异常捕获。
java复制代码@GetMapping("list")
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String userId) {
try {
OrderVO orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderDTO);
} catch (BizException be) {
// 捕捉业务异常
return ResponseResultBuilder.buildErrorResponse(be.getCode, be.getMessage());
} catch (Exception e) {
// 捕捉兜底异常
return ResponseResultBuilder.buildErrorResponse(e.getMessage());
}
}
那么我们应该怎么优化这些重复的异常捕捉处理代码呢?首先我们需要定义一个统一的异常处理器,通过它来对Controller接口的异常进行统一的异常处理,包括异常捕获以及异常信息提示等等。这样就不用再每个实现接口中编写try{}catch{}异常处理逻辑了。示意代码只是简单的说明实现方法,在项目中进行落地的时候,大家可以定义处理更多的异常类型。
java复制代码@ControllerAdvice
@ResponseBody
public class UnifiedException {
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(BizException.class)
@ResponseBody
public ResponseResult handlerBizException(BizException bizexception) {
return ResponseResultBuilder.buildErrorResponseResult(bizexception.getCode(), bizexception.getMessage());
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult handlerException(Exception ex) {
return ResponseResultBuilder.buildErrorResponseResult(ex.getMessage());
}
}
优化后的Controller如下所示,大量的try...catch...不见了,代码结构变得更加清晰直接。
java复制代码@GetMapping("list")
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String userId) {
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
优雅的属性拷贝
在实际的项目开发中我们锁开发的微服务都是分层的有的是MVC三层,有的按照DDD领域分层是四层。无论是三层还是四层都会涉及不同层级的之间的调用,而每个层级都有自己的数据对象模型,比如biz层是dto,domain层是model,repo层是po。因此必然会涉及到数据模型对象之间的相关转换。在一些场景下模型之间的字段很多都是一样的,有的甚至是完全一模一样。比如将DTO转化为业务模型Model,实际上他们之间很多的字段都是一样的,所以经常会出现以下的这种代码,会出现大量的属性赋值 的操作来达到模型转换的需求。实际上我们可以通过一些工具包或者工具类进行属性的拷贝,避免出现大量的重复赋值代码。
java复制代码public class TaskConverter {
public static TaskDTO taskModel2DTO(TaskModel taskModel) {
TaskDTO taskDTO = new TaskDTO();
taskDTO.setId(taskModel.getId());
taskDTO.setName(taskModel.getName());
taskDTO.setType(taskModel.getType());
taskDTO.setContent(taskModel.getContent());
taskDTO.setStartTime(taskModel.getStartTime());
taskDTO.setEndTime(taskModel.getEndTime());
return taskDTO;
}
}
使用BeanUtils的进行属性赋值,很明显不再有那又长又没有感情的一条又一条的属性赋值语句了,整个任务数据模型对象的转换代码看上去立马舒服很多。
java复制代码public class TaskConverter {
public static TaskDTO taskModel2DTO(TaskModel taskModel) {
TaskDTO taskDTO = new TaskDTO();
BeanUtils.copyProperties(taskModel, taskDTO);
return taskDTO;
}
}
当然很多人会说,BeanUtils会存在深拷贝的问题。但是在一些浅拷贝的场景下使用起来还是比较方便的。另外还有Mapstruct工具,大家也可以试用一下。
核心能力抽象
假设有这样的业务场景,系统中需要根据不同的用户类型计算商品结算金额,大致的计算逻辑由三个步骤,分别是计算用户商品总价格,计算不同用户对应的优惠金额,最后计算出用户的结算金额。我们先来看下原有系统中的实现方式。
普通用户结算逻辑:
java复制代码public Class NormalUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.1));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}
VIP用户结算逻辑:
java复制代码public Class VIPUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.2));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}
黑卡用户结算逻辑:
java复制代码public Class BlackCardUserSettlement {
//省略代码
...
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.3));
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}
在这样的场景下,我们可以发现,在三个类中计算商品总额以及计算最后的应付金额逻辑都是一样的,唯一不同的是每个用户类型对应的优惠金额是不同的。因此我们可以把逻辑相同的部分抽象到AbstractSettleMent中,然后定义计算优惠金额的抽象方法由各个不同的用类型子类去实现。这样各个子类只要关心自己的优惠实现就可以了,重复的代码都被抽象复用大大减少重复代码的使用。
java复制代码public Class AbstractSettlement {
//省略代码
...
public abstact Bigdecimal calculateDiscount();
public Bigdecimal calculate(String userId) {
//计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);
Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);
//计算优惠
Bigdecimal discount = calculateDiscount();
//计算应付金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
//省略代码
...
}
自定义注解和AOP
用过Spring框架的同学都知道,AOP是Spring框架核心特性之一,它不仅是一种编程思想更是实际项目中可以落地的技术实现技巧。通过自定义注解和AOP的组合使用,可以实现一些通用能力的抽象。比如很多接口都需要进行鉴权、日志记录或者执行时间统计等操作,但是如果在每个接口中都编写鉴权或者日志记录的代码那就很容易产生很多重复代码,在项目后期不好维护。针对这种场景 我们可以使用AOP同时结合自定义注解实现接口的切面编程,在需要进行通用逻辑处理的接口或者类中增加对应的注解即可。
假设有这样的业务场景,需要计算指定某些接口的耗时情况,一般的做法是在每个接口中都加上计算接口耗时的逻辑,这样各个接口中就会有这样重复计算耗时的逻辑,重复代码就这样产生了。
那么通过自定义注解和AOP的方式可以轻松的解决代码重复的问题。首先定义一个注解,用于需要统计接口耗时的接口方法上。
java复制代码@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCost {
}
定义切面实现类:
java复制代码
@Aspect
@Component
public class CostTimeAspect {
@Pointcut(value = "@annotation(com.mufeng.eshop.anotation.CostTime)")
public void costTime(){ }
@Around("runTime()")
public Object costTimeAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
try {
long beginTime = System.currentTimeMillis();
obj = joinPoint.proceed();
//获取方法名称
String method = joinPoint.getSignature().getName();
//获取类名称
String class = joinPoint.getSignature().getDeclaringTypeName();
//计算耗时
long cost = System.currentTimeMillis() - beginTime;
log.info("类:[{}],方法:[{}] 接口耗时:[{}]", class, method, cost + "毫秒");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
}
优化前的代码:
java复制代码
@GetMapping("/list")
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {
long beginTime = System.currentTimeMillis();
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
log.info("getOrderList耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
@GetMapping("/item")
public ResponseResult<OrderVO> getOrderById(@RequestParam("id")String orderId) {
long beginTime = System.currentTimeMillis();
OrderVO orderVo = orderBizService.queryOrderById(orderId);
log.info("getOrderById耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
优化后的代码:
java复制代码
@GetMapping("/list")
@TimeCost
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {
List<OrderVO> orderVo = orderBizService.queryOrder(userId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
@GetMapping("/item")
@TimeCost
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String orderId) {
OrderVO orderVo = orderBizService.queryOrderById(orderId);
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}
引入规则引擎
大家在做业务开发的时候,可能会遇到这样的场景,业务中充斥着各种各样的规则判断,同时这些业务规则还可能经常发生变化。即便是我们用了策略模式等设计模式来优化代码结构,但是还是不能避免代码中出现大量的if...else...判断代码,一旦增加或者修改规则都需要在原来的业务规则代码中进行修改,维护起来非常不方便。
假设设有这样的业务,销售人员的奖励根据实际的利润进行计算,不同的利润计算奖励的规则并不相同。使用规则引擎之前,可能够会有这样的代码结构,需要根据实际利润所处的区间来计算最终的奖励金额,不同区间范围对应的返点规则是不一样的,因此会有很多的if...else...判断。另外规则有可能随着业务的发展还会经常变化,因此后期可能面临不断修改这部分的计算奖励的代码的情况。
java复制代码public double calculate(int profit) {
if(profit < 1000) {
return profit * 0.1;
} else if(1000 < profit && profit< 2000) {
return profit * 0.15;
} else if(2000 < profit && profit < 3000) {
return profit * 0.2;
}
return profit * 0.3;
}
如果遇到这种业务场景,我们就可以考虑使用规则引擎。通过引入规则引擎,我们可以实现业务代码与业务规则相分离,将各种业务判断规则从原有的平台代码中抽离出来,以后规则的修改都在规则文件中直接修改接可以了,避免代码本身的变更,从而大大提升代码的扩展性。这里简单介绍下常用的规则引擎Drools是如何实现规则扩展管理的。
使用Drools之后:
使用规则引擎优化之后,所有的规则也就是所有的if...else...都会放在规则文件reward.drl中,因此代码中不会再有各种重复的if...else...代码,真正实现了业务规则与业务数据相分离。
java复制代码// 奖励规则
package reward.rule
import com.mufeng.eshop.biz.Reward
// rule1:如果利润小于1000,则奖励计算规则为profit*0.1
rule "reward_rule_1"
when
$reward: Reward(profit < 1000)
then
$reward.setReward($reward.getProfit() * 0.1);
System.out.println("匹配规则1,奖励为利润的1成");
end
// rule2:如果利润大于1000小于2000,则奖励计算规则为profit*0.15
rule "reward_rule_2"
when
$reward: Reward(profit >= 1000 && profit < 2000)
then
$reward.setReward($reward.getProfit() * 0.15);
System.out.println("匹配规则2,奖励为利润的1.5成");
end
// rule3:如果利润大于2000小于3000,则奖励计算规则为profit*0.2
rule "reward_rule_3"
when
$order: Order(profit >= 2000 && profit < 3000)
then
$reward.setReward($reward.getProfit() * 0.2);
System.out.println("匹配规则3,奖励为利润的2成");
end
// rule4:如果利润大于等于3000,则奖励计算规则为profit*0.3
rule "reward_rule_4"
when
$order: Order(profit >= 3000)
then
$reward.setReward($reward.getProfit() * 0.3);
System.out.println("匹配规则4,奖励为利润的3成");
end
在代码中只要将待判断的数据插入到规则引擎的工作内存中,然后执行规则就可以获取到最终的结果,是不是很方便的实现业务规则的解耦,在实际的Java代码中也不用看到各种if...else...判断。
定义规则引擎实现:
java复制代码public class DroolsEngine {
private KieHelper kieHelper;
public DroolsEngine() {
this.kieHelper = new KieHelper();
}
public void executeRule(String rule, Object unit, boolean clear) {
kieHelper.addContent(rule, ResourceType.DRL);
KieSession kieSession = kieHelper.getKieContainer().newKieSession();
//插入判断实体
kieSession.insert(unit);
//执行规则
kieSession.fireAllRules();
if (clear) {
kieSession.dispose();
}
}
}
java复制代码public class Profit {
public double calculateReward(Reward reward) {
String rule = "classpath:rules/reward.drl";
File rewardFile = new File(rule);
String rewardDrl = FileUtils.readFile(rewardFile, "utf-8");
DroolsEngine engine = new DroolsEngine();
engine.executeRule(rewardDrl, reward, true);
return reward.getReward();
}
}
通过引入Drools规则引擎,代码中不再有各种规则判断的重复的if...else...判断语句,而且如果后期要修改奖励规则,代码不用修改,直接更改规则即可,系统的扩展性以及可维护性进一步提升。
消除重复代码方法论
上文中给大家介绍了几种消除重复代码的实战小技巧,不知道大家有没有发现虽然具体落地实操的手段各不相同,无论是提取公用逻辑作为工具类、使用AOP进行面向切面编程还是进行通用逻辑抽象,又或者是借助规则引擎分离实现与规则。实际它们的核心思想本质上都是一致的,都是通过抽离或者抽象相似代码逻辑后进行统一处理。将这种核心思想放在微服务内部就是在系统中的消除重复业务逻辑,如果放在架构层面来看其实和中台思想的本质也是相通的,将用户、支付这种各个平台都会用到的服务抽象为中台,实际就是一种混乱到有序的软件复杂度治理过程以及一种归一的思想。
那么在日常的实际项目中我们应该怎么落地实践消除重复代码呢?这里总结了通过上述文章对于重复代码的处理,我们来试图来提炼消除重复代码的方法论。
Find: 技术同学需要有一双可以发现重复代码的眼睛,能够将表面上的重复我代码以及隐藏的重复代码识别出来。重复代码不仅仅是表示长得一模一样的代码,那些核心业务逻辑一样实际也是一种重复代码。
Analysis: 当我们找到了重复代码之后,就要考虑该如何进行优化了,如果只是工具类型的重复代码,那么直接提取作为一个工具类就可以了,也不用考虑太多。但是如果是涉及业务流程可能需要进一步的进行抽象,
Action: 根据不同的重复代码的类型,我们需要制定不通过的优化重复代码的方案。根据不同的方案实现通过引入规则引擎还是模板方法进行抽象。
总结
不知不觉又到凌晨12点了,每次在这种夜深人静的时候写文章,也是自己最享受的时光。白天工作很忙,晚上又是各种加班,每天能留给自己的时间真的是少之又少。在睡觉前的这一个小时左右的时间,能够将自己的总结和思考沉淀下来其实一件非常值得开心的事情,如果可以给看到文章的同学一点点启发,那更是善莫大焉。今天和大家主要分享了几种项目中消除重复代码的实践方案,同时沉淀了如何优雅消除代码重复的方法论,希望通过这样的沉淀以及总结可以在大家遇到同样的问题的时候可以有所帮助,通过实际的优化代码落地来提升平台的可维护性。大家有没有在项目中实战过的其他消灭重复代码的实践案例呢?欢迎一起分享讨论交流哦。
- 上一篇: 代码太乱不好改?重构的12个理念帮你轻松搞定
- 下一篇: 总是吐槽别人的代码,好像自己很厉害似的
猜你喜欢
- 2025-01-03 好的代码和坏的代码有哪些本质区别
- 2025-01-03 为了绩效,10行代码被我改成了500行...
- 2025-01-03 一站式统一返回值封装、异常处理、异常错误码解决方案
- 2025-01-03 那些程序员才懂的梗,看到第10张笑喷了,网友:太真实了
- 2025-01-03 Java基础 | 专业排行榜前7的Java代码审计工具
- 2025-01-03 细数软件开发败笔:从代码冗余到架构失衡
- 2025-01-03 总是吐槽别人的代码,好像自己很厉害似的
- 2025-01-03 代码太乱不好改?重构的12个理念帮你轻松搞定
- 2025-01-03 一文掌握代码走查规范和代码检查清单
- 2025-01-03 万星开源项目 Clean Code JavaScript:提升代码质量的必备指南
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- powershellfor (55)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)