从零开始学重构——重构的流程及基础重构⼿法
重构的流程
重构⼿法
正如上⼀次所讲的那样,重构有两个基本条件,⼀是要保持代码在重构前后的⾏为基本不变,⼆是整个过程是受控且尽可能少地产⽣错
误。尤其是对于第⼆点,产⽣了⼀系列的重构⼿法,每种重构⼿法都是⼀系列简单⽽机械的操作步骤,通过遵循这⼀系列的操作来实现代码
的结构性调整。因此,重构的整个过程就是不断运⽤不同重构⼿法的过程,是⼀个相对有章可循的流程。
重构⼿法有⼤有⼩,⼤的重构⼿法⼀般由若⼲⼩的基础重构组成,进⽽聚沙成塔实现对代码结构⼤幅度的调整。完整的重构列表请参见
《重构,改善既有代码的设计》⼀书。
例如,replaceconditionalwithpolymorphism这项复杂重构⼿法,就⾄少需要使⽤lfencapsulate,extractmethod,move
method,pulldownmethod这四种基础重构⼿法。因此在学习类级别的复杂重构⼿法前,需要先掌握⾏级别和⽅法级别的基础重构⼿法。
重构步骤
重构的宏观步骤⼀般有如下两种:⾃上⽽下式和⾃下⽽上式。
⾃上⽽下的重构在重构前,⼼中已经⼤致知道重构后的代码将会是什么形态,然后⾄上⽽下地将步骤分解出来,并使⽤相应的重构步骤
⼀⼀实现,最终达到重构后的形态。其流程为:
1.识别代码中的坏味道
2.运⽤设计原则,构思出修改后的⽬标状态
3.将⽬标状态分解为⼀或多项重构步骤
4.运⽤重构步骤
⾃下⽽上的重构则对重构后的代码没有⼀个完整⽽清晰的认识。⼀般⽽⾔,每种重构⼿法都有助于我们解决某种类型的代码坏味,⽽⾃
下⽽上的重构则针对每个发现的代码坏味直接运⽤对应的重构⼿法,直到没有明显的坏味,此时的代码即能⾃动满⾜某种设计模式。是⼀种
迭代的思路,也是所谓重构到模式的思路。其流程为:
1.识别代码中的坏味道
2.运⽤⼀项或多项重构步骤,消除坏味
3.重复1-2,直到没有明显坏味
在⼀般的情况下,这两种重构流程并不是互斥的,经常交错进⾏或互相包含。如先运⽤⾃上⽽下的⽅法识别出代码中的坏味,然后根据
设计原则重构到某个实现,再运⽤⾃下⽽上的⽅法重新寻找新的坏味,迭代重构。
基础重构⼿法
由于基础重构⼿法⽐较多,⽽且相对⽐较简单。因此先列出常⽤的基础重构⼿法和简单介绍,并在最后的实践案例中结合基础重构⼿法
来重构代码。
rename(重命名变量/⽅法/类)
坏味:含义不清的命名
说明:变量名应当体现出变量的作⽤和含义、⽅法名应当表现出⽅法的效果、类名也应提⽰类的职责和在继承体系中的位置。
操作⽅法:IntelliJShift+F6
reorder(调整语句顺序)
坏味:变量的申请和使⽤分离太远
说明:变量的使⽤应当尽可能离使⽤近⼀些,否则会扩⼤变量的作⽤域,在重构时也会产⽣困难。
操作⽅法:IntelliJAlt+Shift+↑↓针对⽆副作⽤的语句,直接调整语句位置。
splitfor/block(拆分for循环/代码块)
坏味:⼀个循环或代码块中同时操作了多个变量或执⾏了多个职责
说明:⼀个循环中若有太多变量要计算,不利于将此循环提取为单独⽅法。
操作⽅法:
1.将循环复制⼀次
2.每个循环中只保留⼀个变量的计算
3.将循环提取为独⽴⽅法
4.将所有循环的出现替换为⽅法的调⽤
guardclaus(卫语句)
坏味:过深的条件嵌套
说明:先判断跳出/过滤的条件,并直接return或continue,可除去多余的el嵌套深度。
操作⽅法:
iJ在if语句上Alt+Enter,选择invertif,可倒转if和el语句
iJ在el语句上Alt+Enter,选择removeredundantel
extractvariable(提取变量)
坏味:单条语句过长,含义不清
说明:将部分语句提取出变量,并为变量起⼀个能够解释变量含义的名称来替代注释
操作说明:IntelliJCtrl+Alt+V
extractmethod(提取⽅法)
坏味:单个⽅法过长,含义不清
说明:将做同⼀件事的代码提取出⽅法(⼀般为计算某个变量,或进⾏单个复杂操作),并为⽅法起⼀个能够解释”这件事”的名称来
替代注释
操作说明:IntelliJCtrl+Alt+M,需要考虑返回和参数的列表,返回不能超过1个变量
inlinemethod(内联⽅法)
坏味:⽅法只有⼀⾏代码,且内容本⾝已经很明确(多⾏也可以,但若原⽅法有返回值,则会⽐较复杂,不推荐)
说明:⽅法的作⽤是聚合操作并提供注释信息,若⽅法内容已经明确,则⽅法本⾝就起不到作⽤,反⽽增加复杂度
操作说明:将⽅法内容复制后,替换⽅法调⽤的部分。再删除⽅法本⾝
addparameter(⽅法增加参数)
坏味:⽅法主体只有部分变量不同
说明:可以提取变化的部分成为参数,从⽽合并两个相似的⽅法
操作说明:
1.将变量的部分提取为变量,并提到⽅法的最开始处
2.将⽅法剩余的内容提取为⼀个新的⽅法,新⽅法会含有新的参数
3.将原来的⽼⽅法内联
案例实践
重构前
代码中的坏味有:1.过长的⽅法,超过了20⾏或⼀屏,2.变量的命名含义不清,读者⽆法理解channelColumnClauTemp,
channelColumnClau以及它们之间的关系,-el中存在重复代码。
下来我们就来使⽤⼀系列基础重构⼿法来整理这段代码。
调整变量申明位置
仔细观察,发现channelColumnClauTemp变量只在if语句中使⽤,因此将channelColumnClauTemp变量的申请放到if中去:
privateSet
publicStringgenerateSql(){
StringchannelColumnClauTemp=(channelColumns,",","","");
StringchannelColumnClau;
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
channelColumnClau=(columns,",","","");
StringchannelColumnsReviewTemp="";
StringchannelColumnsReview="";
if(!y()){
channelColumnsReviewTemp=channelColumnClauTemp+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
channelColumnsReview=channelColumnClau+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
channelColumnsReviewTemp=idTypeColumn+",batch_id";
channelColumnsReview=idTypeColumn+",batch_id";
}
StringBuffervsql=newStringBuffer();
("inrtinto").append(_SCHEMA).append(".").append(tableName)
.append("(").append(channelColumnsReview).append(")")
.append("lectdistinct").append(e(ME_COLUMN,"isnull("+ME_COLUM
.append("from").append(_SCHEMA).append(".").append(sourceTableName).append(";n");
returnreviewTempTableSql+ng();
}
同样,channelColumnClau也只在if块中使⽤,但其中还涉及了columns及for循环部分,也⼀并移动到if块中:
重命名变量
仔细观察channelColumnsReviewTemp和channelColumnsReview两个变量的使⽤场景,发现它们是所拼接的sql语句的lect
xxx和inrt(yyy)这两个部分,因此实际上是源表的列名和⽬标表的列名。从⽽将channelColumnsReviewTemp命名为
sourceColumnsStr,将channelColumnsReview命名为targetColumnsStr。
同样,观察channelColumnClauTemp和channelColumnClau,它们分别⽤于计算channelColumnsReviewTemp和
channelColumnsReview,因此对应的命名为sourceColumnsWithoutBatchId和targetColumnsWithoutBatchId:
if(!y()){
StringchannelColumnClauTemp=(channelColumns,",","","");
channelColumnsReviewTemp=channelColumnClauTemp+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
channelColumnsReview=channelColumnClau+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}
if(!y()){
StringchannelColumnClauTemp=(channelColumns,",","","");
channelColumnsReviewTemp=channelColumnClauTemp+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
StringchannelColumnClau;
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
channelColumnClau=(columns,",","","");
channelColumnsReview=channelColumnClau+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}
StringsourceColumnsStr="";
StringtargetColumnsStr="";
if(!y()){
StringsourceColumnsWithoutBatchId=(channelColumns,",","","");
sourceColumnsStr=sourceColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
StringtargetColumnsWithoutBatchId;
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
targetColumnsWithoutBatchId=(columns,",","","");
targetColumnsStr=targetColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
sourceColumnsStr=idTypeColumn+",batch_id";
targetColumnsStr=idTypeColumn+",batch_id";
}
拆分代码块
再观察,发现if-el代码块中同时操作了sourceColumnsStr和targetColumnsStr两个变量,不利于后续运⽤提取⽅法的重构⼿法。
因此需要运⽤splitblock⼿法,将这两个变量的计算拆分到两个代码块中。先完整拷贝⼀份if-el代码,并在第⼀份中保留对
sourceColumnsStr的计算,在第⼆份中保留对targetColumnsStr的计算,并且调整⼀下这两个变量申明的位置,到if-el计算逻辑的前
⾯:
提取⽅法
⾄此,可以提取两个⽅法:computeSourceColumnsStr()以及computeTargetColumnsStr():
StringsourceColumnsStr="";
if(!y()){
StringsourceColumnsWithoutBatchId=(channelColumns,",","","");
sourceColumnsStr=sourceColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
sourceColumnsStr=idTypeColumn+",batch_id";
}
StringtargetColumnsStr="";
if(!y()){
StringtargetColumnsWithoutBatchId;
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
targetColumnsWithoutBatchId=(columns,",","","");
targetColumnsStr=targetColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
targetColumnsStr=idTypeColumn+",batch_id";
}
观察⼀下提取出来的两个⽅法,发现他们的不同之处在于computeTargetColumnsStr()中多了⼀个对columns集合的计算逻辑,于
是将columns的计算逻辑再封装⼀下:
privateStringcomputeTargetColumnsStr(){
StringtargetColumnsStr="";
if(!y()){
StringtargetColumnsWithoutBatchId;
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
targetColumnsWithoutBatchId=(columns,",","","");
targetColumnsStr=targetColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
targetColumnsStr=idTypeColumn+",batch_id";
}
returntargetColumnsStr;
}
privateStringcomputeSourceColumnsStr(){
StringsourceColumnsStr="";
if(!y()){
StringsourceColumnsWithoutBatchId=(channelColumns,",","","");
sourceColumnsStr=sourceColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
sourceColumnsStr=idTypeColumn+",batch_id";
}
returnsourceColumnsStr;
}
privateStringcomputeTargetColumnsStr(){
StringtargetColumnsStr="";
if(!y()){
Set
StringtargetColumnsWithoutBatchId;
targetColumnsWithoutBatchId=(columns,",","","");
targetColumnsStr=targetColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
targetColumnsStr=idTypeColumn+",batch_id";
}
returntargetColumnsStr;
}
privateSet
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
returncolumns;
}
经过对⽐,还有
这两⾏与StringsourceColumnsWithoutBatchId=(channelColumns,",","","");存在不⼀致。但可以使⽤变量的内联重构成⼀样的
形式。
经过整理后代码的形式如下:
StringtargetColumnsWithoutBatchId;
targetColumnsWithoutBatchId=(columns,",","","");
publicStringgenerateSql(){
StringsourceColumnsStr=computeSourceColumnsStr();
StringtargetColumnsStr=computeTargetColumnsStr();
StringBuffervsql=newStringBuffer();
("inrtinto").append(_SCHEMA).append(".").append(tableName)
.append("(").append(targetColumnsStr).append(")")
.append("lectdistinct").append(e(ME_COLUMN,"isnull("+ME_COLUMN+
.append("from").append(_SCHEMA).append(".").append(sourceTableName).append(";n");
returnreviewTempTableSql+ng();
}
privateStringcomputeTargetColumnsStr(){
StringtargetColumnsStr="";
if(!y()){
Set
StringtargetColumnsWithoutBatchId=(columns,",","","");
targetColumnsStr=targetColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
targetColumnsStr=idTypeColumn+",batch_id";
}
returntargetColumnsStr;
}
privateStringcomputeSourceColumnsStr(){
StringsourceColumnsStr="";
if(!y()){
StringsourceColumnsWithoutBatchId=(channelColumns,",","","");
sourceColumnsStr=sourceColumnsWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
sourceColumnsStr=idTypeColumn+",batch_id";
}
returnsourceColumnsStr;
}
privateSet
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
returncolumns;
}
提炼参数
观察两个⽅法,发现只有columns和channleColumns不同,其它均相同。因此彩提炼参数的⽅法,为其增加参数,从⽽合并为⼀个
⽅法。做法是先将channleColumns再重新提取⼀个名为columns的变量,并提到⽅法最开始处。再把⽅法中的局部变量重命名⼀下,就
变成了:
同样,将computeTargetColumnsStr()⽅法也处理⼀下:
再将两个⽅法的剩下内容提取为⼀个新的⽅法,新⽅法含有columns作为参数:
内联⽅法,并整理
privateStringcomputeSourceColumnsStr(){
Set
StringcolumnsStr="";
if(!y()){
StringcolumnsStrWithoutBatchId=(columns,",","","");
columnsStr=columnsStrWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
columnsStr=idTypeColumn+",batch_id";
}
returncolumnsStr;
}
privateStringcomputeTargetColumnsStr(){
Set
StringcolumnsStr="";
if(!y()){
StringcolumnsStrWithoutBatchId=(columns,",","","");
columnsStr=columnsStrWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
columnsStr=idTypeColumn+",batch_id";
}
returncolumnsStr;
}
privateStringcomputeTargetColumnsStr(){
Set
returntransformToString(columns);
}
privateStringcomputeSourceColumnsStr(){
Set
returntransformToString(columns);
}
privateStringtransformToString(Set
StringcolumnsStr="";
if(!y()){
StringcolumnsStrWithoutBatchId=(columns,",","","");
columnsStr=columnsStrWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
columnsStr=idTypeColumn+",batch_id";
}
returncolumnsStr;
}
原先提取出来的两个⽅法就只剩下2⾏内容了,其中⼀⾏中变量的申明,可以将变量内联:
再将这两个⽅法内联,最终形成如下的形式:
再⼀次提炼⽅法
⾄此,整个代码中没有重复代码,每个⽅法长度得到控制,命名也⽐较恰当,重构可以⾄此结束。但对于⾼要求的风格⽽⾔,应该要求
⽅法中的每个⼦⽅法都在同⼀抽象粒度上。然⽽transformToString()⽅法与后续的sql拼接并不在⼀个抽象粒度上,因此可以将sql拼接再
提取到⼀个新⽅法中,从⽽增加可读性。
做法是先将ng()提取为变量:
privateStringcomputeTargetColumnsStr(){
returntransformToString(getTargetColumns());
}
privateStringcomputeSourceColumnsStr(){
returntransformToString(lColumns);
}
publicStringgenerateSql(){
StringsourceColumnsStr=transformToString(lColumns);
StringtargetColumnsStr=transformToString(getTargetColumns());
StringBuffervsql=newStringBuffer();
("inrtinto").append(_SCHEMA).append(".").append(tableName)
.append("(").append(targetColumnsStr).append(")")
.append("lectdistinct").append(e(ME_COLUMN,"isnull("+ME_COLUMN+
.append("from").append(_SCHEMA).append(".").append(sourceTableName).append(";n");
returnreviewTempTableSql+ng();
}
privateStringtransformToString(Set
StringcolumnsStr="";
if(!y()){
StringcolumnsStrWithoutBatchId=(columns,",","","");
columnsStr=columnsStrWithoutBatchId+
(ns(idTypeColumn)?"":(","+idTypeColumn))+",batch_id";
}el{
columnsStr=idTypeColumn+",batch_id";
}
returncolumnsStr;
}
privateSet
Set
for(Stringstr:channelColumns){
if(_BUS_EML_ng().equals(str)){
(_EML_ng());
}el{
(str);
}
}
returncolumns;
}
再将return之前的部分提取到新⽅法generateInrtSql()中:
总结
重构过程到此结束。
整个重构过程中,使⽤了reorder,rename,extractvariable,extractmethod,inlinemethod,splitfor/codeblock,add
parameter等⼿法。观察⼀下每个步骤都是可控的,如果重构在每个步骤后停⽌,代码依然可以运⾏。更重要的是,每个步骤都能被证明保
持了原有代码的⾏为。这也是重构最重要的两个条件。
重构的案例代码:
StringBuffervsql=newStringBuffer();
("inrtinto").append(_SCHEMA).append(".").append(tableName)
.append("(").append(targetColumnsStr).append(")")
.append("lectdistinct").append(e(ME_COLUMN,"isnull("+ME_COLUMN+",'未知城市
.append("from").append(_SCHEMA).append(".").append(sourceTableName).append(";n");
StringinrtSql=ng();
returnreviewTempTableSql+inrtSql;
publicStringgenerateSql(){
StringsourceColumnsStr=transformToString(lColumns);
StringtargetColumnsStr=transformToString(getTargetColumns());
StringinrtSql=generateInrtSql(sourceColumnsStr,targetColumnsStr);
returnreviewTempTableSql+inrtSql;
}
本文发布于:2023-02-03 04:17:58,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/fan/88/181222.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |