Refactoring

Övning

Refactoring Principles


Hide “how” with “what”
Aim for consistency
Avoid deep nesting
Separate concerns (= Single Responsibility Principle)
Avoid duplication wisely (= Don’t Repeat Yourself)

1. Hide “How” With “What”

This principle is just a part/rephrasing of the clean code principle, as formulated by .
To me, hiding “how” with “what” means extracting classes and methods whenever:
I can identify a distinct, non-trivial function performed by some piece of code, and
I can hide this non-triviality behind a method with a meaningful name.

Example 1: updateRelativePath

Here’s a snippet from before the refactoring:
mainDistribution.contents(copySpec -> copySpec.filesMatching(patchModuleExtension.getJars(), action -> { RelativePath relativePath = action.getRelativePath().getParent().getParent() .append(true, "patchlibs", action.getName()); action.setRelativePath(relativePath);}));

Efter:

mainDistribution.contents( copySpec -> copySpec.filesMatching(patchModuleExtension.getJars(), this::updateRelativePath));To sum up, we:
hid how to update the relative path
with what we do (= the fact that we update it).
Thanks to such refactoring, it’s much easier to grasp what happens to mainDistribution.
For reference, the content of updateRelativePath is available
.

Example 2: buildAddReadsStream & buildAddOpensStream

This is how a part of the TestTask class looked before the refactoring:
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)); });});
Efter:
TestEngine.select(project).ifPresent(testEngine -> Stream.concat( buildAddReadsStream(testEngine), buildAddOpensStream(testEngine)).forEach(jvmArgs::add));
Again, we:
hid how the values of --add-reads and --add-opens options are specified
with what we do (= the fact that we specify them).

For reference, the contents of buildAddReadsStream and buildAddOpensStream are available
.

2. Aim for Consistency

This is very general, but I mean any kind of reasonable consistency that we can get.
For example, ‘s is a great example of striving for consistency. Needless to say, I agree with his conclusion wholeheartedly:
A large system with good symmetry becomes easier to understand, because you can detect and expect recurring patterns.
Donald Raab, Symmetric Sympathy
In the case of Gradle Modules Plugin, this boiled down primarily to extracting AbstractModulePluginTask base class and unifying the task finding & configuration dispatching procedure.
For example, JavadocTask and TestTask before the refactoring were:
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) }}

Efter:

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) { /* ... */ }}

For reference: JavaDocTask
and TestTask
.

3. Avoid Deep Nesting

This is rather obvious, I guess. For me, deep nesting of control structures is extremely hard to read and grasp.
As a consequence, I refactored the following getPackages method:

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;}
Såhär:
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); }}
Full
available here.

4. Separate Concerns

SRP () is a well-known software design principle. Here, we can see its application in extracting StartScriptsMutator from RunTaskMutator.
Innan:
ublic class RunTaskMutator { // common fields
public void configureRun() { /* ... */ }
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.