Rumah  >  Artikel  >  Java  >  Kod itu mengerikan dan saya dipecat!

Kod itu mengerikan dan saya dipecat!

Java后端技术全栈
Java后端技术全栈ke hadapan
2023-08-15 15:44:52427semak imbas

Semalam, seorang kawan memberitahu saya bahawa dia perlu melakukan temuduga olok-olok dan mewujudkan pengoptimuman yang mendalam Namun, saya ingat dia baru menyertai kerja itu bulan lepas, jadi kenapa dia mula mencari kerja semula? Selepas berbual sebentar dengannya, saya mendapat tahu bahawa dia telah dipecat dan kod yang dia tulis adalah seperti cebisan. Alamak, sayangnya saya akhirnya mendapat pekerjaan.

Malah, perbezaan terbesar antara jurutera perisian dan pengekod adalah tabiat mereka semasa menulis kod yang berulang, manakala jurutera perisian akan menggunakan pelbagai teknik untuk menghapuskan kod berulang dan berlebihan.

Pelajar perniagaan mengadu bahawa pembangunan perniagaan tidak mempunyai kandungan teknikal dan tidak menggunakan corak reka bentuk, ciri Java lanjutan dan OOP Mereka biasanya menghabiskan banyak masa menulis kod dan CRUD, menjadikan pertumbuhan peribadi mustahil.

Sebenarnya, saya rasa tidak begitu. Corak reka bentuk dan OOP ialah pengalaman yang dikumpul oleh pendahulu dalam projek berskala besar. Metodologi ini digunakan untuk meningkatkan kebolehselenggaraan projek berskala besar. Sebab mengapa ciri lanjutan seperti refleksi, anotasi dan generik digunakan secara meluas dalam rangka kerja ialah rangka kerja selalunya perlu menggunakan set algoritma yang sama untuk menangani struktur data yang berbeza, dan ciri ini boleh membantu mengurangkan pertindihan kod dan meningkatkan kebolehselenggaraan projek .

Pada pendapat saya, kebolehselenggaraan ialah penunjuk penting kematangan projek besar, dan cara yang sangat penting untuk meningkatkan kebolehselenggaraan ialah mengurangkan pertindihan kod. Jadi mengapa anda mengatakan ini?

  • Jika kod diulang di beberapa tempat untuk mencapai fungsi yang sama, adalah mudah untuk mengubah suai satu tempat dan lupa untuk mengubah suai yang lain, menyebabkan pepijat
  • Sesetengah kod tidak diulang sepenuhnya, tetapi sangat serupa . Mengubah suai ini adalah serupa Kod mudah untuk membuat kesilapan (salin dan tampal), dan perbezaan asal ditukar kepada yang sama.

Hari ini, saya akan mulakan dengan tiga keperluan paling biasa dalam kod perniagaan dan bercakap tentang cara menggunakan beberapa ciri lanjutan, corak reka bentuk dan beberapa alatan dalam Java untuk menghapuskan kod pendua agar menjadi elegan dan mewah. . Melalui kajian hari ini, saya juga berharap dapat mengubah pandangan anda bahawa kod perniagaan tidak mempunyai kandungan teknikal. . pengguna memerlukan Kos penghantaran dikenakan, iaitu 10% daripada harga produk, dan tiada diskaun produk

Pengguna VIP juga perlu mengenakan 10% daripada harga produk untuk penghantaran ekspres, tetapi apabila membeli dua atau lebih daripada produk yang sama, yang ketiga akan menikmati diskaun tertentu;

Pengguna dalaman boleh menikmati penghantaran percuma dan tiada diskaun produk.

  • Matlamat kami adalah untuk melaksanakan tiga jenis logik perniagaan troli beli-belah dan menukar parameter input objek Peta (Kunci ialah ID produk, Nilai ialah kuantiti produk) ke dalam Troli jenis troli beli-belah keluaran.
  • Mula-mula laksanakan logik pemprosesan troli beli-belah untuk pengguna biasa:
  • //购物车  
    @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;  
        }  
    }

    Kemudian laksanakan logik troli beli-belah untuk pengguna VIP. Perbezaan dari logik troli beli-belah pengguna biasa ialah pengguna VIP boleh menikmati diskaun untuk membeli lebih banyak produk yang sama. Oleh itu, bahagian kod ini hanya perlu memproses tambahan bahagian diskaun berbilang beli:

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

    Akhirnya, terdapat penghantaran percuma dan tiada diskaun pengguna dalaman Begitu juga perbezaan logik semasa memproses diskaun produk dan kos penghantaran:

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

    Membandingkan jumlah kod yang boleh ditemui, 70% daripada kod dalam tiga troli beli-belah diduakan. Alasannya mudah. ​​Walaupun pelbagai jenis pengguna mengira kos penghantaran dan diskaun dengan cara yang berbeza, logik untuk memulakan keseluruhan troli beli-belah, mengira jumlah harga, jumlah kos penghantaran, jumlah diskaun dan harga pembayaran adalah sama.

    Seperti yang kami nyatakan pada permulaan, pertindihan kod itu sendiri tidak mengerikan, apa yang mengerikan adalah hilang atau melakukan kesilapan. Contohnya, rakan sekelas yang menulis troli beli-belah pengguna VIP mendapati terdapat pepijat dalam pengiraan jumlah harga produk Daripada menambah harga semua Item bersama-sama, price*quantity semua Item harus ditambah bersama.

    Pada masa ini, dia hanya boleh mengubah suai kod troli beli-belah pengguna VIP, sambil mengabaikan pepijat yang sama dalam pelaksanaan logik berulang pengguna biasa dan troli beli-belah pengguna dalaman.

    Selepas kita mempunyai tiga troli beli-belah, kita perlu menggunakan troli beli-belah yang berbeza mengikut jenis pengguna yang berbeza. Seperti yang ditunjukkan dalam kod berikut, tiga if digunakan untuk melaksanakan kaedah proses untuk jenis pengguna yang berbeza untuk memanggil troli beli-belah yang berbeza:

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

    Kaedah pemasaran e-dagang adalah pelbagai, dan sudah pasti akan terdapat lebih banyak jenis pengguna pada masa hadapan, memerlukan lebih banyak troli beli-belah. Bolehkah kita terus menambah lebih banyak kelas troli beli-belah, menulis logik troli beli-belah berulang, dan menulis lebih banyak lagi jika logik berulang kali?

    Alat menjawab soalan dalam talian

    Sudah tentu tidak, kod yang sama hanya sepatutnya muncul di satu tempat!

    Jika kita menghafal definisi kelas abstrak dan kaedah abstrak, maka kita mungkin berfikir, bolehkah kita mentakrifkan logik berulang dalam kelas abstrak, sehingga ketiga-tiga shopping cart itu hanya perlu melaksanakan logik yang berbeza?

    Sebenarnya corak ini adalah corak kaedah templat. Kami melaksanakan templat proses pemprosesan troli beli-belah dalam kelas induk, dan kemudian biarkan kosong kawasan yang memerlukan pemprosesan khas, iaitu, meninggalkan definisi kaedah abstrak dan biarkan subkelas melaksanakan logik. Oleh kerana logik kelas induk tidak lengkap dan tidak boleh berfungsi sendiri, ia perlu ditakrifkan sebagai kelas abstrak.

    Alat menjawab soalan dalam talian

    如下代码所示,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);  
        }  
    }

    抽象类和三个子类的实现关系图,如下所示:

    Kod itu mengerikan dan saya dipecat!

    是不是比三个独立的购物车程序简单了很多呢?接下来,我们再看看如何能避免三个 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 中不涉及加盐处理)。

    比如,创建用户方法和支付方法的定义是这样的:

    Kod itu mengerikan dan saya dipecat!

    代码很容易实现,直接根据接口定义实现填充操作、加签名、请求调用操作即可:

    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(&#39; &#39;, &#39;_&#39;));  
            //字符串靠左,多余的地方填充_  
            stringBuilder.append(String.format("%-18s", identity).replace(&#39; &#39;, &#39;_&#39;));  
            //数字靠右,多余的地方用0填充  
            stringBuilder.append(String.format("%05d", age));  
            //字符串靠左,多余的地方用_填充  
            stringBuilder.append(String.format("%-11s", mobile).replace(&#39; &#39;, &#39;_&#39;));  
            //最后加上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(&#39; &#39;, &#39;_&#39;));  
                            break;  
                        }  
                        case "N": {  
                            stringBuilder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(&#39; &#39;, &#39;0&#39;));  
                            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 映射工具进行。此外,还可以考虑采用单元测试对所有字段进行赋值正确性校验。

Atas ialah kandungan terperinci Kod itu mengerikan dan saya dipecat!. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:Java后端技术全栈. Jika ada pelanggaran, sila hubungi admin@php.cn Padam