Refactor long methods to method classes
Classes with a lot of lines of code are generally bad. A very long class generally has too many responsibilities, low cohesion, or both. The same holds for functions and class members. They should be ideally no longer than 20 lines, though this is not an hard limit, just a reference.
However, sometimes it is not possible to make functions smaller. Reasons could be various. Nonetheless, what can be done in this circumstance? Extract method to method class is a chance.
The repository for this tutorial is available on github: robertogallea/MethodObjectRefactoring. The result of each refactoring round is represented by a separate branch.
Currently there are 4 branches:
starting-point
round-1
round-2
round-3
The example
In order to descrive this refactoring example, assume to have a class with a bunch of very long methods, like the following HugeClass:
1<?php
2
3
4namespace App;
5
6
7class HugeClass
8{
9 public function firstAction(): string
10 {
11 /** suppose this method perform a lot of instructions */
12 return 'first action';
13 }
14
15 public function secondAction(): string
16 {
17 /** suppose this method perform a lot of instructions too */
18 return 'second action';
19 }
20}
Such class is backed by PHPUnit unit test:
HugeClassTest.php
1<?php
2
3
4namespace tests;
5
6
7use PHPUnit\Framework\TestCase;
8use App\HugeClass;
9
10class HugeClassTest extends TestCase
11{
12 /** @test */
13 public function it_can_do_firstAction()
14 {
15 $hugeClass = new HugeClass();
16
17 $this->assertEquals('first action', $hugeClass->firstAction());
18 }
19
20 /** @test */
21 public function it_can_do_secondAction()
22 {
23 $hugeClass = new HugeClass();
24
25 $this->assertEquals('second action', $hugeClass->secondAction());
26 }
27}
Refactoring - Round 1: extract method to class
Let's start by addressing firstAction(). Create a new class named FirstAction and move firstAction() body to an execute() method. Additionally, you can pass a HugeClass reference to the constructor method. This could be useful to get additional required data from the parent class.
FirstAction.php
1<?php
2
3
4namespace App;
5
6
7class FirstAction
8{
9 private HugeClass $caller;
10
11 public function __construct(HugeClass $caller)
12 {
13 $this->caller = $caller;
14 }
15
16 public function execute(): string
17 {
18 return 'first action';
19 }
20}
firstAction() method now simply instantiates a new FirstAction class, executes its main method and returns the result:
HugeClass.php
1<?php
2
3
4namespace App;
5
6
7class HugeClass
8{
9 public function firstAction(): string
10 {
11 return (new FirstAction($this))->execute();
12 }
13
14 public function secondAction(): string
15 {
16 /** suppose this method perform a lot of instructions too */
17 return 'second action';
18 }
19}
Refactor - Round 2: extend abstract class
Now you can repeat the same approach with other methods. Additionally, since other actions would be compliant with the same interface, every action class can extend a base HugeClassAction class, containing the common parts, which essentially is all but execute() method, which is declared as abstract:
HugeClassAction.php
1<?php
2
3
4namespace App;
5
6
7abstract class HugeClassAction
8{
9 private HugeClass $caller;
10
11 public function __construct(HugeClass $caller)
12 {
13 $this->caller = $caller;
14 }
15
16 public abstract function execute(): string;
17}
HugeClass then becomes:
1<?php
2
3
4namespace App;
5
6
7class HugeClass
8{
9 public function firstAction(): string
10 {
11 return (new FirstAction($this))->execute();
12 }
13
14 public function secondAction(): string
15 {
16 return (new SecondAction($this))->execute();
17 }
18}
Refactor - Round 3: use magic methods
This last step is php specific and leverages php __call() magic method. The use of such method is somewhat controversial, so maybe you would not like to use it. However it is worth describing.
__call($method, $args) magic method is called whenever a call to an unexisting method is attempted, passing the method name and a list of arguments.
Since in the example the body of refactored methods is the same except for the instantiated class, you can define a lookup table (LUT) associating a method name to its action class. Then, when you call a method, if it has an entry in the LUT, then the action is instantiated executed. Otherwise, an exception is thrown:
HugeClass.php
1<?php
2
3
4namespace App;
5
6
7class HugeClass
8{
9 private $actions = [
10 'firstAction' => FirstAction::class,
11 'secondAction' => SecondAction::class
12 ];
13
14 public function __call($method, $args)
15 {
16 $class = $this->actions[$method] ?? null;
17
18 if (!$class) {
19 throw new \Exception('Unsupported method');
20 }
21
22 return (new $class($this))->execute();
23 }
24}
Conclusion
This is a very easy refactoring which can still be very useful to clean and make your class more readable. Please leave your comments, proposed improvements and opinions in the comments section.