25种代码坏味道总结+优化示例

开发 后端
什么样的代码是好代码呢?好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢?这25种代码坏味道大家要注意啦。

 前言

什么样的代码是好代码呢?好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢?这25种代码坏味道大家要注意啦

1. Duplicated Code (重复代码)

重复代码就是不同地点,有着相同的程序结构。一般是因为需求迭代比较快,开发小伙伴担心影响已有功能,就复制粘贴造成的。重复代码很难维护的,如果你要修改其中一段的代码逻辑,就需要修改多次,很可能出现遗漏的情况。

如何优化重复代码呢?分三种情况讨论:

同一个类的两个函数含有相同的表达式 

  1. class A {  
  2.     public void method1() {  
  3.         doSomething1  
  4.         doSomething2  
  5.         doSomething3  
  6.     }  
  7.     public void method2() {  
  8.         doSomething1  
  9.         doSomething2  
  10.         doSomething4  
  11.     }  

优化手段:可以使用Extract Method(提取公共函数) 抽出重复的代码逻辑,组成一个公用的方法。 

  1. class A {  
  2.     public void method1() {  
  3.         commonMethod();  
  4.         doSomething3  
  5.     }  
  6.     public void method2() {  
  7.         commonMethod();  
  8.         doSomething4  
  9.     }   
  10.     public void commonMethod(){  
  11.        doSomething1  
  12.        doSomething2  
  13.     }  

两个互为兄弟的子类内含相同的表达式 

  1. class A extend C {  
  2.     public void method1() {  
  3.         doSomething1  
  4.         doSomething2  
  5.         doSomething3  
  6.     }  
  7.  
  8. class B extend C {  
  9.     public void method1() {  
  10.         doSomething1  
  11.         doSomething2  
  12.         doSomething4  
  13.     }  

优化手段:对两个类都使用Extract Method(提取公共函数),然后把抽取出来的函数放到父类中。 

  1. class C {  
  2.     public void commonMethod(){  
  3.      doSomething1  
  4.      doSomething2  
  5.    }  
  6.  class A extend C {  
  7.     public void method1() {  
  8.         commonMethod();  
  9.         doSomething3  
  10.     }  
  11.  
  12. class B extend C {  
  13.     public void method1() {  
  14.         commonMethod();  
  15.         doSomething4  
  16.     }  

两个毫不相关的类出现重复代码

如果是两个毫不相关的类出现重复代码,可以使用Extract Class将重复代码提炼到一个类中。这个新类可以是一个普通类,也可以是一个工具类,看具体业务怎么划分吧。

2 .Long Method (长函数)

长函数是指一个函数方法几百行甚至上千行,可读性大大降低,不便于理解。反例如下: 

  1. public class Test {  
  2.     private String name;  
  3.     private Vector<Order> orders = new Vector<Order>();  
  4.     public void printOwing() {  
  5.         //print banner  
  6.         System.out.println("****************");  
  7.         System.out.println("*****customer Owes *****");  
  8.         System.out.println("****************");  
  9.         //calculate totalAmount  
  10.         Enumeration env = orders.elements();  
  11.         double totalAmount = 0.0;  
  12.         while (env.hasMoreElements()) {  
  13.             Order order = (Order) env.nextElement();  
  14.             totalAmount += order.getAmout();  
  15.         }  
  16.         //print details  
  17.         System.out.println("name:" + name);  
  18.         System.out.println("amount:" + totalAmount);  
  19.         ...... 
  20.     }  

可以使用Extract Method,抽取功能单一的代码段,组成命名清晰的小函数,去解决长函数问题,正例如下: 

  1. public class Test {  
  2.     private String name;  
  3.     private Vector<Order> orders = new Vector<Order>();  
  4.     public void printOwing() {  
  5.         //print banner  
  6.         printBanner();  
  7.         //calculate totalAmount  
  8.         double totalAmount = getTotalAmount();  
  9.         //print details  
  10.         printDetail(totalAmount);  
  11.     }  
  12.     void printBanner(){  
  13.         System.out.println("****************");  
  14.         System.out.println("*****customer Owes *****");  
  15.         System.out.println("****************");  
  16.     }  
  17.     double getTotalAmount(){  
  18.         Enumeration env = orders.elements();  
  19.         double totalAmount = 0.0;  
  20.         while (env.hasMoreElements()) {  
  21.             Order order = (Order) env.nextElement();  
  22.             totalAmount += order.getAmout();  
  23.         }  
  24.         return totalAmount;  
  25.     }  
  26.     void printDetail(double totalAmount){  
  27.         System.out.println("name:" + name);  
  28.         System.out.println("amount:" + totalAmount);  
  29.     }   

3.  Large Class (过大的类)

一个类做太多事情,维护了太多功能,可读性变差,性能也会下降。举个例子,订单相关的功能你放到一个类A里面,商品库存相关的也放在类A里面,积分相关的还放在类A里面...反例如下: 

  1. Class A{  
  2.   public void printOrder(){  
  3.    System.out.println("订单");  
  4.   }    
  5.   public void printGoods(){  
  6.    System.out.println("商品");  
  7.   }   
  8.   public void printPoints(){  
  9.    System.out.println("积分");  
  10.   }  

试想一下,乱七八糟的代码块都往一个类里面塞,还谈啥可读性。应该按单一职责,使用Extract Class把代码划分开,正例如下: 

  1. Class Order{  
  2.   public void printOrder(){  
  3.    System.out.println("订单");  
  4.   }  
  5.  
  6. Class Goods{  
  7.    public void printGoods(){  
  8.    System.out.println("商品");  
  9.   }  
  10.   
  11. Class Points{     
  12.   public void printPoints(){  
  13.    System.out.println("积分");  
  14.   }  
  15.  }  

4. Long Parameter List (过长参数列)

方法参数数量过多的话,可读性很差。如果有多个重载方法,参数很多的话,有时候你都不知道调哪个呢。并且,如果参数很多,做新老接口兼容处理也比较麻烦。 

  1. public void getUserInfo(String name,String age,String sex,String mobile){  
  2.   // do something ...  

如何解决过长参数列问题呢?将参数封装成结构或者类,比如我们将参数封装成一个DTO类,如下: 

  1. public void getUserInfo(UserInfoParamDTO userInfoParamDTO){  
  2.   // do something ...  
  3.  
  4. class UserInfoParamDTO{  
  5.   private String name;  
  6.   private String age;  
  7.   private String sex;  
  8.   private String mobile;  

5. Divergent Change (发散式变化)

对程序进行维护时, 如果添加修改组件, 要同时修改一个类中的多个方法, 那么这就是 Divergent Change。举个汽车的例子,某个汽车厂商生产三种品牌的汽车:BMW、Benz和LaoSiLaiSi,每种品牌又可以选择燃油、纯电和混合动力。反例如下: 

  1. /**  
  2.  *  公众号:捡田螺的小男孩  
  3.  */  
  4. public class Car {  
  5.     private String name;  
  6.     void start(Engine engine) {  
  7.         if ("HybridEngine".equals(engine.getName())) {  
  8.             System.out.println("Start Hybrid Engine..."); 
  9.          } else if ("GasolineEngine".equals(engine.getName())) {  
  10.             System.out.println("Start Gasoline Engine...");  
  11.         } else if ("ElectricEngine".equals(engine.getName())) {  
  12.             System.out.println("Start Electric Engine");  
  13.         }  
  14.     }  
  15.     void drive(Engine engine,Car car) {  
  16.         this.start(engine);  
  17.         System.out.println("Drive " + getBrand(car) + " car...");  
  18.     }  
  19.     String getBrand(Car car) {  
  20.         if ("Baoma".equals(car.getName())) {  
  21.             return "BMW";  
  22.         } else if ("BenChi".equals(car.getName())) {  
  23.             return "Benz";  
  24.         } else if ("LaoSiLaiSi".equals(car.getName())) {  
  25.             return "LaoSiLaiSi";  
  26.         }  
  27.         return null; 
  28.      }  
  29.  } 

如果新增一种品牌新能源电车,然后它的启动引擎是核动力呢,那么就需要修改Car类的start和getBrand方法啦,这就是代码坏味道:Divergent Change (发散式变化)。

如何优化呢?一句话总结:拆分类,将总是一起变化的东西放到一块。

    ★        运用提炼类(Extract Class) 拆分类的行为。

               如果不同的类有相同的行为,提炼超类(Extract Superclass) 和 提炼子类(Extract Subclass)。    ”

正例如下:

因为Engine是独立变化的,所以提取一个Engine接口,如果新加一个启动引擎,多一个实现类即可。如下: 

  1. //IEngine  
  2. public interface IEngine {  
  3.     void start();  
  4.  
  5. public class HybridEngineImpl implements IEngine {   
  6.     @Override  
  7.     public void start() {  
  8.         System.out.println("Start Hybrid Engine...");  
  9.     }  

因为drive方法依赖于Car,IEngine,getBand方法;getBand方法是变化的,也跟Car是有关联的,所以可以搞个抽象Car的类,每个品牌汽车继承于它即可,如下 

  1. public abstract class AbstractCar {  
  2.     protected IEngine engine;  
  3.     public AbstractCar(IEngine engine) {  
  4.         this.engine = engine;  
  5.     }  
  6.     public abstract void drive();  
  7.  
  8. //奔驰汽车  
  9. public class BenzCar extends AbstractCar {  
  10.     public BenzCar(IEngine engine) { 
  11.          super(engine);  
  12.     }  
  13.     @Override  
  14.     public void drive() {  
  15.       this.engine.start();  
  16.       System.out.println("Drive " + getBrand() + " car..."); 
  17.     }  
  18.     private String getBrand() {  
  19.         return "Benz";  
  20.     }  
  21.  
  22. //宝马汽车  
  23. public class BaoMaCar extends AbstractCar {  
  24.     public BaoMaCar(IEngine engine) {  
  25.         super(engine);  
  26.     }  
  27.     @Override  
  28.     public void drive() {  
  29.         this.engine.start();  
  30.         System.out.println("Drive " + getBrand() + " car...");  
  31.     }  
  32.     private String getBrand() {  
  33.         return "BMW";  
  34.     }  

细心的小伙伴,可以发现不同子类BaoMaCar和BenzCar的drive方法,还是有相同代码,所以我们可以再扩展一个抽象子类,把drive方法推进去,如下: 

  1. public abstract class AbstractRefinedCar extends AbstractCar {  
  2.     public AbstractRefinedCar(IEngine engine) {  
  3.         super(engine);  
  4.     }  
  5.     @Override  
  6.     public void drive() {  
  7.         this.engine.start();  
  8.         System.out.println("Drive " + getBrand() + " car...");  
  9.     }  
  10.     abstract String getBrand();  
  11.  
  12. //宝马  
  13. public class BaoMaRefinedCar extends AbstractRefinedCar {  
  14.     public BaoMaRefinedCar(IEngine engine) {  
  15.         super(engine);  
  16.     }  
  17.     @Override  
  18.     String getBrand() {  
  19.         return  "BMW";  
  20.     }  

如果再添加一个新品牌,搞个子类,继承AbstractRefinedCar即可,如果新增一种启动引擎,也是搞个类实现IEngine接口即可

6. Shotgun Surgery(散弹式修改)

当你实现某个小功能时,你需要在很多不同的类做出小修改。这就是Shotgun Surgery(散弹式修改)。它跟发散式变化(Divergent Change) 的区别就是,它指的是同时对多个类进行单一的修改,发散式变化指在一个类中修改多处。反例如下: 

  1. public class DbAUtils {  
  2.     @Value("${db.mysql.url}")  
  3.     private String mysqlDbUrl;  
  4.     ...  
  5.  
  6. public class DbBUtils {  
  7.     @Value("${db.mysql.url}")  
  8.     private String mysqlDbUrl;  
  9.     ...  

多个类使用了db.mysql.url这个变量,如果将来需要切换mysql到别的数据库,如Oracle,那就需要修改多个类的这个变量!

如何优化呢?将各个修改点,集中到一起,抽象成一个新类。

    ★    可以使用 Move Method (搬移函数)和 Move Field (搬移字段)把所有需要修改的代码放进同一个类,如果没有合适的类,就去new一个。”

正例如下: 

  1. public class DbUtils {  
  2.     @Value("${db.mysql.url}")  
  3.     private String mysqlDbUrl;  
  4.     ...  

7. Feature Envy (依恋情节)

某个函数为了计算某个值,从另一个对象那里调用几乎半打的取值函数。通俗点讲,就是一个函数使用了大量其他类的成员,有人称之为红杏出墙的函数。反例如下: 

  1. public class User{  
  2.  private Phone phone;  
  3.   public User(Phone phone){  
  4.         this.phone = phone;  
  5.     }  
  6.     public void getFullPhoneNumber(Phone phone){  
  7.         System.out.println("areaCode:" + phone.getAreaCode());  
  8.         System.out.println("prefix:" + phone.getPrefix());  
  9.         System.out.println("number:" + phone.getNumber());  
  10.     }  

如何解决呢?在这种情况下,你可以考虑将这个方法移动到它使用的那个类中。例如,要将 getFullPhoneNumber()从 User 类移动到Phone类中,因为它调用了Phone类的很多方法。

8. Data Clumps(数据泥团)

数据项就像小孩子,喜欢成群结队地呆在一块。如果一些数据项总是一起出现的,并且一起出现更有意义的,就可以考虑,按数据的业务含义来封装成数据对象。反例如下: 

  1. public class User {  
  2.     private String firstName;  
  3.     private String lastName;  
  4.     private String province;  
  5.     private String city;  
  6.     private String area;  
  7.     private String street;  

正例: 

  1. public class User {  
  2.     private UserName username;  
  3.     private Adress adress;  
  4.  
  5. class UserName{  
  6.     private String firstName;  
  7.     private String lastName;  
  8.  
  9. class Address{  
  10.     private String province;  
  11.     private String city;  
  12.     private String area;  
  13.     private String street;  

9. Primitive Obsession (基本类型偏执)

多数编程环境都有两种数据类型,结构类型和基本类型。这里的基本类型,如果指Java语言的话,不仅仅包括那八大基本类型哈,也包括String等。如果是经常一起出现的基本类型,可以考虑把它们封装成对象。我个人觉得它有点像Data Clumps(数据泥团) 举个反例如下: 

  1. // 订单  
  2. public class Order {  
  3.     private String customName;  
  4.     private String address;  
  5.     private Integer orderId;  
  6.     private Integer price;  

正例: 

  1. // 订单类  
  2. public class Order {  
  3.     private Custom custom;  
  4.     private Integer orderId;  
  5.     private Integer price;  
  6.  
  7. // 把custom相关字段封装起来,在Order中引用Custom对象  
  8. public class Custom {  
  9.     private String name;  
  10.     private String address;  

当然,这里不是所有的基本类型,都建议封装成对象,有关联或者一起出现的,才这么建议哈。

10. Switch Statements (Switch 语句)

这里的Switch语句,不仅包括Switch相关的语句,也包括多层if...else的语句哈。很多时候,switch语句的问题就在于重复,如果你为它添加一个新的case语句,就必须找到所有的switch语句并且修改它们。

示例代码如下:   

  1. String medalType = "guest" 
  2.     if ("guest".equals(medalType)) {  
  3.         System.out.println("嘉宾勋章");  
  4.      } else if ("vip".equals(medalType)) {  
  5.         System.out.println("会员勋章");  
  6.     } else if ("guard".equals(medalType)) {  
  7.         System.out.println("守护勋章");  
  8.     }  
  9.     ... 

这种场景可以考虑使用多态优化: 

  1. //勋章接口  
  2. public interface IMedalService {  
  3.     void showMedal();  
  4.  
  5. //守护勋章策略实现类  
  6. public class GuardMedalServiceImpl implements IMedalService {  
  7.     @Override  
  8.     public void showMedal() {  
  9.         System.out.println("展示守护勋章");  
  10.     }  
  11.  
  12. //嘉宾勋章策略实现类  
  13. public class GuestMedalServiceImpl implements IMedalService {  
  14.     @Override  
  15.     public void showMedal() {  
  16.         System.out.println("嘉宾勋章");  
  17.     }  
  18.  
  19. //勋章服务工厂类  
  20. public class MedalServicesFactory {  
  21.     private static final Map<String, IMedalService> map = new HashMap<>();  
  22.     static {  
  23.         map.put("guard", new GuardMedalServiceImpl());  
  24.         map.put("vip", new VipMedalServiceImpl());  
  25.         map.put("guest", new GuestMedalServiceImpl());  
  26.     }  
  27.     public static IMedalService getMedalService(String medalType) {  
  28.         return map.get(medalType);  
  29.     }  

当然,多态只是优化的一个方案,一个方向。如果只是单一函数有些简单选择示例,并不建议动不动就使用动态,因为显得有点杀鸡使用牛刀了。

11.Parallel Inheritance Hierarchies( 平行继承体系)

平行继承体系 其实算是Shotgun Surgery的特殊情况啦。当你为A类的一个子类Ax,也必须为另一个类B相应的增加一个子类Bx。

解决方法:遇到这种情况,就要消除两个继承体系之间的引用,有一个类是可以去掉继承关系的。

12. Lazy Class (冗赘类)

把这些不再重要的类里面的逻辑,合并到相关类,删掉旧的。一个比较常见的场景就是,假设系统已经有日期工具类DateUtils,有些小伙伴在开发中,需要用到日期转化等,不管三七二十一,又自己实现一个新的日期工具类。

13. Speculative Generality(夸夸其谈未来性)

尽量避免过度设计的代码。例如:

只有一个if else,那就不需要班门弄斧使用多态;

如果某个抽象类没有什么太大的作用,就运用Collapse Hierarchy(折叠继承体系)

如果函数的某些参数没用上,就移除。

14. Temporary Field(令人迷惑的临时字段)

某个实例变量仅为某种特定情况而定而设,这样的代码就让人不易理解,我们称之为 Temporary Field(令人迷惑的临时字段)。反例如下: 

  1. public class PhoneAccount {  
  2.     private double excessMinutesCharge;  
  3.     private static final double RATE = 8.0;  
  4.     public double computeBill(int minutesUsed, int includedMinutes) {  
  5.         excessMinutesCharge = 0.0;  
  6.         int excessMinutes = minutesUsed - includedMinutes;  
  7.         if (excessMinutes >= 1) {  
  8.             excessMinutesexcessMinutesCharge = excessMinutes * RATE;  
  9.         }  
  10.         return excessMinutesCharge;  
  11.     }  
  12.     public double chargeForExcessMinutes(int minutesUsed, int includedMinutes) {  
  13.         computeBill(minutesUsed, includedMinutes);  
  14.         return excessMinutesCharge;  
  15.     }  

思考一下,临时字段excessMinutesCharge是否多余呢?

15. Message Chains (过度耦合的消息链)

当你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象...这就是消息链。实际代码中,你看到的可能是一长串getThis()或一长串临时变量。反例如下:

  1. A.getB().getC().getD().getTianLuoBoy().getData(); 

A想要获取需要的数据时,必须要知道B,又必须知道C,又必须知道D...其实A需要知道得太多啦,回头想下封装性,嘻嘻。其实可以通过拆函数或者移动函数解决,比如由B作为代理,搞个函数直接返回A需要数据。

16. Middle Man (中间人)

对象的基本特征之一就是封装,即对外部世界隐藏其内部细节。封装往往伴随委托,过度运用委托就不好:某个类接口有一半的函数都委托给其他类。可以使用Remove Middle Man优化。反例如下: 

  1. A.B.getC(){  
  2.    return C.getC();  

其实,A可以直接通过C去获取C,而不需要通过B去获取。

17. Inappropriate Intimacy(狎昵关系)

如果两个类过于亲密,过分狎昵,你中有我,我中有你,两个类彼此使用对方的私有的东西,就是一种坏代码味道。我们称之为Inappropriate Intimacy(狎昵关系)

建议尽量把有关联的方法或属性抽离出来,放到公共类,以减少关联。

18. Alternative Classes with Different Interfaces (异曲同工的类)

A类的接口a,和B类的接口b,做的的是相同一件事,或者类似的事情。我们就把A和B叫做异曲同工的类。

可以通过重命名,移动函数,或抽象子类等方式优化

19. Incomplete Library Class (不完美的类库)

大多数对象只要够用就好,如果类库构造得不够好,我们不可能修改其中的类使它完成我们希望完成的工作。可以酱紫:包一层函数或包成新的类。

20. Data Class (纯数据类)

什么是Data Class? 它们拥有一些字段,以及用于访问(读写)这些字段的函数。这些类很简单,仅有公共成员变量,或简单操作的函数。

如何优化呢?将相关操作封装进去,减少public成员变量。比如:

如果拥有public字段-> Encapsulate Field

如果这些类内含容器类的字段,应该检查它们是不是得到了恰当地封装-> Encapsulate Collection封装起来

对于不该被其他类修改的字段-> Remove Setting Method->找出取值/设置函数被其他类运用的地点-> Move Method 把这些调用行为搬移到Data Class来。如果无法搬移整个函数,就运用Extract Method产生一个可被搬移的函数->Hide Method把这些取值/设置函数隐藏起来。

21. Refused Bequest (被拒绝的馈赠)

子类应该继承父类的数据和函数。子类继承得到所有函数和数据,却只使用了几个,那就是继承体系设计错误,需要优化。

    需要为这个子类新建一个兄弟类->Push Down Method和Push Down Field把所有用不到的函数下推给兄弟类,这样一来,超类就只持有所有子类共享的东西。所有超类都应该是抽象的。

    如果子类复用了超类的实现,又不愿意支持超类的接口,可以不以为然。但是不能胡乱修改继承体系->Replace Inheritance with Delegation(用委派替换继承).

22. Comments (过多的注释)

这个点不是说代码不建议写注释哦,而是,建议大家避免用注释解释代码,避免过多的注释。这些都是常见注释的坏味道:

多余的解释

日志式注释

用注释解释变量等

 ...

如何优化呢?

方法函数、变量的命名要规范、浅显易懂、避免用注释解释代码。

关键、复杂的业务,使用清晰、简明的注释

23. 神奇命名

方法函数、变量、类名、模块等,都需要简单明了,浅显易懂。避免靠自己主观意识瞎起名字。

反例: 

  1. boolean test = chenkParamResult(req); 

正例: 

  1. boolean isParamPass = chenkParamResult(req); 

24. 神奇魔法数

日常开发中,经常会遇到这种代码: 

  1. if(userType==1){  
  2.    //doSth1  
  3. }else If( userType ==2){  
  4.    //doSth2  
  5.  
  6. ... 

代码中的这个1和2都表示什么意思呢?再比如setStatus(1)中的1又表示什么意思呢?看到类似坏代码,可以这两种方式优化:

新建个常量类,把一些常量放进去,统一管理,并且写好注释;

建一个枚举类,把相关的魔法数字放到一起管理。

25. 混乱的代码层次调用

我们代码一般会分dao层、service层和controller层。

dao层主要做数据持久层的工作,与数据库打交道。

service层主要负责业务逻辑处理。

controller层负责具体的业务模块流程的控制。

所以一般就是controller调用service,service调dao。如果你在代码看到controller直接调用dao,那可以考虑是否优化啦。反例如下: 

  1. @RestController("user")  
  2. public class UserController {  
  3.     Autowired  
  4.     private UserDao userDao;  
  5.     @RequestMapping("/queryUserInfo")  
  6.     public String queryUserInfo(String userName) {  
  7.         return userDao.selectByUserName(userName);  
  8.     }  
  9.  

 

责任编辑:庞桂玉 来源: Java编程
相关推荐

2019-10-11 09:07:46

Java代码对象

2020-06-12 08:21:58

JavaScript代码开发

2012-07-13 09:35:58

PHP

2012-07-13 09:38:15

项目代码

2015-07-29 13:22:40

.NET代码

2012-07-19 10:42:17

项目

2018-08-24 21:25:02

编程语言代码重构GitHub

2022-01-26 10:29:24

微服务循环依赖代码

2022-05-07 10:01:20

好代码坏代码

2010-04-12 17:47:01

Oracle多表查询

2020-12-01 08:36:10

代码程序员函数

2015-09-15 08:30:23

Android代码优化

2009-11-16 10:57:51

PHP上传文件代码

2021-02-16 16:43:21

工具性能调优

2015-11-05 09:02:05

Java代码性能优化

2019-09-29 16:17:25

Java代码性能编程语言

2009-09-08 17:20:01

C#排序算法

2010-04-26 16:30:00

DNS负载均衡

2020-04-26 10:01:14

编程学习技术

2024-04-22 13:42:32

大型语言模型人工智能
点赞
收藏

51CTO技术栈公众号