久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

Java中干掉 “重復(fù)代碼” 的技巧大全(經(jīng)驗(yàn)總結(jié))

 昵稱10087950 2022-07-20 發(fā)布于江蘇

大家好,,我是寶哥!

軟件工程師和碼農(nóng)最大的區(qū)別就是平時(shí)寫代碼時(shí)習(xí)慣問(wèn)題,,碼農(nóng)很喜歡寫重復(fù)代碼而軟件工程師會(huì)利用各種技巧去干掉重復(fù)的冗余代碼,。

業(yè)務(wù)同學(xué)抱怨業(yè)務(wù)開(kāi)發(fā)沒(méi)有技術(shù)含量,用不到設(shè)計(jì)模式,、Java 高級(jí)特性,、OOP,平時(shí)寫代碼都在堆 CRUD,,個(gè)人成長(zhǎng)無(wú)從談起,。

其實(shí),我認(rèn)為不是這樣的,。設(shè)計(jì)模式,、OOP 是前輩們?cè)诖笮晚?xiàng)目中積累下來(lái)的經(jīng)驗(yàn),通過(guò)這些方法論來(lái)改善大型項(xiàng)目的可維護(hù)性,。反射、注解,、泛型等高級(jí)特性在框架中大量使用的原因是,,框架往往需要以同一套算法來(lái)應(yīng)對(duì)不同的數(shù)據(jù)結(jié)構(gòu),而這些特性可以幫助減少重復(fù)代碼,提升項(xiàng)目可維護(hù)性,。

在我看來(lái),,可維護(hù)性是大型項(xiàng)目成熟度的一個(gè)重要指標(biāo),而提升可維護(hù)性非常重要的一個(gè)手段就是減少代碼重復(fù),。那為什么這樣說(shuō)呢,?

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

今天,我就從業(yè)務(wù)代碼中最常見(jiàn)的三個(gè)需求展開(kāi),,聊聊如何使用 Java 中的一些高級(jí)特性,、設(shè)計(jì)模式,以及一些工具消除重復(fù)代碼,,才能既優(yōu)雅又高端,。通過(guò)今天的學(xué)習(xí),也希望改變你對(duì)業(yè)務(wù)代碼沒(méi)有技術(shù)含量的看法,。

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

假設(shè)要開(kāi)發(fā)一個(gè)購(gòu)物車下單的功能,針對(duì)不同用戶進(jìn)行不同處理:

  • 普通用戶需要收取運(yùn)費(fèi),,運(yùn)費(fèi)是商品價(jià)格的 10%,,無(wú)商品折扣;
  • VIP 用戶同樣需要收取商品價(jià)格 10% 的快遞費(fèi),,但購(gòu)買兩件以上相同商品時(shí),,第三件開(kāi)始享受一定折扣;
  • 內(nèi)部用戶可以免運(yùn)費(fèi),,無(wú)商品折扣,。

我們的目標(biāo)是實(shí)現(xiàn)三種類型的購(gòu)物車業(yè)務(wù)邏輯,把入?yún)?Map 對(duì)象(Key 是商品 ID,,Value 是商品數(shù)量),,轉(zhuǎn)換為出參購(gòu)物車類型 Cart。

先實(shí)現(xiàn)針對(duì)普通用戶的購(gòu)物車處理邏輯:

//購(gòu)物車
@Data
public class Cart {
    //商品清單
    private List<Item> items = new ArrayList<>();
    //總優(yōu)惠
    private BigDecimal totalDiscount;
    //商品總價(jià)
    private BigDecimal totalItemPrice;
    //總運(yùn)費(fèi)
    private BigDecimal totalDeliveryPrice;
    //應(yīng)付總價(jià)
    private BigDecimal payPrice;
}
//購(gòu)物車中的商品
@Data
public class Item {
    //商品ID
    private long id;
    //商品數(shù)量
    private int quantity;
    //商品單價(jià)
    private BigDecimal price;
    //商品優(yōu)惠
    private BigDecimal couponPrice;
    //商品運(yùn)費(fèi)
    private BigDecimal deliveryPrice;
}
//普通用戶購(gòu)物車處理
public class NormalUserCart {
    public Cart process(long userId, Map<Long, Integer> items) {
        Cart cart = new Cart();

        //把Map的購(gòu)物車轉(zhuǎn)換為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);

        //處理運(yùn)費(fèi)和商品優(yōu)惠
        itemList.stream().forEach(item -> {
            //運(yùn)費(fèi)為商品總價(jià)的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal('0.1')));
            //無(wú)優(yōu)惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        //計(jì)算商品總價(jià)
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //計(jì)算運(yùn)費(fèi)總價(jià)
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //計(jì)算總優(yōu)惠
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //應(yīng)付總價(jià)=商品總價(jià)+運(yùn)費(fèi)總價(jià)-總優(yōu)惠
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }
}

然后實(shí)現(xiàn)針對(duì) VIP 用戶的購(gòu)物車邏輯,。與普通用戶購(gòu)物車邏輯的不同在于,,VIP 用戶能享受同類商品多買的折扣。所以,,這部分代碼只需要額外處理多買折扣部分:


public class VipUserCart {


    public Cart process(long userId, Map<Long, Integer> items) {
        ...


        itemList.stream().forEach(item -> {
            //運(yùn)費(fèi)為商品總價(jià)的10%
            item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal('0.1')));
            //購(gòu)買兩件以上相同商品,,第三件開(kāi)始享受一定折扣
            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;
    }
}

最后是免運(yùn)費(fèi)、無(wú)折扣的內(nèi)部用戶,同樣只是處理商品折扣和運(yùn)費(fèi)時(shí)的邏輯差異:


public class InternalUserCart {


    public Cart process(long userId, Map<Long, Integer> items) {
        ...

        itemList.stream().forEach(item -> {
            //免運(yùn)費(fèi)
            item.setDeliveryPrice(BigDecimal.ZERO);
            //無(wú)優(yōu)惠
            item.setCouponPrice(BigDecimal.ZERO);
        });

        ...
        return cart;
    }
}

對(duì)比一下代碼量可以發(fā)現(xiàn),,三種購(gòu)物車 70% 的代碼是重復(fù)的,。原因很簡(jiǎn)單,雖然不同類型用戶計(jì)算運(yùn)費(fèi)和優(yōu)惠的方式不同,,但整個(gè)購(gòu)物車的初始化,、統(tǒng)計(jì)總價(jià)、總運(yùn)費(fèi),、總優(yōu)惠和支付價(jià)格的邏輯都是一樣的,。

正如我們開(kāi)始時(shí)提到的,代碼重復(fù)本身不可怕,,可怕的是漏改或改錯(cuò),。比如,寫 VIP 用戶購(gòu)物車的同學(xué)發(fā)現(xiàn)商品總價(jià)計(jì)算有 Bug,,不應(yīng)該是把所有 Item 的 price 加在一起,,而是應(yīng)該把所有 Item 的 price*quantity 加在一起。缺項(xiàng)目請(qǐng)點(diǎn)Java項(xiàng)目分享

這時(shí),,他可能會(huì)只修改 VIP 用戶購(gòu)物車的代碼,而忽略了普通用戶,、內(nèi)部用戶的購(gòu)物車中,,重復(fù)的邏輯實(shí)現(xiàn)也有相同的 Bug。

有了三個(gè)購(gòu)物車后,,我們就需要根據(jù)不同的用戶類型使用不同的購(gòu)物車了,。如下代碼所示,使用三個(gè) if 實(shí)現(xiàn)不同類型用戶調(diào)用不同購(gòu)物車的 process 方法:


@GetMapping('wrong')
public Cart wrong(@RequestParam('userId') int userId) {
    //根據(jù)用戶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);
    }
    //內(nèi)部用戶處理邏輯
    if (userCategory.equals('Internal')) {
        InternalUserCart internalUserCart = new InternalUserCart();
        return internalUserCart.process(userId, items);
    }

    return null;
}

電商的營(yíng)銷玩法是多樣的,,以后勢(shì)必還會(huì)有更多用戶類型,,需要更多的購(gòu)物車。我們就只能不斷增加更多的購(gòu)物車類,,一遍一遍地寫重復(fù)的購(gòu)物車邏輯,、寫更多的 if 邏輯嗎?

當(dāng)然不是,,相同的代碼應(yīng)該只在一處出現(xiàn),!

如果我們熟記抽象類和抽象方法的定義的話,這時(shí)或許就會(huì)想到,,是否可以把重復(fù)的邏輯定義在抽象類中,,三個(gè)購(gòu)物車只要分別實(shí)現(xiàn)不同的那份邏輯呢?

其實(shí),,這個(gè)模式就是模板方法模式,。我們?cè)诟割愔袑?shí)現(xiàn)了購(gòu)物車處理的流程模板,,然后把需要特殊處理的地方留空白也就是留抽象方法定義,讓子類去實(shí)現(xiàn)其中的邏輯,。由于父類的邏輯不完整無(wú)法單獨(dú)工作,因此需要定義為抽象類,。

如下代碼所示,,AbstractCart 抽象類實(shí)現(xiàn)了購(gòu)物車通用的邏輯,額外定義了兩個(gè)抽象方法讓子類去實(shí)現(xiàn),。其中,,processCouponPrice 方法用于計(jì)算商品折扣,processDeliveryPrice 方法用于計(jì)算運(yùn)費(fèi),。


public abstract class AbstractCart {
    //處理購(gòu)物車的大量重復(fù)邏輯在父類實(shí)現(xiàn)
    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);
        //讓子類處理每一個(gè)商品的優(yōu)惠
        itemList.stream().forEach(item -> {
            processCouponPrice(userId, item);
            processDeliveryPrice(userId, item);
        });
        //計(jì)算商品總價(jià)
        cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));
        //計(jì)算總運(yùn)費(fèi)
        cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //計(jì)算總折扣
        cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
        //計(jì)算應(yīng)付價(jià)格
        cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
        return cart;
    }

    //處理商品優(yōu)惠的邏輯留給子類實(shí)現(xiàn)
    protected abstract void processCouponPrice(long userId, Item item);
    //處理配送費(fèi)的邏輯留給子類實(shí)現(xiàn)
    protected abstract void processDeliveryPrice(long userId, Item item);
}

有了這個(gè)抽象類,,三個(gè)子類的實(shí)現(xiàn)就非常簡(jiǎn)單了。普通用戶的購(gòu)物車 NormalUserCart,,實(shí)現(xiàn)的是 0 優(yōu)惠和 10% 運(yùn)費(fèi)的邏輯:


@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 用戶的購(gòu)物車 VipUserCart,,直接繼承了 NormalUserCart,只需要修改多買優(yōu)惠策略:


@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);
        }
    }
}

內(nèi)部用戶購(gòu)物車 InternalUserCart 是最簡(jiǎn)單的,,直接設(shè)置 0 運(yùn)費(fèi)和 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);
    }
}

抽象類和三個(gè)子類的實(shí)現(xiàn)關(guān)系圖,,如下所示:

Image

是不是比三個(gè)獨(dú)立的購(gòu)物車程序簡(jiǎn)單了很多呢?接下來(lái),,我們?cè)倏纯慈绾文鼙苊馊齻€(gè) if 邏輯,。

或許你已經(jīng)注意到了,定義三個(gè)購(gòu)物車子類時(shí),,我們?cè)?@Service 注解中對(duì) Bean 進(jìn)行了命名,。既然三個(gè)購(gòu)物車都叫 XXXUserCart,那我們就可以把用戶類型字符串拼接 UserCart 構(gòu)成購(gòu)物車 Bean 的名稱,,然后利用 Spring 的 IoC 容器,,通過(guò) Bean 的名稱直接獲取到 AbstractCart,調(diào)用其 process 方法即可實(shí)現(xiàn)通用,。

其實(shí),,這就是工廠模式,只不過(guò)是借助 Spring 容器實(shí)現(xiàn)罷了:


@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);
}

試想,, 之后如果有了新的用戶類型,、新的用戶邏輯,是不是完全不用對(duì)代碼做任何修改,,只要新增一個(gè) XXXUserCart 類繼承 AbstractCart,,實(shí)現(xiàn)特殊的優(yōu)惠和運(yùn)費(fèi)處理邏輯就可以了?

這樣一來(lái),,我們就利用工廠模式 + 模板方法模式,,不僅消除了重復(fù)代碼,,還避免了修改既有代碼的風(fēng)險(xiǎn)。這就是設(shè)計(jì)模式中的開(kāi)閉原則:對(duì)修改關(guān)閉,,對(duì)擴(kuò)展開(kāi)放,。

2. 利用注解 + 反射消除重復(fù)代碼

是不是有點(diǎn)興奮了,業(yè)務(wù)代碼居然也能 OOP 了,。我們?cè)倏匆粋€(gè)三方接口的調(diào)用案例,,同樣也是一個(gè)普通的業(yè)務(wù)邏輯。

假設(shè)銀行提供了一些 API 接口,,對(duì)參數(shù)的序列化有點(diǎn)特殊,,不使用 JSON,而是需要我們把參數(shù)依次拼在一起構(gòu)成一個(gè)大字符串,。

  • 按照銀行提供的 API 文檔的順序,,把所有參數(shù)構(gòu)成定長(zhǎng)的數(shù)據(jù),然后拼接在一起作為整個(gè)字符串,。
  • 因?yàn)槊恳环N參數(shù)都有固定長(zhǎng)度,,未達(dá)到長(zhǎng)度時(shí)需要做填充處理:
    • 字符串類型的參數(shù)不滿長(zhǎng)度部分需要以下劃線右填充,也就是字符串內(nèi)容靠左,;
    • 數(shù)字類型的參數(shù)不滿長(zhǎng)度部分以 0 左填充,,也就是實(shí)際數(shù)字靠右;
    • 貨幣類型的表示需要把金額向下舍入 2 位到分,,以分為單位,,作為數(shù)字類型同樣進(jìn)行左填充。
  • 對(duì)所有參數(shù)做 MD5 操作作為簽名(為了方便理解,,Demo 中不涉及加鹽處理),。缺項(xiàng)目請(qǐng)點(diǎn)Java項(xiàng)目分享

比如,,創(chuàng)建用戶方法和支付方法的定義是這樣的:

Image

代碼很容易實(shí)現(xiàn),,直接根據(jù)接口定義實(shí)現(xiàn)填充操作、加簽名,、請(qǐng)求調(diào)用操作即可:


public class BankService {

    //創(chuàng)建用戶方法
    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(' ''_'));
        //數(shù)字靠右,,多余的地方用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();
        //數(shù)字靠右,多余的地方用0填充
        stringBuilder.append(String.format('%020d', userId));
        //金額向下舍入2位到分,,以分為單位,,作為數(shù)字靠右,多余的地方用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();
    }
}

可以看到,,這段代碼的重復(fù)粒度更細(xì):

  • 三種標(biāo)準(zhǔn)數(shù)據(jù)類型的處理邏輯有重復(fù),,稍有不慎就會(huì)出現(xiàn) Bug,;
  • 處理流程中字符串拼接、加簽和發(fā)請(qǐng)求的邏輯,,在所有方法重復(fù),;
  • 實(shí)際方法的入?yún)⒌膮?shù)類型和順序,不一定和接口要求一致,,容易出錯(cuò),;
  • 代碼層面針對(duì)每一個(gè)參數(shù)硬編碼,無(wú)法清晰地進(jìn)行核對(duì),,如果參數(shù)達(dá)到幾十個(gè)、上百個(gè),,出錯(cuò)的概率極大,。

那應(yīng)該如何改造這段代碼呢?沒(méi)錯(cuò),,就是要用注解和反射,!

使用注解和反射這兩個(gè)武器,就可以針對(duì)銀行請(qǐng)求的所有邏輯均使用一套代碼實(shí)現(xiàn),,不會(huì)出現(xiàn)任何重復(fù),。

要實(shí)現(xiàn)接口邏輯和邏輯實(shí)現(xiàn)的剝離,首先需要以 POJO 類(只有屬性沒(méi)有任何業(yè)務(wù)邏輯的數(shù)據(jù)類)的方式定義所有的接口參數(shù),。比如,,下面這個(gè)創(chuàng)建用戶 API 的參數(shù):

@Data
public class CreateUserAPI {
    private String name;
    private String identity;
    private String mobile;
    private int age;
}

有了接口參數(shù)定義,我們就能通過(guò)自定義注解為接口和所有參數(shù)增加一些元數(shù)據(jù),。如下所示,,我們定義一個(gè)接口 API 的注解 BankAPI,包含接口 URL 地址和接口說(shuō)明:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {
    String desc() default '';
    String url() default '';
}

然后,,我們?cè)俣x一個(gè)自定義注解 @BankAPIField,,用于描述接口的每一個(gè)字段規(guī)范,包含參數(shù)的次序,、類型和長(zhǎng)度三個(gè)屬性:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {
    int order() default -1;
    int length() default -1;
    String type() default '';
}

接下來(lái),,注解就可以發(fā)揮威力了。

如下所示,,我們定義了 CreateUserAPI 類描述創(chuàng)建用戶接口的信息,,通過(guò)為接口增加 @BankAPI 注解,來(lái)補(bǔ)充接口的 URL 和描述等元數(shù)據(jù),;通過(guò)為每一個(gè)字段增加 @BankAPIField 注解,,來(lái)補(bǔ)充參數(shù)的順序、類型和長(zhǎng)度等元數(shù)據(jù):

@BankAPI(url = '/bank/createUser', desc = '創(chuàng)建用戶接口')
@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;
}

另一個(gè) PayAPI 類也是類似的實(shí)現(xiàn):


@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 個(gè)類繼承的 AbstractAPI 類是一個(gè)空實(shí)現(xiàn),,因?yàn)檫@個(gè)案例中的接口并沒(méi)有公共數(shù)據(jù)可以抽象放到基類,。Java項(xiàng)目分享

通過(guò)這 2 個(gè)類,我們可以在幾秒鐘內(nèi)完成和 API 清單表格的核對(duì),。理論上,,如果我們的核心翻譯過(guò)程(也就是把注解和接口 API 序列化為請(qǐng)求需要的字符串的過(guò)程)沒(méi)問(wèn)題,只要注解和表格一致,,API 請(qǐng)求的翻譯就不會(huì)有任何問(wèn)題,。

以上,我們通過(guò)注解實(shí)現(xiàn)了對(duì) API 參數(shù)的描述,。接下來(lái),,我們?cè)倏纯捶瓷淙绾闻浜献⒔鈱?shí)現(xiàn)動(dòng)態(tài)的接口參數(shù)組裝:

  • 第 3 行代碼中,我們從類上獲得了 BankAPI 注解,,然后拿到其 URL 屬性,,后續(xù)進(jìn)行遠(yuǎn)程調(diào)用。
  • 第 6~9 行代碼,,使用 stream 快速實(shí)現(xiàn)了獲取類中所有帶 BankAPIField 注解的字段,,并把字段按 order 屬性排序,然后設(shè)置私有字段反射可訪問(wèn),。
  • 第 12~38 行代碼,,實(shí)現(xiàn)了反射獲取注解的值,然后根據(jù) BankAPIField 拿到的參數(shù)類型,,按照三種標(biāo)準(zhǔn)進(jìn)行格式化,,將所有參數(shù)的格式化邏輯集中在了這一處。
  • 第 41~48 行代碼,,實(shí)現(xiàn)了參數(shù)加簽和請(qǐng)求調(diào)用,。

private static String remoteCall(AbstractAPI api) throws IOException {
    //從BankAPI注解獲取請(qǐng)求地址
    BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
    bankAPI.url();
    StringBuilder stringBuilder = new StringBuilder();
    Arrays.stream(api.getClass().getDeclaredFields()) //獲得所有字段
            .filter(field -> field.isAnnotationPresent(BankAPIField.class)) //查找標(biāo)記了注解的字段
            .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //根據(jù)注解中的order對(duì)字段排序
            .peek(field -> field.setAccessible(true)) //設(shè)置可以訪問(wèn)私有字段
            .forEach(field -> 
{
                //獲得注解
                BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class);
                Object value = '';
                try {
                    //反射獲取字段值
                    value = field.get(api);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                //根據(jù)字段類型以正確的填充方式格式化字符串
                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();
    //發(fā)請(qǐng)求
    String result = Request.Post('http://localhost:45678/reflection' + bankAPI.url())
            .bodyString(param, ContentType.APPLICATION_JSON)
            .execute().returnContent().asString();
    log.info('調(diào)用銀行API {} url:{} 參數(shù):{} 耗時(shí):{}ms', bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin);
    return result;
}

可以看到,所有處理參數(shù)排序,、填充,、加簽、請(qǐng)求調(diào)用的核心邏輯,,都匯聚在了 remoteCall 方法中,。有了這個(gè)核心方法,BankService 中每一個(gè)接口的實(shí)現(xiàn)就非常簡(jiǎn)單了,,只是參數(shù)的組裝,,然后調(diào)用 remoteCall 即可。


//創(chuàng)建用戶方法
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);
}

其實(shí),,許多涉及類結(jié)構(gòu)性的通用處理,,都可以按照這個(gè)模式來(lái)減少重復(fù)代碼

反射給予了我們?cè)诓恢獣灶惤Y(jié)構(gòu)的時(shí)候,,按照固定的邏輯處理類的成員,;而注解給了我們?yōu)檫@些成員補(bǔ)充元數(shù)據(jù)的能力,,使得我們利用反射實(shí)現(xiàn)通用邏輯的時(shí)候,可以從外部獲得更多我們關(guān)心的數(shù)據(jù),。

3. 利用屬性拷貝工具消除重復(fù)代碼

最后,,我們?cè)賮?lái)看一種業(yè)務(wù)代碼中經(jīng)常出現(xiàn)的代碼邏輯,實(shí)體之間的轉(zhuǎn)換復(fù)制,。

對(duì)于三層架構(gòu)的系統(tǒng),,考慮到層之間的解耦隔離以及每一層對(duì)數(shù)據(jù)的不同需求,通常每一層都會(huì)有自己的 POJO 作為數(shù)據(jù)實(shí)體,。比如,,數(shù)據(jù)訪問(wèn)層的實(shí)體一般叫作 DataObject 或 DO,業(yè)務(wù)邏輯層的實(shí)體一般叫作 Domain,,表現(xiàn)層的實(shí)體一般叫作 Data Transfer Object 或 DTO,。

這里我們需要注意的是,如果手動(dòng)寫這些實(shí)體之間的賦值代碼,,同樣容易出錯(cuò)。

對(duì)于復(fù)雜的業(yè)務(wù)系統(tǒng),,實(shí)體有幾十甚至幾百個(gè)屬性也很正常,。就比如 ComplicatedOrderDTO 這個(gè)數(shù)據(jù)傳輸對(duì)象,描述的是一個(gè)訂單中的幾十個(gè)屬性,。如果我們要把這個(gè) DTO 轉(zhuǎn)換為一個(gè)類似的 DO,,復(fù)制其中大部分的字段,然后把數(shù)據(jù)入庫(kù),,勢(shì)必需要進(jìn)行很多屬性映射賦值操作,。就像這樣,密密麻麻的代碼是不是已經(jīng)讓你頭暈了,?


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()); //屬性錯(cuò)誤
orderDO.setComplainable(orderDTO.isCommentable()); //屬性錯(cuò)誤
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()); //對(duì)象錯(cuò)誤

如果不是代碼中有注釋,,你能看出其中的諸多問(wèn)題嗎?

如果原始的 DTO 有 100 個(gè)字段,,我們需要復(fù)制 90 個(gè)字段到 DO 中,,保留 10 個(gè)不賦值,最后應(yīng)該如何校驗(yàn)正確性呢,?數(shù)數(shù)嗎,?即使數(shù)出有 90 行代碼,也不一定正確,,因?yàn)閷傩钥赡苤貜?fù)賦值,。

有的時(shí)候字段命名相近,比如 complainable 和 commentable,,容易搞反(第 7 和第 8 行),,或者對(duì)兩個(gè)目標(biāo)字段重復(fù)賦值相同的來(lái)源字段(比如第 28 行)

明明要把 DTO 的值賦值到 DO 中,,卻在 set 的時(shí)候從 DO 自己取值(比如第 20 行),導(dǎo)致賦值無(wú)效,。

這段代碼并不是我隨手寫出來(lái)的,,而是一個(gè)真實(shí)案例。有位同學(xué)就像代碼中那樣把經(jīng)緯度賦值反了,,因?yàn)槁鋷?kù)的字段實(shí)在太多了,。這個(gè) Bug 很久都沒(méi)發(fā)現(xiàn),直到真正用到數(shù)據(jù)庫(kù)中的經(jīng)緯度做計(jì)算時(shí),,才發(fā)現(xiàn)一直以來(lái)都存錯(cuò)了,。

修改方法很簡(jiǎn)單,可以使用類似 BeanUtils 這種 Mapping 工具來(lái)做 Bean 的轉(zhuǎn)換,,copyProperties 方法還允許我們提供需要忽略的屬性:

ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, 'id');
return orderDO;

總結(jié)

第一種代碼重復(fù)是,,有多個(gè)并行的類實(shí)現(xiàn)相似的代碼邏輯。我們可以考慮提取相同邏輯在父類中實(shí)現(xiàn),,差異邏輯通過(guò)抽象方法留給子類實(shí)現(xiàn),。使用類似的模板方法把相同的流程和邏輯固定成模板,保留差異的同時(shí)盡可能避免代碼重復(fù),。同時(shí),,可以使用 Spring 的 IoC 特性注入相應(yīng)的子類,來(lái)避免實(shí)例化子類時(shí)的大量 if…else 代碼,。

第二種代碼重復(fù)是,,使用硬編碼的方式重復(fù)實(shí)現(xiàn)相同的數(shù)據(jù)處理算法。我們可以考慮把規(guī)則轉(zhuǎn)換為自定義注解,,作為元數(shù)據(jù)對(duì)類或?qū)ψ侄?、方法進(jìn)行描述,然后通過(guò)反射動(dòng)態(tài)讀取這些元數(shù)據(jù),、字段或調(diào)用方法,,實(shí)現(xiàn)規(guī)則參數(shù)和規(guī)則定義的分離。也就是說(shuō),,把變化的部分也就是規(guī)則的參數(shù)放入注解,,規(guī)則的定義統(tǒng)一處理。

第三種代碼重復(fù)是,,業(yè)務(wù)代碼中常見(jiàn)的 DO,、DTO、VO 轉(zhuǎn)換時(shí)大量字段的手動(dòng)賦值,,遇到有上百個(gè)屬性的復(fù)雜類型,,非常非常容易出錯(cuò)。我的建議是,不要手動(dòng)進(jìn)行賦值,,考慮使用 Bean 映射工具進(jìn)行,。此外,還可以考慮采用單元測(cè)試對(duì)所有字段進(jìn)行賦值正確性校驗(yàn),。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多