Java解決代碼重復(fù)的三個(gè)絕招分享
業(yè)務(wù)同學(xué)抱怨業(yè)務(wù)開發(fā)沒有技術(shù)含量,用不到設(shè)計(jì)模式、Java 高級(jí)特性、OOP,平時(shí)寫代碼都在堆 CRUD,個(gè)人成長無從談起。
其實(shí),我認(rèn)為不是這樣的。設(shè)計(jì)模式、OOP 是前輩們?cè)诖笮晚?xiàng)目中積累下來的經(jīng)驗(yàn),通過這些方法論來改善大型項(xiàng)目的可維護(hù)性。反射、注解、泛型等高級(jí)特性在框架中大量使用的原因是,框架往往需要以同一套算法來應(yīng)對(duì)不同的數(shù)據(jù)結(jié)構(gòu),而這些特性可以幫助減少重復(fù)代碼,提升項(xiàng)目可維護(hù)性。
在我看來,可維護(hù)性是大型項(xiàng)目成熟度的一個(gè)重要指標(biāo),而提升可維護(hù)性非常重要的一個(gè)手段就是減少代碼重復(fù)。那為什么這樣說呢?
- 如果多處重復(fù)代碼實(shí)現(xiàn)完全相同的功能,很容易修改一處忘記修改另一處,造成 Bug
- 有一些代碼并不是完全重復(fù),而是相似度很高,修改這些類似的代碼容易改(復(fù)制粘貼)錯(cuò),把原本有區(qū)別的地方改為了一樣。
今天,我就從業(yè)務(wù)代碼中最常見的三個(gè)需求展開,聊聊如何使用 Java 中的一些高級(jí)特性、設(shè)計(jì)模式,以及一些工具消除重復(fù)代碼,才能既優(yōu)雅又高端。通過今天的學(xué)習(xí),也希望改變你對(duì)業(yè)務(wù)代碼沒有技術(shù)含量的看法。
1. 利用工廠模式 + 模板方法模式,消除 if…else 和重復(fù)代碼
假設(shè)要開發(fā)一個(gè)購物車下單的功能,針對(duì)不同用戶進(jìn)行不同處理:
- 普通用戶需要收取運(yùn)費(fèi),運(yùn)費(fèi)是商品價(jià)格的 10%,無商品折扣;
- VIP 用戶同樣需要收取商品價(jià)格 10% 的快遞費(fèi),但購買兩件以上相同商品時(shí),第三件開始享受一定折扣;
- 內(nèi)部用戶可以免運(yùn)費(fèi),無商品折扣。
我們的目標(biāo)是實(shí)現(xiàn)三種類型的購物車業(yè)務(wù)邏輯,把入?yún)?Map 對(duì)象(Key 是商品 ID,Value 是商品數(shù)量),轉(zhuǎn)換為出參購物車類型 Cart。
先實(shí)現(xiàn)針對(duì)普通用戶的購物車處理邏輯:
//購物車 @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; } //購物車中的商品 @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; } //普通用戶購物車處理 public class NormalUserCart { public Cart process(long userId, Map<Long, Integer> items) { Cart cart = new Cart(); //把Map的購物車轉(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"))); //無優(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 用戶的購物車邏輯。與普通用戶購物車邏輯的不同在于,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"))); //購買兩件以上相同商品,第三件開始享受一定折扣 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)、無折扣的內(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); //無優(yōu)惠 item.setCouponPrice(BigDecimal.ZERO); }); ... return cart; } }
對(duì)比一下代碼量可以發(fā)現(xiàn),三種購物車 70% 的代碼是重復(fù)的。原因很簡單,雖然不同類型用戶計(jì)算運(yùn)費(fèi)和優(yōu)惠的方式不同,但整個(gè)購物車的初始化、統(tǒng)計(jì)總價(jià)、總運(yùn)費(fèi)、總優(yōu)惠和支付價(jià)格的邏輯都是一樣的。
正如我們開始時(shí)提到的,代碼重復(fù)本身不可怕,可怕的是漏改或改錯(cuò)。比如,寫 VIP 用戶購物車的同學(xué)發(fā)現(xiàn)商品總價(jià)計(jì)算有 Bug,不應(yīng)該是把所有 Item 的 price 加在一起,而是應(yīng)該把所有 Item 的 price*quantity 加在一起。
這時(shí),他可能會(huì)只修改 VIP 用戶購物車的代碼,而忽略了普通用戶、內(nèi)部用戶的購物車中,重復(fù)的邏輯實(shí)現(xiàn)也有相同的 Bug。
有了三個(gè)購物車后,我們就需要根據(jù)不同的用戶類型使用不同的購物車了。如下代碼所示,使用三個(gè) if 實(shí)現(xiàn)不同類型用戶調(diào)用不同購物車的 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; }
電商的營銷玩法是多樣的,以后勢必還會(huì)有更多用戶類型,需要更多的購物車。我們就只能不斷增加更多的購物車類,一遍一遍地寫重復(fù)的購物車邏輯、寫更多的 if 邏輯嗎?
當(dāng)然不是,相同的代碼應(yīng)該只在一處出現(xiàn)!
如果我們熟記抽象類和抽象方法的定義的話,這時(shí)或許就會(huì)想到,是否可以把重復(fù)的邏輯定義在抽象類中,三個(gè)購物車只要分別實(shí)現(xiàn)不同的那份邏輯呢?
其實(shí),這個(gè)模式就是模板方法模式。我們?cè)诟割愔袑?shí)現(xiàn)了購物車處理的流程模板,然后把需要特殊處理的地方留空白也就是留抽象方法定義,讓子類去實(shí)現(xiàn)其中的邏輯。由于父類的邏輯不完整無法單獨(dú)工作,因此需要定義為抽象類。
如下代碼所示,AbstractCart 抽象類實(shí)現(xiàn)了購物車通用的邏輯,額外定義了兩個(gè)抽象方法讓子類去實(shí)現(xiàn)。其中,processCouponPrice 方法用于計(jì)算商品折扣,processDeliveryPrice 方法用于計(jì)算運(yùn)費(fèi)。
public abstract class AbstractCart { //處理購物車的大量重復(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)就非常簡單了。普通用戶的購物車 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 用戶的購物車 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)部用戶購物車 InternalUserCart 是最簡單的,直接設(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)系圖,如下所示:
是不是比三個(gè)獨(dú)立的購物車程序簡單了很多呢?接下來,我們?cè)倏纯慈绾文鼙苊馊齻€(gè) if 邏輯。
或許你已經(jīng)注意到了,定義三個(gè)購物車子類時(shí),我們?cè)?nbsp;@Service 注解中對(duì) Bean 進(jìn)行了命名。既然三個(gè)購物車都叫 XXXUserCart,那我們就可以把用戶類型字符串拼接 UserCart 構(gòu)成購物車 Bean 的名稱,然后利用 Spring 的 IoC 容器,通過 Bean 的名稱直接獲取到 AbstractCart,調(diào)用其 process 方法即可實(shí)現(xiàn)通用。
其實(shí),這就是工廠模式,只不過是借助 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)處理邏輯就可以了?
這樣一來,我們就利用工廠模式 + 模板方法模式,不僅消除了重復(fù)代碼,還避免了修改既有代碼的風(fēng)險(xiǎn)。這就是設(shè)計(jì)模式中的開閉原則:對(duì)修改關(guān)閉,對(duì)擴(kuò)展開放。
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)成定長的數(shù)據(jù),然后拼接在一起作為整個(gè)字符串。
- 因?yàn)槊恳环N參數(shù)都有固定長度,未達(dá)到長度時(shí)需要做填充處理:
字符串類型的參數(shù)不滿長度部分需要以下劃線右填充,也就是字符串內(nèi)容靠左;
數(shù)字類型的參數(shù)不滿長度部分以 0 左填充,也就是實(shí)際數(shù)字靠右;
貨幣類型的表示需要把金額向下舍入 2 位到分,以分為單位,作為數(shù)字類型同樣進(jìn)行左填充。
對(duì)所有參數(shù)做 MD5 操作作為簽名(為了方便理解,Demo 中不涉及加鹽處理)。
比如,創(chuàng)建用戶方法和支付方法的定義是這樣的:
代碼很容易實(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ù)硬編碼,無法清晰地進(jìn)行核對(duì),如果參數(shù)達(dá)到幾十個(gè)、上百個(gè),出錯(cuò)的概率極大。
那應(yīng)該如何改造這段代碼呢?沒錯(cuò),就是要用注解和反射!
使用注解和反射這兩個(gè)武器,就可以針對(duì)銀行請(qǐng)求的所有邏輯均使用一套代碼實(shí)現(xiàn),不會(huì)出現(xiàn)任何重復(fù)。
要實(shí)現(xiàn)接口邏輯和邏輯實(shí)現(xiàn)的剝離,首先需要以 POJO 類(只有屬性沒有任何業(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ù)定義,我們就能通過自定義注解為接口和所有參數(shù)增加一些元數(shù)據(jù)。如下所示,我們定義一個(gè)接口 API 的注解 BankAPI,包含接口 URL 地址和接口說明:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Inherited public @interface BankAPI { String desc() default ""; String url() default ""; }
然后,我們?cè)俣x一個(gè)自定義注解 @BankAPIField,用于描述接口的每一個(gè)字段規(guī)范,包含參數(shù)的次序、類型和長度三個(gè)屬性:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented @Inherited public @interface BankAPIField { int order() default -1; int length() default -1; String type() default ""; }
接下來,注解就可以發(fā)揮威力了。
如下所示,我們定義了 CreateUserAPI 類描述創(chuàng)建用戶接口的信息,通過為接口增加 @BankAPI 注解,來補(bǔ)充接口的 URL 和描述等元數(shù)據(jù);通過為每一個(gè)字段增加 @BankAPIField 注解,來補(bǔ)充參數(shù)的順序、類型和長度等元數(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è)案例中的接口并沒有公共數(shù)據(jù)可以抽象放到基類。
通過這 2 個(gè)類,我們可以在幾秒鐘內(nèi)完成和 API 清單表格的核對(duì)。理論上,如果我們的核心翻譯過程(也就是把注解和接口 API 序列化為請(qǐng)求需要的字符串的過程)沒問題,只要注解和表格一致,API 請(qǐng)求的翻譯就不會(huì)有任何問題。
以上,我們通過注解實(shí)現(xiàn)了對(duì) API 參數(shù)的描述。接下來,我們?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è)置私有字段反射可訪問。
第 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è)置可以訪問私有字段 .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)就非常簡單了,只是參數(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è)模式來減少重復(fù)代碼。
反射給予了我們?cè)诓恢獣灶惤Y(jié)構(gòu)的時(shí)候,按照固定的邏輯處理類的成員;而注解給了我們?yōu)檫@些成員補(bǔ)充元數(shù)據(jù)的能力,使得我們利用反射實(shí)現(xiàn)通用邏輯的時(shí)候,可以從外部獲得更多我們關(guān)心的數(shù)據(jù)。
3. 利用屬性拷貝工具消除重復(fù)代碼
最后,我們?cè)賮砜匆环N業(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ù)訪問層的實(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ù)入庫,勢必需要進(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ò)誤
如果不是代碼中有注釋,你能看出其中的諸多問題嗎?
如果原始的 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ù)賦值相同的來源字段(比如第 28 行)
明明要把 DTO 的值賦值到 DO 中,卻在 set 的時(shí)候從 DO 自己取值(比如第 20 行),導(dǎo)致賦值無效。
這段代碼并不是我隨手寫出來的,而是一個(gè)真實(shí)案例。有位同學(xué)就像代碼中那樣把經(jīng)緯度賦值反了,因?yàn)槁鋷斓淖侄螌?shí)在太多了。這個(gè) Bug 很久都沒發(fā)現(xiàn),直到真正用到數(shù)據(jù)庫中的經(jīng)緯度做計(jì)算時(shí),才發(fā)現(xiàn)一直以來都存錯(cuò)了。
修改方法很簡單,可以使用類似 BeanUtils 這種 Mapping 工具來做 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),差異邏輯通過抽象方法留給子類實(shí)現(xiàn)。使用類似的模板方法把相同的流程和邏輯固定成模板,保留差異的同時(shí)盡可能避免代碼重復(fù)。同時(shí),可以使用 Spring 的 IoC 特性注入相應(yīng)的子類,來避免實(shí)例化子類時(shí)的大量 if…else 代碼。
第二種代碼重復(fù)是,使用硬編碼的方式重復(fù)實(shí)現(xiàn)相同的數(shù)據(jù)處理算法。我們可以考慮把規(guī)則轉(zhuǎn)換為自定義注解,作為元數(shù)據(jù)對(duì)類或?qū)ψ侄巍⒎椒ㄟM(jìn)行描述,然后通過反射動(dòng)態(tài)讀取這些元數(shù)據(jù)、字段或調(diào)用方法,實(shí)現(xiàn)規(guī)則參數(shù)和規(guī)則定義的分離。也就是說,把變化的部分也就是規(guī)則的參數(shù)放入注解,規(guī)則的定義統(tǒng)一處理。
第三種代碼重復(fù)是,業(yè)務(wù)代碼中常見的 DO、DTO、VO 轉(zhuǎn)換時(shí)大量字段的手動(dòng)賦值,遇到有上百個(gè)屬性的復(fù)雜類型,非常非常容易出錯(cuò)。我的建議是,不要手動(dòng)進(jìn)行賦值,考慮使用 Bean 映射工具進(jìn)行。此外,還可以考慮采用單元測試對(duì)所有字段進(jìn)行賦值正確性校驗(yàn)。
以上就是Java解決代碼重復(fù)的三個(gè)絕招分享的詳細(xì)內(nèi)容,更多關(guān)于Java代碼重復(fù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Spring 攔截器流程及多個(gè)攔截器的執(zhí)行順序
這篇文章主要介紹了Spring 攔截器流程及多個(gè)攔截器的執(zhí)行順序的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用Spring框架,感興趣的朋友可以了解下2021-05-05SpringBoot使用ApplicationEvent&Listener完成業(yè)務(wù)解耦
這篇文章主要介紹了SpringBoot使用ApplicationEvent&Listener完成業(yè)務(wù)解耦示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05SpringBoot整合SpringSecurity實(shí)現(xiàn)權(quán)限控制之實(shí)現(xiàn)多標(biāo)簽頁
這篇文章主要介紹了SpringBoot整合SpringSecurity實(shí)現(xiàn)權(quán)限控制之實(shí)現(xiàn)多標(biāo)簽頁,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11mybatis?foreach傳兩個(gè)參數(shù)批量刪除
這篇文章主要介紹了mybatis?foreach?批量刪除傳兩個(gè)參數(shù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04如何使用JDBC連接數(shù)據(jù)庫并執(zhí)行SQL語句
JDBC是Java數(shù)據(jù)庫連接的縮寫,是Java程序與數(shù)據(jù)庫進(jìn)行交互的標(biāo)準(zhǔn)API。JDBC主要包括Java.sql和javax.sql兩個(gè)包,通過DriverManager獲取數(shù)據(jù)庫連接對(duì)象Connection,并通過Statement或PreparedStatement執(zhí)行SQL語句2023-04-04