• 大话重构连载16:超级大函数


    事情总是这种:当我们对一个遗留系统一忍再忍,再忍,忍,还要忍……最终积攒到某一天,实在忍无可忍了,拍案而起,不能再忍了,重构!

    事情就这样发生了。

    然而,在这时你突然发现,重构的工作千头万绪,真不知从何開始。堆积如山的问题此起彼伏,期望改动的设计思绪万千。

    这里有个想法,那里有个思路,什么都想做,却什么都做不了,真是脑子里一团乱麻。

    这时候,没有一个合理的步骤。清晰的计划,瞎干蛮干是十分危急的,它会为你的重构带来不可预期的未来。

    无数次的经验告诉我,不论是什么系统。採用什么架构,从分解大函数開始,肯定没有错。

    大函数,就是那些业务逻辑特别复杂、程序代码特别多、一提起就叫人头疼不已的超级方法。超级大函数,非常难让人读懂,更难于维护与变更,毫无疑问是软件退化的重灾区。它起初可能并不复杂,也逻辑清晰、易于读懂,但随着业务逻辑的一次次变更。不停地往里面加入代码,再加上一些不合理的设计,经过天长日久,变得越来越臃肿,超级大函数就这样产生了。

    超级大函数的产生是有它内在的客观原因的。怎么这么说呢?前面我们谈过。软件发展的客观规律就是业务逻辑越来越复杂。随着业务逻辑越来越复杂,正确的办法就是适时地重构和优化我们的代码。但非常遗憾地是。差点儿非常少有人认识到这一点。

    这种结果就是,随着业务逻辑越来越复杂,人们总是就着原有的程序结构不停地往里面加入新的代码。原有的清晰而简单的程序。随着新代码的不断加入,開始变得越来越复杂而难懂了。

    正由于如此,在大多数软件企业的遗留系统中,超级大函数就变成了一种通病。让我们用HelloWorld为例来演变一番它的历程吧。

    如前面第三章所述,最开初的HelloWorld程序是这种:

    /**
     * The Refactoring's hello-world program
     * @author fangang
     */
    public class HelloWorld {
             /**
              * Say hello to everyone
              * @param now
              * @param user
              * @return the words what to say
              */
             public String sayHello(Date now, Stringuser){
                       //Get current hour of day
                       Calendar calendar =Calendar.getInstance();
                       calendar.setTime(now);
                       int hour =calendar.get(Calendar.HOUR_OF_DAY);
                      
                       //Get the right words to sayhello
                       String words = null;
                       if(hour>=6 &&hour<12){
                                words = "Goodmorning!";
                       }else if(hour>=12&& hour<19){
                                words = "Goodafternoon!";
                       }else{
                                words = "Goodnight!";
                       }
                       words = "Hi,"+user+". "+words;
                       return words;
             }
    }

    了了数十行代码,简单明了。随后就開始变更了,首先是关于时间的问候变得复杂了,加入了一些特殊的节日的问题,如新年问候“Happy new year! ”、情人节问候“Happyvalentine’s day! ”、三八妇女节问候“Happy women’sday! ”。等等。

    同一时候。对一天中的问候也变得更加精细。

    对于这种需求,IT攻城狮们敲着键盘就開始改写了:

    /**
     * The Refactoring's hello-world program
     * @author fangang
     */
    public class HelloWorld {
             /**
              * Say hello to everyone
              * @param now
              * @param user
              * @return the words what to say
              */
             public String sayHello(Date now, Stringuser){
                       //Get current month, date andhour.
                       Calendar calendar =Calendar.getInstance();
                       calendar.setTime(now);
                       int hour =calendar.get(Calendar.HOUR_OF_DAY);
                       int month =calendar.get(Calendar.MONTH);
                       int day = calendar.get(Calendar.DAY_OF_MONTH);
                      
                       //Get the right words to sayhello
                       String words = null;
                       if(month==1 &&day==1){
                                words = "Happynew year!";
                       }else if(month==1 &&day==14){
                                words = "Happyvalentine's day!";
                       }else if(month==3 &&day==8){
                                words = "Happywomen's day!";
                       }else if(month==5 &&day==1){
                                words = "HappyLabor day!";
                      
                       ……
                      
                       }else if(hour>=6&& hour<12){
                                words = "Goodmorning!";
                       }else if(hour==12){
                                words = "Goodnoon!";
                       }else if(hour>=12&& hour<19){
                                words = "Good afternoon!";
                       }else{
                                words = "Goodnight!";
                       }
                       words = "Hi,"+user+". "+words;
                       return words;
             }
    }

    代码量開始翻倍。接着,客户要求全部的用户信息应当来源于数据库的用户表,同一时候设计了问候语规则表,全部关于时间的问候都应来源于对该表的查询。

    这时我们继续膨胀sayHello()这种方法:

    /**
     * The Refactoring's hello-world program
     * @author fangang
     */
    public class HelloWorld {
             /**
              * Say hello to everyone
              * @param now
              * @param user
              * @return the words what to say
              */
             public String sayHello(Date now, longuserId){
                       //Get database connection.
                       try {
                                Class.forName("oracle.jdbc.driver.OracleDriver");
                       } catch(ClassNotFoundException e1) {
                                throw newRuntimeException("No found JDBC driver");
                       }
                       String url ="jdbc:oracle:thin:@localhost:1521:helloworld";
                       String username ="test";
                       String password ="testpwd";
                       Connection connection;
                       try {
                                connection =DriverManager.getConnection(url,username,password);
                       } catch (SQLException e1) {
                                throw newRuntimeException("Connect database failed!");
                       }
                      
                       //Get current month, date andhour.
                       Calendar calendar =Calendar.getInstance();
                       calendar.setTime(now);
                       int hour =calendar.get(Calendar.HOUR_OF_DAY);
                       int month =calendar.get(Calendar.MONTH);
                       int day =calendar.get(Calendar.DAY_OF_MONTH);
                      
                       //Get the right words to sayhello
                       String words = null;
                       String greetingRuleSql =
                                "select words,month, day, hourLower, hourUpper from greeting_rules";
                       try {
                                PreparedStatementstatement =
                                         connection.prepareStatement(greetingRuleSql);
                                ResultSet resultSet= statement.executeQuery();
                                while(!resultSet.isLast()){
                                         intmonthOfRule = resultSet.getInt("month");
                                         intdayOfRule = resultSet.getInt("day");
                                         if(month==monthOfRule&& day==dayOfRule){
                                                   words= resultSet.getString("words");
                                                   break;
                                         }
                                         inthourLower = resultSet.getInt("hourLower");
                                         inthourUpper = resultSet.getInt("hourUpper");
                                         if(hour>=hourLower&& hour<hourUpper){
                                                   words= resultSet.getString("words");
                                                   break;
                                         }
                                }
                                if(words==null)
                                throw new RuntimeException("Errorwhen searching greeting rules.");
                       } catch (SQLException e1) {
                                throw newRuntimeException("Error when getting greeting rules.");
                       }
                      
                       //Get user's name
                       String user = "";
                       String userSql = "selectname from rms_user where user_id=?";
                       try {
                                PreparedStatementstatement = connection.prepareStatement(userSql);
                                statement.setLong(1,userId);
                                ResultSet resultSet= statement.executeQuery();
                                user =resultSet.getString(1);
                       } catch (SQLException e) {
                                throw new RuntimeException("Errorwhen getting user's name.");
                       }
                      
                       words = "Hi,"+user+". "+words;
                       return words;
             }
    }

    这是一个十分简单的演示样例,但我们能够看到它已经由短短十来行膨胀成了60多行,膨胀了4倍之多。在最后这个版本号中。sayHello()既要负责连接数据库、查询数据,又要获得当前时间的月份、日期与小时。还要完毕对应的业务逻辑的推断。使程序变得相当复杂。

    我们能够继续想象,假设继续提出新的需求。比方支持多语言、支持多数据库,程序质量将继续下滑。直到我们无法忍受。

    解决超级大函数问题最有效的办法就是分解,依照功能一步一步分解。还原其应有的优化结构。在这个过程中我们经常使用的重构方法叫“抽取方法(ExtractMethod)”。重构是一个探索的过程,由于我们总是起初对要重构的系统并不了解,没有设计文档(即使有。对不正确还是一说呢)。没有熟悉系统的人(哪怕仅仅是在不明确的时候问一问)。你,仅仅是接到任务才開始接手这个系统,你对这个系统的了解简直就是一猫黑。而这时。抽取方法是我们開始这样的探索,了解这个系统最有效的工具,它往往是这样进行的:

    当我们在阅读一段大函数时,我们能够自觉不自觉地为这些代码分段,为一段功能相对独立的代码编写凝视。有时我们可能还须要调整代码的先后顺序,将一些有很多其它关联的代码放在一起。如将变量的声明与变量真正使用的代码放在一起。或者将有明显前后关系的代码放在一起。

    这种调整是一个好的开端。由于它让我们的代码開始变得有序。開始变得可读。

    随后,我们就能够使用“抽取方法”了。将被我们分段、加上凝视的代码从原函数中抽取出来,放在另外一个独立的函数中,为这个函数取个易懂的名称——这是一个非常重要的好习惯。我经常为了给一个函数取一个正确的名字而思索非常长时间,甚至改动好几次。不要觉得这是在浪费时间。它也是优化代码重要的一个环节。

    但起初我们对这段代码的理解可能不那么深,因此我们往往选择用结果变量为其命名。随着我们对这段代码理解的深入。能够运用重构中的“重命名方法(RenameMethod)”,依据其代码意图又一次为其命名(很多开发工具,如eclipse,都支持该重构方法,它们使你在重命名的同一时候,同步改动了全部对该方法的调用)。经过这样对代码段的抽取,原代码在这里就变成了对这个新函数的调用。

    举一个样例吧。这时一段真实的遗留系统,原有程序是这样写的:

             ......
             intiCtbz = -1;
             ElecObjelecObj = null;
             //获取办理时间
             intiCzyf = Integer.valueOf(Stream[9]).intValue();
             StringczMonth = String.valueOf(iCzyf % 20).toString().trim();
             StringczYear = String.valueOf( (iCzyf - (iCzyf % 20)) / 20 + 2000).
                          toString();
             if(czMonth.length() == 1){
               czMonth = "0" + czMonth;
             }
             longlBlsj = Long.valueOf(czYear + czMonth).longValue();
             Stringdqyear = sysDate.toString().substring(0, 4);
             ......
    这段代码写得并不好,有很多须要我们优化的地方。但记住“小步快跑”原则,此时不是解决其他问题的时候,如今我们首先是运用抽取方法优化程序结构。

    经过抽取以后。将以上加粗的部分改为了这样:

             ......
             intiCtbz = -1;
             ElecObjelecObj = null;
             //获取办理时间
             intiCzyf = Integer.valueOf(Stream[9]).intValue();
             longlBlsj = getBlsj(iCzyf);
             Stringdqyear = sysDate.toString().substring(0, 4);
             ......

    加粗的部分就是被改写的内容,同一时候将抽取的内容放进了一个独立的函数:

     
    /**
    *@param iCzyf
    *@return 获取办理时间
    */
    private long getBlsj(int iCzyf) {
             String czMonth = String.valueOf(iCzyf %20).toString().trim();
             String czYear = String.valueOf( (iCzyf- (iCzyf % 20)) / 20 + 2000).
                       toString();
             if (czMonth.length() == 1){
                 czMonth = "0" + czMonth;
             }
             return Long.valueOf(czYear +czMonth).longValue();
    }
     /**
    *@param iCzyf
    *@return 获取办理时间
    */
    private long getBlsj(int iCzyf) {
             String czMonth = String.valueOf(iCzyf %20).toString().trim();
             String czYear = String.valueOf( (iCzyf- (iCzyf % 20)) / 20 + 2000).
                       toString();
             if (czMonth.length() == 1){
                 czMonth = "0" + czMonth;
             }
             return Long.valueOf(czYear +czMonth).longValue();
    }

    在给这个新创建的函数命名时。我们对这段代码的理解并不深刻。在原函数中。这段代码运行的结果是获得了lBlsj这个结果变量,即获得了办理时间。为此,我们先为该函数命名为getBlsj这个函数名。但在兴许的工作中,我们逐渐理解到,它不仅能够获取办理时间,还能够获取非常多时间。它实质是将时间由一种表示方式转换成还有一种表示方式,因此我们运用开发工具的重命名功能,将该函数命名为transformDate,其他调用它的代码也随之改动了。

    重命名后的函数就不再只运用在此处获取办理时间。还应用在其他业务代码的处理中。

    抽取方法可大可小,你能够将一段数百行的代码抽取走,也可能仅仅抽取了数行。但不论如何,被抽取走的代码一定是功能内聚的,也就是说它们运行的是一个说得清道得明、清晰明白的功能。

    同一时候,被抽取走的代码一定是运行的一个清晰的功能,而不是多个。它可大可小,大或许还能分解为多个功能。但至少在逻辑上,这些功能是这个大的功能的组成部分。

    抽取方法是一个探索的过程,最关键是那个红线划在哪里,即抽代替码的范围。

    多一行也对。少一行也对。关键在于我们抽取出来的这个函数,它的功能我们是如何定义的。

    公说公有理。婆说婆有理,没有一个定论。所以方法的抽取经常是反重复复。開始我们依照一个思路抽取出来,后来想想认为不正确,因此又放回原函数。又一次划分,又一次抽取,重复多次。

    有一次,我在重构一个Servlet的时候,抽取出来的函数是在运行一大段业务逻辑操作。

    但在完毕了一系列业务操作以后,原程序要将返回值转换成二进制代码写入response中。返回前端。起初,我将整个这一段都抽取出来。但令人非常别扭的是,传參的时候必需要把response传进去,这使得业务逻辑与Web应用环境耦合,不利于日后的优化。编写自己主动化測试。随后我还原了这段代码,又一次进行抽取,将写response的部分留在了原函数中。而将红线画在了完毕业务逻辑操作之后,写入response之前。新重构的函数得以与web应用解耦。为后面的进一步优化做好了准备。

    重构的过程,是考验开发者能力的过程,需要大家重复练习与钻研。

    另外,抽取方法就像核裂变,開始由一个函数裂变为几个函数。分解出来的函数又裂变为另外几个函数。不断这样往复下去。同一时候,抽取方法总是在一个类中发生裂变。而当这个类分解出来的方法达到一定程度以后,随之而来的就是类的裂变,由一个类分解成多个类,分解出来的类再分解……类的分解我们採用的是还有一个方法——“抽取类(Extract Class)”,我们将在后面讲述。

    重构是一系列的等量变换。抽取方法是这些等量变换中最典型的样例。

    将一段代码从原函数中抽取出来,代码依旧是那些代码。仅仅是程序结构发生了变换。

    正由于如此,才干保证我们的重构过程的安全可靠。


    大话重构连载首页:http://blog.csdn.net/mooodo/article/details/32083021

    特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!


  • 相关阅读:
    Tomcat线程参数
    CDH平台规划注意事项
    python 不同数据类型的序列化
    Python 中__new__方法详解及使用
    线程生命周期
    如何在JAVA中每隔一段时间执行一段程序
    手动开启是事务提交回滚
    MySQL数据类型转换函数CAST与CONVERT的用法
    mybatis插入是返回主键id
    解决dubbo注册zookepper服务IP乱入问题的三种方式
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/5392282.html
Copyright © 2020-2023  润新知