代码重构(二)25种代码优化示例
前言
什么样的代码是好代码呢?好的代码应该命名规范、可读性强、扩展性强、健壮性......而不好的代码又有哪些典型特征呢?
1. Duplicated Code (重复代码)
重复代码就是不同地点,有着相同的程序结构。一般是因为需求迭代比较快,开发小伙伴担心影响已有功能,就复制粘贴造成的。重复代码很难维护的,如果要修改其中一段的代码逻辑,就需要修改多次,很可能出现遗漏的情况。
如何优化重复代码呢?分三种情况讨论:
- 同一个类的两个函数含有相同的表达式
class A {public void method1() {doSomething1doSomething2doSomething3}public void method2() {doSomething1doSomething2doSomething4} }
优化手段:可以使用Extract Method(提取公共函数)抽出重复的代码逻辑,组成一个公用的方法。
class A {public void method1() {commonMethod();doSomething3}public void method2() {commonMethod();doSomething4}public void commonMethod(){doSomething1doSomething2} }
- 两个互为兄弟的子类内含相同的表达式
class A extend C {public void method1() {doSomething1doSomething2doSomething3} }class B extend C {public void method1() {doSomething1doSomething2doSomething4} }
优化手段:对两个类都使用Extract Method(提取公共函数),然后把抽取出来的函数放到父类中。
class C {public void commonMethod(){doSomething1doSomething2} } class A extend C {public void method1() {commonMethod();doSomething3} }class B extend C {public void method1() {commonMethod();doSomething4} }
- 两个毫不相关的类出现重复代码
如果是两个毫不相关的类出现重复代码,可以使用Extract Class将重复代码提炼到一个类中。这个新类可以是一个普通类,也可以是一个工具类,看具体业务怎么划分吧。
2. Long Method (长函数)
长函数是指一个函数方法几百行甚至上千行,可读性大大降低,不便于理解。反例如下:
public class Test {private String name;private Vector<Order> orders = new Vector<Order>();public void printOwing() {//print bannerSystem.out.println("****************");System.out.println("*****customer Owes *****");System.out.println("****************");//calculate totalAmountEnumeration env = orders.elements();double totalAmount = 0.0;while (env.hasMoreElements()) {Order order = (Order) env.nextElement();totalAmount += order.getAmout();}//print detailsSystem.out.println("name:" + name);System.out.println("amount:" + totalAmount);......} }
可以使用Extract Method,抽取功能单一的代码段,组成命名清晰的小函数,去解决长函数问题,正例如下:
public class Test {private String name;private Vector<Order> orders = new Vector<Order>();public void printOwing() {//print bannerprintBanner();//calculate totalAmountdouble totalAmount = getTotalAmount();//print detailsprintDetail(totalAmount);}void printBanner(){System.out.println("****************");System.out.println("*****customer Owes *****");System.out.println("****************");}double getTotalAmount(){Enumeration env = orders.elements();double totalAmount = 0.0;while (env.hasMoreElements()) {Order order = (Order) env.nextElement();totalAmount += order.getAmout();}return totalAmount;}void printDetail(double totalAmount){System.out.println("name:" + name);System.out.println("amount:" + totalAmount);}}
3. Large Class (过大的类)
一个类做太多事情,维护了太多功能,可读性变差,性能也会下降。举个例子,订单相关的功能放到一个类A里面,商品库存相关的也放在类A里面,积分相关的还放在类A里面...反例如下:
Class A{public void printOrder(){System.out.println("订单");}public void printGoods(){System.out.println("商品");}public void printPoints(){System.out.println("积分");} }
试想一下,乱七八糟的代码块都往一个类里面塞,还谈啥可读性。应该按单一职责,使用Extract Class把代码划分开,正例如下:
Class Order{public void printOrder(){System.out.println("订单");} }Class Goods{public void printGoods(){System.out.println("商品");} }Class Points{ public void printPoints(){System.out.println("积分");}} }
4. Long Parameter List (过长参数列)
方法参数数量过多的话,可读性很差。如果有多个重载方法,参数很多的话,有时候都不知道调哪个呢。并且,如果参数很多,做新老接口兼容处理也比较麻烦。
public void getUserInfo(String name,String age,String sex,String mobile){// do something ... }
如何解决过长参数列问题呢?将参数封装成结构或者类,比如将参数封装成一个DTO类,如下:
public void getUserInfo(UserInfoParamDTO userInfoParamDTO){// do something ... }class UserInfoParamDTO{private String name;private String age; private String sex;private String mobile; }
5. Divergent Change (发散式变化)
对程序进行维护时,如果添加修改组件, 要同时修改一个类中的多个方法, 那么这就是 Divergent Change。举个汽车的例子,某个汽车厂商生产三种品牌的汽车:BMW、Benz和LaoSiLaiSi,每种品牌又可以选择燃油、纯电和混合动力。反例如下:
public class Car {private String name;void start(Engine engine) {if ("HybridEngine".equals(engine.getName())) {System.out.println("Start Hybrid Engine...");} else if ("GasolineEngine".equals(engine.getName())) {System.out.println("Start Gasoline Engine...");} else if ("ElectricEngine".equals(engine.getName())) {System.out.println("Start Electric Engine");}}void drive(Engine engine,Car car) {this.start(engine);System.out.println("Drive " + getBrand(car) + " car...");}String getBrand(Car car) {if ("Baoma".equals(car.getName())) {return "BMW";} else if ("BenChi".equals(car.getName())) {return "Benz";} else if ("LaoSiLaiSi".equals(car.getName())) {return "LaoSiLaiSi";}return null;}}
如果新增一种品牌新能源电车,然后它的启动引擎是核动力呢,那么就需要修改Car类的start
和getBrand
方法,这就是代码坏味道:Divergent Change (发散式变化)。
如何优化呢?一句话总结:拆分类,将总是一起变化的东西放到一块。
- 运用提炼类(Extract Class) 拆分类的行为。
- 如果不同的类有相同的行为,提炼超类(Extract Superclass) 和 提炼子类(Extract Subclass)。
正例如下:
因为Engine是独立变化的,所以提取一个Engine接口,如果新加一个启动引擎,多一个实现类即可。如下:
//IEngine public interface IEngine {void start(); }public class HybridEngineImpl implements IEngine { @Overridepublic void start() {System.out.println("Start Hybrid Engine...");} }
因为drive
方法依赖于Car
,IEngine
,getBand
方法;getBand
方法是变化的,也跟Car是有关联的,所以可以搞个抽象Car的类,每个品牌汽车继承于它即可,如下
public abstract class AbstractCar {protected IEngine engine;public AbstractCar(IEngine engine) {this.engine = engine;}public abstract void drive(); }//奔驰汽车 public class BenzCar extends AbstractCar {public BenzCar(IEngine engine) {super(engine);}@Overridepublic void drive() {this.engine.start();System.out.println("Drive " + getBrand() + " car...");}private String getBrand() {return "Benz";} }//宝马汽车 public class BaoMaCar extends AbstractCar {public BaoMaCar(IEngine engine) {super(engine);}@Overridepublic void drive() {this.engine.start();System.out.println("Drive " + getBrand() + " car...");}private String getBrand() {return "BMW";} }
细心的小伙伴,可以发现不同子类BaoMaCar和BenzCar的drive
方法,还是有相同代码,所以可以再扩展一个抽象子类,把drive
方法推进去,如下:
public abstract class AbstractRefinedCar extends AbstractCar {public AbstractRefinedCar(IEngine engine) {super(engine);}@Overridepublic void drive() {this.engine.start();System.out.println("Drive " + getBrand() + " car...");}abstract String getBrand(); }//宝马 public class BaoMaRefinedCar extends AbstractRefinedCar {public BaoMaRefinedCar(IEngine engine) {super(engine);}@OverrideString getBrand() {return "BMW";} }
如果再添加一个新品牌,搞个子类,继承AbstractRefinedCar
即可,如果新增一种启动引擎,也是搞个类实现IEngine
接口即可
6. Shotgun Surgery(散弹式修改)
当实现某个小功能时,需要在很多不同的类做出小修改。这就是Shotgun Surgery(散弹式修改)。它跟发散式变化(Divergent Change)的区别就是,它指的是同时对多个类进行单一的修改,发散式变化指在一个类中修改多处。反例如下:
public class DbAUtils {@Value("${db.mysql.url}")private String mysqlDbUrl;... }public class DbBUtils {@Value("${db.mysql.url}")private String mysqlDbUrl;... }
多个类使用了db.mysql.url这个变量,如果将来需要切换mysql到别的数据库,如Oracle,那就需要修改多个类的这个变量!
如何优化呢?将各个修改点,集中到一起,抽象成一个新类。
可以使用 Move Method (搬移函数)和 Move Field (搬移字段)把所有需要修改的代码放进同一个类,如果没有合适的类,就去new一个。
正例如下:
public class DbUtils {@Value("${db.mysql.url}")private String mysqlDbUrl;... }
7. Feature Envy (依恋情节)
某个函数为了计算某个值,从另一个对象那里调用几乎半打的取值函数。通俗点讲,就是一个函数使用了大量其他类的成员,有人称之为红杏出墙的函数。反例如下:
public class User{private Phone phone;public User(Phone phone){this.phone = phone;}public void getFullPhoneNumber(Phone phone){System.out.println("areaCode:" + phone.getAreaCode());System.out.println("prefix:" + phone.getPrefix());System.out.println("number:" + phone.getNumber());} }
如何解决呢?在这种情况下,可以考虑将这个方法移动到它使用的那个类中。例如,要将getFullPhoneNumber()
从User
类移动到Phone
类中,因为它调用了Phone
类的很多方法。
8. Data Clumps(数据泥团)
数据项就像小孩子,喜欢成群结队地呆在一块。如果一些数据项总是一起出现的,并且一起出现更有意义的,就可以考虑,按数据的业务含义来封装成数据对象。反例如下:
public class User {private String firstName;private String lastName;private String province;private String city;private String area;private String street; }
正例:
public class User {private UserName username;private Adress adress; }class UserName{private String firstName;private String lastName; } class Address{private String province;private String city;private String area;private String street; }
9. Primitive Obsession (基本类型偏执)
多数编程环境都有两种数据类型,结构类型和基本类型。这里的基本类型,如果指Java语言的话,不仅仅包括那八大基本类型哈,也包括String等。如果是经常一起出现的基本类型,可以考虑把它们封装成对象。它有点像Data Clumps(数据泥团)举个反例如下:
// 订单 public class Order {private String customName;private String address;private Integer orderId;private Integer price; }
正例:
// 订单类 public class Order {private Custom custom;private Integer orderId;private Integer price; } // 把custom相关字段封装起来,在Order中引用Custom对象 public class Custom {private String name;private String address; }
当然,这里不是所有的基本类型,都建议封装成对象,有关联或者一起出现的,才这么建议哈。
10. Switch Statements (Switch 语句)
这里的Switch语句,不仅包括Switch相关的语句,也包括多层if...else的语句哈。很多时候,switch语句的问题就在于重复,如果为它添加一个新的case语句,就必须找到所有的switch语句并且修改它们。
示例代码如下:
String medalType = "guest"; if ("guest".equals(medalType)) {System.out.println("嘉宾勋章"); } else if ("vip".equals(medalType)) {System.out.println("会员勋章"); } else if ("guard".equals(medalType)) {System.out.println("守护勋章"); } ...
这种场景可以考虑使用多态优化:
//勋章接口 public interface IMedalService {void showMedal(); }//守护勋章策略实现类 public class GuardMedalServiceImpl implements IMedalService {@Overridepublic void showMedal() {System.out.println("展示守护勋章");} } //嘉宾勋章策略实现类 public class GuestMedalServiceImpl implements IMedalService {@Overridepublic void showMedal() {System.out.println("嘉宾勋章");} }//勋章服务工厂类 public class MedalServicesFactory {private static final Map<String, IMedalService> map = new HashMap<>();static {map.put("guard", new GuardMedalServiceImpl());map.put("vip", new VipMedalServiceImpl());map.put("guest", new GuestMedalServiceImpl());}public static IMedalService getMedalService(String medalType) {return map.get(medalType);} }
当然,多态只是优化的一个方案,一个方向。如果只是单一函数有些简单选择示例,并不建议动不动就使用动态,因为显得有点杀鸡使用牛刀了。
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(令人迷惑的临时字段)。反例如下:
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;} }
思考一下,临时字段excessMinutesCharge是否多余呢?
15. Message Chains (过度耦合的消息链)
当看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象...这就是消息链。实际代码中,看到的可能是一长串getThis()
或一长串临时变量。反例如下:
A.getB().getC().getD().getTianLuoBoy().getData();
A想要获取需要的数据时,必须要知道B,又必须知道C,又必须知道D...其实A需要知道得太多,回头想下封装性,嘻嘻。其实可以通过拆函数或者移动函数解决,比如由B作为代理,搞个函数直接返回A需要数据。
16. Middle Man (中间人)
对象的基本特征之一就是封装,即对外部世界隐藏其内部细节。封装往往伴随委托,过度运用委托就不好:某个类接口有一半的函数都委托给其他类。可以使用Remove Middle Man优化。反例如下:
A.B.getC(){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. 神奇命名
方法函数、变量、类名、模块等,都需要简单明了,浅显易懂。避免靠自己主观意识瞎起名字。
反例:
boolean test = chenkParamResult(req);
正例:
boolean isParamPass = chenkParamResult(req);
24. 神奇魔法数
日常开发中,经常会遇到这种代码:
if(userType==1){//doSth1 }else If( userType ==2){//doSth2 } ...
代码中的这个1和2都表示什么意思呢?再比如setStatus(1)中的1又表示什么意思呢?看到类似坏代码,可以这两种方式优化:
- 新建个常量类,把一些常量放进去,统一管理,并且写好注释;
- 建一个枚举类,把相关的魔法数字放到一起管理。
25. 混乱的代码层次调用
代码一般会分dao层、service层和controller层。
- dao层主要做数据持久层的工作,与数据库打交道。
- service层主要负责业务逻辑处理。
- controller层负责具体的业务模块流程的控制。
所以一般就是controller调用service,service调dao。如果在代码看到controller直接调用dao,那可以考虑是否优化。反例如下:
@RestController("user") public class UserController {Autowiredprivate UserDao userDao;@RequestMapping("/queryUserInfo")public String queryUserInfo(String userName) {return userDao.selectByUserName(userName);} }
代码重构(二)25种代码优化示例相关推荐
- java代码重构工具_代码重构什么意思 Java代码重构的几种模式
指对软件代码做任何更动以增加可读性或者简化结构而不影响输出结果. 软件重构需要借助工具完成,重构工具能够修改代码同时修改所有引用该代码的地方.在极限编程的方法学中,重构需要单元测试来支持. 在软件工程 ...
- java代码重构的思路Java代码重构的几种模式
Java代码重构的几种模式 Java代码的重构模式主要有三种:重命名方法重构模式.引入解释性变量重构模式.以查询取代临时变量重构模式重命名方法重构模式建议执行如下的步骤来完成:1.建立一个具有新名称的 ...
- 代码重构(二):类重构规则
在上篇博客<代码重构(一):函数重构规则(Swift版)>中,详细的介绍了函数的重构规则,其中主要包括:Extract Method, Inline Method, Inline Temp ...
- 【JAVA】代码重构技巧
简介 重构是持续改进代码的基础.抵制重构将带来技术麻烦:忘记代码片段的功能.创建无法测试的代码等等. 而有了重构,使用单元测试.共享代码以及更可靠的无bug 的代码这些最佳实践就显得简单多了. 鉴于重 ...
- 25种代码坏味道总结+优化示例
前言 什么样的代码是好代码呢?好的代码应该命名规范.可读性强.扩展性强.健壮性......而不好的代码又有哪些典型特征呢?这25种代码坏味道大家要注意啦 1. Duplicated Code (重复代 ...
- Java代码设计模式讲解二十三种设计模式
设计模式 文章目录 设计模式 一.创造型设计模式 1.1 单例模式 1.1.1 饿汉式单例模式 1.1.2 懒汉式单例模式 (1)线程不安全的情况 (2)线程安全的情况 1. 实例化的方法上加sync ...
- 代码重构的方法和经验_关于烂代码优化重构的几点经验
是否已经读过前面两篇关于烂代码和好代码的文章? 这些让人抓狂的烂代码,你碰到几种? 什么才是好代码.高质量代码? 工作中,总会不可避免的接触到烂代码,就像之前说的,几乎没有程序员可以完全避免写出烂代码 ...
- html语言闪烁特效代码,css3 实现文字闪烁效果的三种方式示例代码
1.通过改变透明度来实现文字的渐变闪烁,效果图: 文字闪烁 星星之火可以燎原 .myclass{ letter-spacing:5px;/*字间距*/ color: red; font-weight: ...
- 2021-01-22学习记录 || 通过二维数组初始化窗体并进行代码重构
今天主要是通过二维数组将整个界面16个数字块展示出来,并为了下一步添加左移.右移功能创建子类MainFrame继承JFrame类并进行代码重构. 二维数组展示初始化界面 由于2048小游戏需要16个数 ...
最新文章
- 更改ubuntu的默认shell
- java enum 关联_Java:如果EnUM常量在本质上是静态的,那么它如何可以拥有与其相关联的构造函数和方法...
- Thinkphp 整合tcpdf
- 华为p50预装鸿蒙系统,华为P50系列将至,内部测试预装鸿蒙系统,还有4款重磅新品将发布...
- 无论是工作还是生活都要记住这些话
- C#中如何利用操作符重载和转换操作符
- 无线通信技术协议-Zigbee 3.0
- 接口带声音输出吗_智能化时代,你的功放跟得上科技的脚步吗?
- [设计模式][C++]单例模式
- 项目中遇到生产事故追踪---重复组单
- Windows Installer服务总是自动关闭导致无法安装在win10上安装英伟达显卡驱动的解决方案...
- 坐火车硬座20小时是怎样的体验?
- 毕业实习感想—软件测试
- 获取当前日期是今年的第几周
- tnsnames.ora配置未生效_汽车保险商业车险的生效时间是什么时候?商业车险的险种有哪些?我们应该怎么买?...
- Rosalind Java|Overlap Graphs
- Linux基础(2)/Linux 基本概念及操作
- 你还用60款下架侵害用户权益APP吗!!!!!
- Java基本类型转换
- 程序员月薪2万,被大学女友怒怼:毕业后年薪50万才是正常水平