Practical PHP Refactoring: Preserve Whole Object
Join the DZone community and get the full member experience.
Join For FreeIn the scenario of today, we are extracting some fields or calculated values from an object, and then calling a method somewhere else by passing them in as parameters.
The code under scrutiny has dependencies both on the object, the method to call and the internals that it has to extract. An alternative, which this refactoring leads us to, is to pass in the whole object instead.
Why a single object as parameter?
First of all, passing the object often results in clearer signature of the method as it takes an higher-level entity with respect to some scattered fields. It also correspondes to a shorter list of parameters.
The code under refactoring also shows feature envy and lack of encapsulation with respect to the whole object: it's better to expose that in a single place (thecalled method) than to rewrite it for each call (with lots of duplication.)
Fowler notes that the refactoring closes against a particular change: the case where new data is needed from the object. You won't need to change all calls in that case, just the internal code of the method and the interface of the object.
A note on dependencies
Originally, there is a dependency from the refactored code to the object pieces to extract and pass. After the refactoring, this dependency is eliminated and substituted by a dependency from the refactored method to the whole object. Choose the best case for your situation.
In case you need to establish an ugly dependency, introduce also an interface between the new method and the whole object. It makes miracles for ease of testing and reuse of code.
Alternatives
We have alternatives when the method extracts lots of things from the object: it is a case of feature envy and part of its logic should be moved on the object itself to preserve encapsulation (and avoiding exposing each single private field with a getter).
There is also the case of the whole object calling the method by passing himself. I personally do not like to pass $this instead of $this->field{1,2,3} when the method on A is called by the object B passing its own values.
But it's a matter of dependency: the question to answer on a case-by-case basis is if it's better to pass to the method of A a subset of the interface of B (calls to one or two methods with good names) or a set of scalar parameters (fields/collaborators of B).
Steps
- Add a new parameter to the method: the whole object. Use Add Parameter.
- Determine the parameters to take from the object.
- For each of them, replace the inside reference with code that obtains it from the whole object.
- Delete the parameter and restart with the next one. Use Remove Parameter to avoid breaking the code.
Remember to simplify the code surrounding the calls now that they pass the whole object: extracting parameters which are not passed anymore is not necessary.
Example
We start from a method accepting some information about a date: it is a time slot for a calendar, which is specified for a particular month. It tells the client code whether the slot will contains at least one week when starting it at a particular date. A week is defined as more than seven days.
We want to pass the date directly instead of its pieces, which are scalar values.
<?php class PreserveWholeObject extends PHPUnit_Framework_TestCase { public function testTheSlotEvaluatesItsLength() { $today = new DateTime('2011-11-23'); $slot = new MonthSpecificSlot(); $this->assertTrue($slot->containsAWeek($today->format('m'), $today->format('d'))); } } class MonthSpecificSlot { public function containsAWeek($month, $day) { $reference = new DateTime('2011-' . $month . '-01'); $daysInMonth = $reference->format('t'); return $day + 6 <= $daysInMonth; } }
We add a new parameter: the DateTime object from which day and month are extracted. We don't need to address the dependency towards the object by introducing an interface, because DateTime is a plain ValueObject featured by the language, like (for example) ArrayIterator.
We execute the transformations for both parameters in a single step, although you may tackle one at the time in complex situations. It makes sense to batch the parameters together as you will modify the same files containing the calls for everyone of them.
<?php class PreserveWholeObject extends PHPUnit_Framework_TestCase { public function testTheSlotEvaluatesItsLength() { $today = new DateTime('2011-11-23'); $slot = new MonthSpecificSlot(); $this->assertTrue($slot->containsAWeek($today, $today->format('m'), $today->format('d'))); } } class MonthSpecificSlot { public function containsAWeek($startDate, $month, $day) { $reference = new DateTime('2011-' . $month . '-01'); $daysInMonth = $reference->format('t'); return $day + 6 <= $daysInMonth; } }
We now extract the parameters from inside the object in the method. The test still passes.
class MonthSpecificSlot { public function containsAWeek($startDate, $month, $day) { $month = $startDate->format('m'); $day = $startDate->format('d'); $reference = new DateTime('2011-' . $month . '-01'); $daysInMonth = $reference->format('t'); return $day + 6 <= $daysInMonth; } }
We remove the unused parameters, by acting also on the client code.
<?php class PreserveWholeObject extends PHPUnit_Framework_TestCase { public function testTheSlotEvaluatesItsLength() { $today = new DateTime('2011-11-23'); $slot = new MonthSpecificSlot(); $this->assertTrue($slot->containsAWeek($today)); } } class MonthSpecificSlot { public function containsAWeek($startDate) { $month = $startDate->format('m'); $day = $startDate->format('d'); $reference = new DateTime('2011-' . $month . '-01'); $daysInMonth = $reference->format('t'); return $day + 6 <= $daysInMonth; } }
The refactoring is finished, but there is a further refinement we can make: obtaining the number of days in the month from the original DateTime object. Mikado refactorings such as this technique enable further simplifications of the code.
class MonthSpecificSlot { public function containsAWeek($startDate) { $day = $startDate->format('d'); $daysInMonth = $startDate->format('t'); return $day + 6 <= $daysInMonth; } }
Opinions expressed by DZone contributors are their own.
Comments