<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    干掉 “重復代碼”,這三種方式絕了!

    VSole2022-11-28 11:04:17

    軟件工程師和碼農最大的區別就是平時寫代碼時習慣問題,碼農很喜歡寫重復代碼而軟件工程師會利用各種技巧去干掉重復的冗余代碼。

    業務同學抱怨業務開發沒有技術含量,用不到設計模式、Java 高級特性、OOP,平時寫代碼都在堆 CRUD,個人成長無從談起。

    其實,我認為不是這樣的。設計模式、OOP 是前輩們在大型項目中積累下來的經驗,通過這些方法論來改善大型項目的可維護性。反射、注解、泛型等高級特性在框架中大量使用的原因是,框架往往需要以同一套算法來應對不同的數據結構,而這些特性可以幫助減少重復代碼,提升項目可維護性。

    在我看來,可維護性是大型項目成熟度的一個重要指標,而提升可維護性非常重要的一個手段就是減少代碼重復。那為什么這樣說呢?

    • 如果多處重復代碼實現完全相同的功能,很容易修改一處忘記修改另一處,造成 Bug
    • 有一些代碼并不是完全重復,而是相似度很高,修改這些類似的代碼容易改(復制粘貼)錯,把原本有區別的地方改為了一樣。

    今天,我就從業務代碼中最常見的三個需求展開,聊聊如何使用 Java 中的一些高級特性、設計模式,以及一些工具消除重復代碼,才能既優雅又高端。通過今天的學習,也希望改變你對業務代碼沒有技術含量的看法。

    1. 利用工廠模式 + 模板方法模式,消除 if…else 和重復代碼

    假設要開發一個購物車下單的功能,針對不同用戶進行不同處理:

    • 普通用戶需要收取運費,運費是商品價格的 10%,無商品折扣;
    • VIP 用戶同樣需要收取商品價格 10% 的快遞費,但購買兩件以上相同商品時,第三件開始享受一定折扣;
    • 內部用戶可以免運費,無商品折扣。

    我們的目標是實現三種類型的購物車業務邏輯,把入參 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);  
        }  
    }  
    

    抽象類和三個子類的實現關系圖,如下所示:

    是不是比三個獨立的購物車程序簡單了很多呢?接下來,我們再看看如何能避免三個 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 行代碼,也不一定正確,因為屬性可能重復賦值。

    有的時候字段命名相近,比如 complainablecommentable,容易搞反(第 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 映射工具進行。此外,還可以考慮采用單元測試對所有字段進行賦值正確性校驗。

    關系邏輯bigdecimal
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    數據顯示,目前企業組織每年在數據安全防護上投入的資金已經超過千億美元,但數據泄露事件依然在持續增長。為了更好地保護企業數據安全投資,安全人員應該更好地理解和診斷自身數據安全盲點。07執行管理和審查要明確規定誰來負責執行策略,同時還要對策略執行情況進行審查,保證策略的有效實施。如果未遵守策略要求,將受到相應的違規處罰。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类