5个重构原理示例

这篇文章介绍了重构真正的开源代码( Gradle Modules Plugin )时应用的五​​种(最著名的)重构原理。

语境

当我为Gradle Modules Plugin (PR #73单独编译 module-info.java ,我注意到了一些重构的潜力。 结果,我提交了问题#79 ,后来又通过PR #88 (尚未合并)解决了该问题,在其中重构了代码。

事实证明,重构比我最初想象的要广泛得多。 在这里,我介绍此PR的一部分,作为我在此处应用的重构原则的示例。

重构原理

注意:这里列出的列表绝不是全面的,并且原则不是原创的(不过,我以自己的声音并根据自己的理解提出了这些原则)。 正如我所看到的,这篇文章的最大价值在于遵循这些原则的真实示例。

这里介绍的五项原则是:

  1. 用“什么”隐藏“如何”
  2. 力求一致性
  3. 避免深层嵌套
  4. 单独的关注点(=单一责任原则)
  5. 明智地避免重复(=不要重复自己)

1.用“什么”隐藏“如何”

该原则只是由Robert Martin提出的“ 干净代码”原则的一部分。

对我来说,用“什么”隐藏“如何”意味着在任何时候提取类和方法

  • 我可以识别出由一段代码执行的独特,不平凡的功能,并且
  • 我可以用一个有意义的名称将这种不琐碎的事情隐藏起来。

示例1:

重构之前,这是RunTaskMutator的一个片段:

 mainDistribution.contents(copySpec -> copySpec.filesMatching(patchModuleExtension.getJars(), action -> { 
   RelativePath relativePath = action.getRelativePath().getParent().getParent() 
       .append( true , "patchlibs" , action.getName()); 
   action.setRelativePath(relativePath);  })); 

这是重构后的代码段:

 mainDistribution.contents( 
     copySpec -> copySpec.filesMatching(patchModuleExtension.getJars(), this ::updateRelativePath)  ); 

综上所述,我们:

  • 隐藏如何更新相对路径
  • 与我们有什么 (=我们更新它的事实)。

由于有了这样的重构,掌握mainDistribution发生的事情要容易mainDistribution

作为参考, 这里提供了updateRelativePath的内容。

示例2:

这是重构之前TestTask类的一部分的样子:

 TestEngine.select(project).ifPresent(testEngine -> { 
   args.addAll(List.of( "--add-reads" , moduleName + "=" + testEngine.moduleName)); 
   Set<File> testDirs = testSourceSet.getOutput().getClassesDirs().getFiles(); 
   getPackages(testDirs).forEach(p -> { 
     args.add( "--add-opens" ); 
     args.add(String.format( "%s/%s=%s" , moduleName, p, testEngine.addOpens)); 
   });  }); 

如下所示:

 TestEngine.select(project).ifPresent(testEngine -> Stream.concat( 
     buildAddReadsStream(testEngine), 
     buildAddOpensStream(testEngine)  ).forEach(jvmArgs::add)); 

同样,我们:

  • 隐藏如何 --add-reads--add-opens选项的值
  • 与我们有什么 (=我们指定它们的事实)。

作为参考,可在此处获得buildAddReadsStreambuildAddOpensStream的内容。

2.追求一致性

这很笼统,但是我的意思是我们可以获得任何合理的一致性。

例如, 唐纳德·拉布Donald Raab关于对称博客文章就是努力保持一致性的一个很好的例子。 不用说,我完全同意他的结论:

具有对称性的大型系统变得更容易理解,因为您可以检测并期望重复出现的模式。

Donald Raab,对称的同情

对于Gradle Modules Plugin,这主要归结为提取AbstractModulePluginTask基类并统一任务查找和配置调度过程。

例如,重构之前的JavadocTaskTestTask是:

 public class JavadocTask { 
   public void configureJavaDoc(Project project) { 
     Javadoc javadoc = (Javadoc) project.getTasks().findByName(JavaPlugin.JAVADOC_TASK_NAME); 
     if (javadoc != null ) { 
       // ... 
     } 
   }  }  public class TestTask { 
   public void configureTestJava(Project project, String moduleName) { 
     Test testJava = (Test) project.getTasks().findByName(JavaPlugin.TEST_TASK_NAME); 
     // ... (no null check) 
   }  } 

之后,它们是:

 public class JavadocTask extends AbstractModulePluginTask { 
   public void configureJavaDoc() { 
     helper().findTask(JavaPlugin.JAVADOC_TASK_NAME, Javadoc. class ) 
         .ifPresent( this ::configureJavaDoc); 
   } 
   private void configureJavaDoc(Javadoc javadoc) { /* ... */ }  }  public class TestTask extends AbstractModulePluginTask { 
   public void configureTestJava() { 
     helper().findTask(JavaPlugin.TEST_TASK_NAME, Test. class ) 
         .ifPresent( this ::configureTestJava); 
   } 
   private void configureTestJava(Test testJava) { /* ... */ }  } 

供参考: JavaDocTask diffTestTask diff

3.避免深度嵌套

我想这很明显。 对我而言,控制结构的深层嵌套非常难以阅读和掌握。

结果,我重构了以下getPackages方法:

 private static Set<String> getPackages(Collection<File> dirs) { 
   Set<String> packages = new TreeSet<>(); 
   for (File dir : dirs) { 
     if (dir.isDirectory()) { 
       Path dirPath = dir.toPath(); 
       try (Stream<Path> entries = Files.walk(dirPath)) { 
         entries.forEach(entry -> { 
           if (entry.toFile().isFile()) { 
             String path = entry.toString(); 
             if (isValidClassFileReference(path)) { 
               Path relPath = dirPath.relativize(entry.getParent()); 
               packages.add(relPath.toString().replace(File.separatorChar, '.' )); 
             } 
           } 
         }); 
       } catch (IOException e) { 
         throw new GradleException( "Failed to scan " + dir, e); 
       } 
     } 
   } 
   return packages;  } 

如下所示:

 private static Set<String> getPackages(Collection<File> dirs) { 
   return dirs.stream() 
       .map(File::toPath) 
       .filter(Files::isDirectory) 
       .flatMap(TestTask::buildRelativePathStream) 
       .map(relPath -> relPath.toString().replace(File.separatorChar, '.' )) 
       .collect(Collectors.toCollection(TreeSet:: new ));  }  private static Stream<Path> buildRelativePathStream(Path dir) { 
   try { 
     return Files.walk(dir) 
         .filter(Files::isRegularFile) 
         .filter(path -> isValidClassFileReference(path.toString())) 
         .map(path -> dir.relativize(path.getParent())); 
   } catch (IOException e) { 
     throw new GradleException( "Failed to scan " + dir, e); 
   }  } 

可在此处找到完整的差异

4.单独的问题

SRP( 单一职责原则 )是众所周知的软件设计原则。 在这里,我们可以看到其在从RunTaskMutator中提取StartScriptsMutator应用程序。

之前:

 public class RunTaskMutator { 
   // common fields 
   public void configureRun() { /* ... */ } 
   public void updateStartScriptsTask(String taskStartScriptsName) { /* ... */ } 
   // 12 other methods (incl. 2 common methods)  } 

后:

 public class RunTaskMutator extends AbstractExecutionMutator { 
   public void configureRun() { /* ... */ }   
   // 2 other methods  }  public class StartScriptsMutator extends AbstractExecutionMutator { 
   public void updateStartScriptsTask(String taskStartScriptsName) { /* ... */ } 
   // 8 other methods  } 

由于提取了StartScriptsMutator ,因此更容易理解以下范围:

供参考:以上提取的提交

5.明智地避免重复

DRY( 请勿重复 )是另一种著名的软件开发原理。 但是,以我的经验,这个原则有时太过复杂,导致代码无法重复,但是也太复杂了。

换句话说,只有在成本/收益比为正时,才应该重复数据删除:

  • 成本 :重构时间,产生的复杂性等
  • 获得 :没有重复(或更严格地说,是唯一的真理来源 )。

Gradle Modules Plugin中的一个这样的例子(在我看来,成本/收益比接近零,但仍然为正)是PatchModuleResolver的引入。

下面是重构的代码片段其中包括:

  1. PatchModuleExtension.configure方法。
  2. 使用它的地方( TestTask )。
  3. 无法使用的地方( RunTaskMutator )。
  4. 不能使用它的另一个地方( JavadocTask )。
 // 1. PatchModuleExtension  public List<String> configure(FileCollection classpath) { 
   List<String> args = new ArrayList<>(); 
   config.forEach(patch -> { 
         String[] split = patch.split( "=" ); 
         String asPath = classpath.filter(jar -> jar.getName().endsWith(split[ 1 ])).getAsPath(); 
         if (asPath.length() > 0 ) { 
           args.add( "--patch-module" ); 
           args.add(split[ 0 ] + "=" + asPath); 
         } 
       } 
   ); 
   return args;  }  // 2. TestTask  args.addAll(patchModuleExtension.configure(testJava.getClasspath()));  // 3. RunTaskMutator  patchModuleExtension.getConfig().forEach(patch -> { 
       String[] split = patch.split( "=" ); 
       jvmArgs.add( "--patch-module" ); 
       jvmArgs.add(split[ 0 ] + "=" + PATCH_LIBS_PLACEHOLDER + "/" + split[ 1 ]); 
     }  );  // 4. JavadocTask  patchModuleExtension.getConfig().forEach(patch -> { 
       String[] split = patch.split( "=" ); 
       String asPath = javadoc.getClasspath().filter(jar -> jar.getName().endsWith(split[ 1 ])).getAsPath(); 
       if (asPath != null && asPath.length() > 0 ) { 
         options.addStringOption( "-patch-module" , split[ 0 ] + "=" + asPath); 
       } 
     }  ); 

引入PatchModuleResolver ,代码如下所示:

 // 1. PatchModuleExtension  public PatchModuleResolver resolve(FileCollection classpath) { 
   return resolve(jarName -> classpath.filter(jar -> jar.getName().endsWith(jarName)).getAsPath());  }  public PatchModuleResolver resolve(UnaryOperator<String> jarNameResolver) { 
   return new PatchModuleResolver( this , jarNameResolver);  }  // 2. TestTask  patchModuleExtension.resolve(testJava.getClasspath()).toArgumentStream().forEach(jvmArgs::add);  // 3. RunTaskMutator  patchModuleExtension.resolve(jarName -> PATCH_LIBS_PLACEHOLDER + "/" + jarName).toArgumentStream().forEach(jvmArgs::add);  // 4. JavadocTask  patchModuleExtension.resolve(javadoc.getClasspath()).toValueStream() 
     .forEach(value -> options.addStringOption( "-patch-module" , value)); 

多亏了重构,现在只有一个地方( PatchModuleResolver ),我们在其中分割了PatchModuleExtension类的config条目。

供参考:DIFFS 1234

摘要

在这篇文章中,我介绍了以下五个重构原则:

  1. 用“什么”隐藏“如何”
  2. 力求一致性
  3. 避免深层嵌套
  4. 单独关注
  5. 明智地避免重复

每个原则都附有一个真实的示例,希望该示例显示了遵循该原则如何产生简洁的代码。

翻译自: https://www.javacodegeeks.com/2019/05/5-refactoring-principles-example.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值