前言
什么样的代码是好代码呢?好的代码应该命名规范、可读性强、扩展性强、健壮性……而不好的代码又有哪些典型特征呢?这25种代码坏味道大家要注意啦
- 公众号:捡田螺的小男孩
- github地址
1. Duplicated Code (重复代码)
重复代码就是不同地点,有着相同的程序结构。一般是因为需求迭代比较快,开发小伙伴担心影响已有功能,就复制粘贴造成的。重复代码很难维护的,如果你要修改其中一段的代码逻辑,就需要修改多次,很可能出现遗漏的情况。
如何优化重复代码呢?分三种情况讨论:
- 同一个类的两个函数含有相同的表达式
1 | csharp复制代码class A { |
优化手段:可以使用Extract Method(提取公共函数) 抽出重复的代码逻辑,组成一个公用的方法。
1 | csharp复制代码class A { |
- 两个互为兄弟的子类内含相同的表达式
1 | csharp复制代码class A extend C { |
优化手段:对两个类都使用Extract Method(提取公共函数),然后把抽取出来的函数放到父类中。
1 | csharp复制代码class C { |
- 两个毫不相关的类出现重复代码
如果是两个毫不相关的类出现重复代码,可以使用Extract Class将重复代码提炼到一个类中。这个新类可以是一个普通类,也可以是一个工具类,看具体业务怎么划分吧。
2 .Long Method (长函数)
长函数是指一个函数方法几百行甚至上千行,可读性大大降低,不便于理解。反例如下:
1 | csharp复制代码public class Test { |
可以使用Extract Method
,抽取功能单一的代码段,组成命名清晰的小函数,去解决长函数问题,正例如下:
1 | scss复制代码public class Test { |
3. Large Class (过大的类)
一个类做太多事情,维护了太多功能,可读性变差,性能也会下降。举个例子,订单相关的功能你放到一个类A里面,商品库存相关的也放在类A里面,积分相关的还放在类A里面...反例如下:
1 | csharp复制代码Class A{ |
试想一下,乱七八糟的代码块都往一个类里面塞,还谈啥可读性。应该按单一职责,使用Extract Class
把代码划分开,正例如下:
1 | csharp复制代码Class Order{ |
4. Long Parameter List (过长参数列)
方法参数数量过多的话,可读性很差。如果有多个重载方法,参数很多的话,有时候你都不知道调哪个呢。并且,如果参数很多,做新老接口兼容处理也比较麻烦。
1 | arduino复制代码public void getUserInfo(String name,String age,String sex,String mobile){ |
如何解决过长参数列问题呢?将参数封装成结构或者类,比如我们将参数封装成一个DTO类,如下:
1 | arduino复制代码public void getUserInfo(UserInfoParamDTO userInfoParamDTO){ |
5. Divergent Change (发散式变化)
对程序进行维护时, 如果添加修改组件, 要同时修改一个类中的多个方法, 那么这就是 Divergent Change。举个汽车的例子,某个汽车厂商生产三种品牌的汽车:BMW、Benz和LaoSiLaiSi,每种品牌又可以选择燃油、纯电和混合动力。反例如下:
1 | csharp复制代码/** |
如果新增一种品牌新能源电车,然后它的启动引擎是核动力呢,那么就需要修改Car类的start
和getBrand
方法啦,这就是代码坏味道:Divergent Change (发散式变化)。
如何优化呢?一句话总结:拆分类,将总是一起变化的东西放到一块。
- 运用提炼类(Extract Class) 拆分类的行为。
- 如果不同的类有相同的行为,提炼超类(Extract Superclass) 和 提炼子类(Extract Subclass)。
正例如下:
因为Engine是独立变化的,所以提取一个Engine接口,如果新加一个启动引擎,多一个实现类即可。如下:
1 | csharp复制代码//IEngine |
因为drive
方法依赖于Car,IEngine,getBand
方法;getBand
方法是变化的,也跟Car是有关联的,所以可以搞个抽象Car的类,每个品牌汽车继承于它即可,如下
1 | scala复制代码public abstract class AbstractCar { |
细心的小伙伴,可以发现不同子类BaoMaCar和BenzCar的drive
方法,还是有相同代码,所以我们可以再扩展一个抽象子类,把drive
方法推进去,如下:
1 | scala复制代码public abstract class AbstractRefinedCar extends AbstractCar { |
如果再添加一个新品牌,搞个子类,继承AbstractRefinedCar
即可,如果新增一种启动引擎,也是搞个类实现IEngine
接口即可
6. Shotgun Surgery(散弹式修改)
当你实现某个小功能时,你需要在很多不同的类做出小修改。这就是Shotgun Surgery(散弹式修改)。它跟发散式变化(Divergent Change) 的区别就是,它指的是同时对多个类进行单一的修改,发散式变化指在一个类中修改多处。反例如下:
1 | kotlin复制代码public class DbAUtils { |
多个类使用了db.mysql.url
这个变量,如果将来需要切换mysql
到别的数据库,如Oracle
,那就需要修改多个类的这个变量!
如何优化呢?将各个修改点,集中到一起,抽象成一个新类。
可以使用 Move Method (搬移函数)和 Move Field (搬移字段)把所有需要修改的代码放进同一个类,如果没有合适的类,就去new一个。
正例如下:
1 | kotlin复制代码public class DbUtils { |
7. Feature Envy (依恋情节)
某个函数为了计算某个值,从另一个对象那里调用几乎半打的取值函数。通俗点讲,就是一个函数使用了大量其他类的成员,有人称之为红杏出墙的函数。反例如下:
1 | csharp复制代码public class User{ |
如何解决呢?在这种情况下,你可以考虑将这个方法移动到它使用的那个类中。例如,要将 getFullPhoneNumber()
从 User
类移动到Phone
类中,因为它调用了Phone
类的很多方法。
8. Data Clumps(数据泥团)
数据项就像小孩子,喜欢成群结队地呆在一块。如果一些数据项总是一起出现的,并且一起出现更有意义的,就可以考虑,按数据的业务含义来封装成数据对象。反例如下:
1 | arduino复制代码public class User { |
正例:
1 | arduino复制代码public class User { |
9. Primitive Obsession (基本类型偏执)
多数编程环境都有两种数据类型,结构类型和基本类型。这里的基本类型,如果指Java语言的话,不仅仅包括那八大基本类型哈,也包括String等。如果是经常一起出现的基本类型,可以考虑把它们封装成对象。我个人觉得它有点像Data Clumps(数据泥团) 举个反例如下:
1 | vbnet复制代码// 订单 |
正例:
1 | vbnet复制代码// 订单类 |
当然,这里不是所有的基本类型,都建议封装成对象,有关联或者一起出现的,才这么建议哈。
10. Switch Statements (Switch 语句)
这里的Switch语句,不仅包括Switch
相关的语句,也包括多层if...else
的语句哈。很多时候,switch语句的问题就在于重复,如果你为它添加一个新的case语句,就必须找到所有的switch语句并且修改它们。
示例代码如下:
1 | csharp复制代码 String medalType = "guest"; |
这种场景可以考虑使用多态优化:
1 | typescript复制代码//勋章接口 |
当然,多态只是优化的一个方案,一个方向。如果只是单一函数有些简单选择示例,并不建议动不动就使用动态,因为显得有点杀鸡使用牛刀了。
11.Parallel Inheritance Hierarchies( 平行继承体系)
平行继承体系 其实算是Shotgun Surgery
的特殊情况啦。当你为A类的一个子类Ax,也必须为另一个类B相应的增加一个子类Bx。
解决方法:遇到这种情况,就要消除两个继承体系之间的引用,有一个类是可以去掉继承关系的。
12. Lazy Class (冗赘类)
把这些不再重要的类里面的逻辑,合并到相关类,删掉旧的。一个比较常见的场景就是,假设系统已经有日期工具类DateUtils
,有些小伙伴在开发中,需要用到日期转化等,不管三七二十一,又自己实现一个新的日期工具类。
13. Speculative Generality(夸夸其谈未来性)
尽量避免过度设计的代码。例如:
- 只有一个if else,那就不需要班门弄斧使用多态;
- 如果某个抽象类没有什么太大的作用,就运用
Collapse Hierarchy
(折叠继承体系)1
2
3
4
5
6
7* 如果函数的某些参数没用上,就移除。
### 14. Temporary Field(令人迷惑的临时字段)
某个实例变量仅为某种特定情况而定而设,这样的代码就让人不易理解,我们称之为 `Temporary Field(令人迷惑的临时字段)`。 反例如下:
arduino复制代码public class PhoneAccount {
private double excessMinutesCharge;
private static final double RATE = 8.0;
public double computeBill(int minutesUsed, int includedMinutes) {
excessMinutesCharge = 0.0;
int excessMinutes = minutesUsed - includedMinutes;
if (excessMinutes >= 1) {
excessMinutesCharge = excessMinutes * RATE;
}
return excessMinutesCharge;
}
public double chargeForExcessMinutes(int minutesUsed, int includedMinutes) {
computeBill(minutesUsed, includedMinutes);
return excessMinutesCharge;
}
}
1 |
|
scss复制代码A.getB().getC().getD().getTianLuoBoy().getData();
1 |
|
javascript复制代码A.B.getC(){
return C.getC();
}
1 |
|
ini复制代码boolean test = chenkParamResult(req);
1 |
|
ini复制代码boolean isParamPass = chenkParamResult(req);
1 |
|
scss复制代码if(userType==1){
//doSth1
}else If( userType ==2){
//doSth2
}
…
1 |
|
kotlin复制代码@RestController(“user”)
public class UserController {
Autowired
private UserDao userDao;
@RequestMapping("/queryUserInfo")
public String queryUserInfo(String userName) {
return userDao.selectByUserName(userName);
}
}
### 参考与感谢
* [软工实验:常见的代码坏味道以及重构举例](https://blog.csdn.net/weixin_45763536/article/details/106315969?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.vipsorttest&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.vipsorttest)
* [22种代码的坏味道,一句话概括](https://blog.csdn.net/windcao/article/details/25773219?utm_medium=distribute.pc_relevant_download.none-task-blog-2~default~BlogCommendFromBaidu~default-15.nonecase&depth_1-utm_source=distribute.pc_relevant_download.none-task-blog-2~default~BlogCommendFromBaidu~default-15.nonecas)
* [【重构】 代码的坏味道总结 Bad Smell](https://blog.csdn.net/shulianghan/article/details/20009689)
* [Code Smell](https://www.dazhuanlan.com/2019/12/26/5e04029e800c9/)
* 《重构改善既有代码的设计》
**本文转载自:** [掘金](https://juejin.cn/post/6962812178537644063)
*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*