You have a single big method that writes information to local variables.
Instead write results to a Collecting Parameter which gets passed to the extracted methods.
Long Method
easy
A Collecting Parameter is an object that you pass to methods in order to collect information from those methods. Therefore, often this refactoring is combined with the Compose Method refactoring.
Instead of calling many methods in sequence and collecting their results in local variables, you instead pass an object to these methods and the methods can store their results in the passed object.
The following code is a slightly adapted version of a real-world class. We will now refactor it to use the Collecting Parameter pattern.
The accumulation method in our example is addService
which accumulates its
results in the local $return
variable:
class PhpDumper { // ... private function addService($id, $definition) { $return = array(); $return[] = '/**'; if ($definition->isSynthetic()) { $return[] = ' * @throws RuntimeException always since this service is expected to be injected dynamically'; } elseif ($class = $definition->getClass()) { $return[] = sprintf(" * @return %s A %s instance.", 0 === strpos($class, '%') ? 'object' : $class, $class); } elseif ($definition->getFactoryClass()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryClass(), $definition->getFactoryMethod()); } elseif ($definition->getFactoryService()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryService(), $definition->getFactoryMethod()); } $return[] = " */"; $return[] = "public function get{$this->camelize($id)}Service()"; $return[] = "{"; $return[] = $this->addServiceBody($id, $definition); $return[] = "}"; return implode("\n", $return); } // ... }
The first step in the addService
method is to add a doc-comment for the current
service. We will extract the related code to a new method and pass the $return
variable as a parameter to it:
class PhpDumper { // ... private function addService($id, $definition) { $return = array(); $this->addServiceDocComment($id, $definition, $return); $return[] = "public function get{$this->camelize($id)}Service()"; $return[] = "{"; $return[] = $this->addServiceBody($id, $definition); $return[] = "}"; return implode("\n", $return); } private function addServiceDocComment($id, $definition, array &$return) { $return[] = '/**'; if ($definition->isSynthetic()) { $return[] = ' * @throws RuntimeException always since this service is expected to be injected dynamically'; } elseif ($class = $definition->getClass()) { $return[] = sprintf(" * @return %s A %s instance.", 0 === strpos($class, '%') ? 'object' : $class, $class); } elseif ($definition->getFactoryClass()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryClass(), $definition->getFactoryMethod()); } elseif ($definition->getFactoryService()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryService(), $definition->getFactoryMethod()); } $return[] = " */"; } // ... }
After applying step 2 to the remaining body of the addService
method, our
refactored code looks like this:
class PhpDumper { // ... private function addService($id, $definition) { $return = array(); $this->addServiceDocComment($id, $definition, $return); $this->addServiceCode($id, $definition, $return); return implode("\n", $return); } private function addServiceCode($id, $definition, array &$return) { $return[] = "public function get{$this->camelize($id)}Service()"; $return[] = "{"; $return[] = $this->addServiceBody($id, $definition); $return[] = "}"; } private function addServiceDocComment($id, $definition, array &$return) { $return[] = '/**'; if ($definition->isSynthetic()) { $return[] = ' * @throws RuntimeException always since this service is expected to be injected dynamically'; } elseif ($class = $definition->getClass()) { $return[] = sprintf(" * @return %s A %s instance.", 0 === strpos($class, '%') ? 'object' : $class, $class); } elseif ($definition->getFactoryClass()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryClass(), $definition->getFactoryMethod()); } elseif ($definition->getFactoryService()) { $return[] = sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryService(), $definition->getFactoryMethod()); } $return[] = " */"; } // ... }
We could now also go ahead and apply this refactoring to the newly created methods to further decrease their size if desired.
In this chapter, we will perform the same refactoring as in step 2a except that
we will be using an object as Collecting Parameter. We will call this new object
StringBuilder
:
class StringBuilder { private $content = ''; public function appendln($content) { $this->content .= $content."\n"; return $this; } public function getContent() { return $this->content; } } class PhpDumper { // ... private function addService($id, $definition) { $sb = new StringBuilder(); $this->addServiceDocComment($id, $definition, $sb); $sb ->appendln("public function get{$this->camelize($id)}Service()") ->appendln("{") ; $this->addServiceBody($id, $definition, $sb); $sb->appendln("}"); return $sb->getContent(); } private function addServiceDocComment($id, $definition, StringBuilder $sb) { $sb->appendln('/**'); if ($definition->isSynthetic()) { $sb->appendln(' * @throws RuntimeException always since this service is expected to be injected dynamically'); } elseif ($class = $definition->getClass()) { $sb->appendln(sprintf(" * @return %s A %s instance.", 0 === strpos($class, '%') ? 'object' : $class, $class)); } elseif ($definition->getFactoryClass()) { $sb->appendln(sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryClass(), $definition->getFactoryMethod())); } elseif ($definition->getFactoryService()) { $sb->appendln(sprintf(' * @return object An instance returned by %s::%s().', $definition->getFactoryService(), $definition->getFactoryMethod())); } $sb->appendln(" */"); } // ... }
In contrast to step 2a, using an object gives us a bit more flexibility. We could for
example start to add convenience methods to it. In our case, appendDocComment
,
appendDocCommentStart
or indentation handling would be good candidates for
such methods, and would help avoid duplication.
Checklists present a proven order of necessary refactoring steps and are especially helpful for beginners.
If you feel comfortable with a refactoring, there is no need to follow each of the steps literally; you can often perform multiple steps at once or skip some.
We suggest to try the recommended order at least once; this will often provide new insights - even for seasoned developers.