这篇文章介绍了重构真正的开源代码( Gradle Modules Plugin )时应用的五种(最著名的)重构原理。
语境
当我为Gradle Modules Plugin (PR #73 ) 单独编译 module-info.java
,我注意到了一些重构的潜力。 结果,我提交了问题#79 ,后来又通过PR #88 (尚未合并)解决了该问题,在其中重构了代码。
事实证明,重构比我最初想象的要广泛得多。 在这里,我介绍此PR的一部分,作为我在此处应用的重构原则的示例。
重构原理
注意:这里列出的列表绝不是全面的,并且原则不是原创的(不过,我以自己的声音并根据自己的理解提出了这些原则)。 正如我所看到的,这篇文章的最大价值在于遵循这些原则的真实示例。
这里介绍的五项原则是:
- 用“什么”隐藏“如何”
- 力求一致性
- 避免深层嵌套
- 单独的关注点(=单一责任原则)
- 明智地避免重复(=不要重复自己)
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
选项的值 - 与我们有什么 (=我们指定它们的事实)。
作为参考,可在此处获得buildAddReadsStream
和buildAddOpensStream
的内容。
2.追求一致性
这很笼统,但是我的意思是我们可以获得任何合理的一致性。
例如, 唐纳德·拉布 ( Donald Raab ) 关于对称的博客文章就是努力保持一致性的一个很好的例子。 不用说,我完全同意他的结论:
具有对称性的大型系统变得更容易理解,因为您可以检测并期望重复出现的模式。
Donald Raab,对称的同情
对于Gradle Modules Plugin,这主要归结为提取AbstractModulePluginTask
基类并统一任务查找和配置调度过程。
例如,重构之前的JavadocTask
和TestTask
是:
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
diff和TestTask
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
,因此更容易理解以下范围:
- 本身配置
run
任务, - 配置相关的
startScripts
任务。
供参考:以上提取的提交 。
5.明智地避免重复
DRY( 请勿重复 )是另一种著名的软件开发原理。 但是,以我的经验,这个原则有时太过复杂,导致代码无法重复,但是也太复杂了。
换句话说,只有在成本/收益比为正时,才应该重复数据删除:
- 成本 :重构时间,产生的复杂性等
- 获得 :没有重复(或更严格地说,是唯一的真理来源 )。
Gradle Modules Plugin中的一个这样的例子(在我看来,成本/收益比接近零,但仍然为正)是PatchModuleResolver
的引入。
下面是重构前的代码片段,其中包括:
-
PatchModuleExtension.configure
方法。 - 使用它的地方(
TestTask
)。 - 无法使用的地方(
RunTaskMutator
)。 - 不能使用它的另一个地方(
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 1 , 2 , 3 , 4 。
摘要
在这篇文章中,我介绍了以下五个重构原则:
- 用“什么”隐藏“如何”
- 力求一致性
- 避免深层嵌套
- 单独关注
- 明智地避免重复
每个原则都附有一个真实的示例,希望该示例显示了遵循该原则如何产生简洁的代码。
翻译自: https://www.javacodegeeks.com/2019/05/5-refactoring-principles-example.html