어제 친구가 모의면접을 해보고 심층적인 최적화가 필요하다고 하더군요. 그런데 지난달에야 입사한 걸로 기억하는데 그 친구는 왜 다시 이직을 시작했나요? 그와 잠시 대화를 나눈 끝에 그가 해고되었고 그가 작성한 코드가 똥과 같다는 것을 알게 되었습니다. 젠장, 드디어 일자리를 구해서 안타깝네요.
실제로 소프트웨어 엔지니어와 코더의 가장 큰 차이점은 코드를 작성할 때의 습관입니다. 코더는 반복되는 코드를 작성하는 것을 좋아하는 반면, 소프트웨어 엔지니어는 반복되고 중복되는 코드를 제거하기 위해 다양한 기술을 사용합니다.
비즈니스 학생들은 비즈니스 개발에 기술적인 내용이 없고 디자인 패턴, Java 고급 기능 및 OOP를 사용하지 않는다고 불평합니다. 그들은 일반적으로 코드를 작성하고 CRUD에 쌓여 개인적인 성장이 불가능합니다.
사실은 그렇지 않은 것 같아요. 디자인 패턴과 OOP는 대규모 프로젝트의 선배들이 축적한 경험입니다. 이러한 방법론은 대규모 프로젝트의 유지 관리성을 향상시키는 데 사용됩니다. 리플렉션, 주석, 제네릭과 같은 고급 기능이 프레임워크에서 널리 사용되는 이유는 프레임워크가 다양한 데이터 구조를 처리하기 위해 동일한 알고리즘 세트를 사용해야 하는 경우가 많으며 이러한 기능은 코드 중복을 줄이고 프로젝트 유지 관리성을 향상시키는 데 도움이 될 수 있기 때문입니다. .
제 생각에는 유지관리성은 대규모 프로젝트의 성숙도를 나타내는 중요한 지표이며, 유지관리성을 높이는 매우 중요한 방법은 코드 중복을 줄이는 것입니다. 그럼 왜 이런 말을 하는 걸까요?
오늘은 비즈니스 코드에서 가장 일반적인 세 가지 요구 사항부터 시작하여 고급 기능, 디자인 패턴 및 Java의 일부 도구를 사용하여 중복 코드를 제거하여 우아하고 고급스러운 코드를 만드는 방법에 대해 이야기하겠습니다. . 또한 오늘의 연구를 통해 비즈니스 코드에는 기술적인 내용이 없다는 관점이 바뀌기를 바랍니다.
장바구니 주문 기능을 개발하고 사용자마다 다른 처리를 수행한다고 가정합니다.
우리의 목표는 세 가지 유형의 장바구니 비즈니스 로직을 구현하고 입력 매개변수 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; } }
찾아낸 코드의 양을 비교해보면 장바구니 3개에 담긴 코드 중 70%가 중복된 코드입니다. 이유는 간단합니다. 사용자마다 배송비와 할인액을 계산하는 방법이 다르지만 장바구니 전체를 초기화하고 총 가격, 총 배송비, 총 할인 및 결제 가격을 계산하는 논리는 동일합니다.
처음에 언급했듯이 코드 복제 자체는 끔찍한 것이 아니라 누락되거나 실수하는 것이 끔찍한 것입니다. 예를 들어, VIP 사용자 장바구니를 작성한 동급생이 제품의 총 가격 계산에 버그가 있음을 발견했습니다. 모든 항목의 가격을 합산하는 대신 모든 항목의 price*quantity
을 합산해야 합니다.
이때 VIP 사용자의 장바구니 코드만 수정할 수 있으며, 일반 사용자와 내부 사용자의 장바구니의 반복적인 논리 구현에서 동일한 버그는 무시됩니다.
세 개의 장바구니가 있으면 다양한 사용자 유형에 따라 서로 다른 장바구니를 사용해야 합니다. 다음 코드에서 볼 수 있듯이 세 가지 ifs를 사용하여 다양한 유형의 사용자가 다양한 장바구니를 호출할 수 있는 프로세스 방법을 구현합니다.
@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; }
전자상거래 마케팅 방법은 다양하며 앞으로는 필연적으로 더 많은 사용자 유형이 있을 것입니다. 더 많은 장바구니가 필요합니다. 장바구니 클래스를 계속 추가하고, 반복적인 장바구니 로직을 작성하고, 로직을 계속해서 더 작성할 수 있을까요?
물론, 같은 코드는 한 곳에만 나타나서는 안 됩니다!
추상 클래스와 추상 메서드의 정의를 외우면 추상 클래스에 반복되는 논리를 정의하여 장바구니 3개가 서로 다른 논리만 구현하면 되지 않을까 하는 생각이 들 수 있습니다.
사실 이 패턴은 템플릿 메소드 패턴입니다. 부모 클래스에 장바구니 처리 프로세스 템플릿을 구현한 후 특수 처리가 필요한 영역, 즉 추상 메소드 정의를 남겨두고 하위 클래스에서 로직을 구현하도록 했습니다. 상위 클래스의 로직은 불완전하고 단독으로 동작할 수 없으므로 추상 클래스로 정의해야 합니다.
如下代码所示,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
,实现特殊的优惠和运费处理逻辑就可以了?
这样一来,我们就利用工厂模式 + 模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险。这就是设计模式中的开闭原则:对修改关闭,对扩展开放。
是不是有点兴奋了,业务代码居然也能 OOP 了。我们再看一个三方接口的调用案例,同样也是一个普通的业务逻辑。
假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON,而是需要我们把参数依次拼在一起构成一个大字符串。
按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。
因为每一种参数都有固定长度,未达到长度时需要做填充处理:
对所有参数做 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(); } }
可以看到,这段代码的重复粒度更细:
那应该如何改造这段代码呢?没错,就是要用注解和反射!
使用注解和反射这两个武器,就可以针对银行请求的所有逻辑均使用一套代码实现,不会出现任何重复。
要实现接口逻辑和逻辑实现的剥离,首先需要以 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 参数的描述。接下来,我们再看看反射如何配合注解实现动态的接口参数组装:
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); }
其实,许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码。
反射给予了我们在不知晓类结构的时候,按照固定的逻辑处理类的成员;而注解给了我们为这些成员补充元数据的能力,使得我们利用反射实现通用逻辑的时候,可以从外部获得更多我们关心的数据。
最后,我们再来看一种业务代码中经常出现的代码逻辑,实体之间的转换复制。
对于三层架构的系统,考虑到层之间的解耦隔离以及每一层对数据的不同需求,通常每一层都会有自己的 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;
第一种代码重复是,有多个并行的类实现相似的代码逻辑。我们可以考虑提取相同逻辑在父类中实现,差异逻辑通过抽象方法留给子类实现。使用类似的模板方法把相同的流程和逻辑固定成模板,保留差异的同时尽可能避免代码重复。同时,可以使用 Spring 的 IoC 特性注入相应的子类,来避免实例化子类时的大量 if…else
代码。
第二种代码重复是,使用硬编码的方式重复实现相同的数据处理算法。我们可以考虑把规则转换为自定义注解,作为元数据对类或对字段、方法进行描述,然后通过反射动态读取这些元数据、字段或调用方法,实现规则参数和规则定义的分离。也就是说,把变化的部分也就是规则的参数放入注解,规则的定义统一处理。
第三种代码重复是,业务代码中常见的 DO、DTO、VO 转换时大量字段的手动赋值,遇到有上百个属性的复杂类型,非常非常容易出错。我的建议是,不要手动进行赋值,考虑使用 Bean 映射工具进行。此外,还可以考虑采用单元测试对所有字段进行赋值正确性校验。
위 내용은 코드가 형편없어서 해고당했어요!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!