Practical PHP Refactoring: Introduce Parameter Object
Join the DZone community and get the full member experience.
Join For FreeIn the scenario of today, two or more parameters are often passed together to a set of similar methods. This happens for example with timing information containing day and month, or hours and minutes; or, in other domains, with parameters that are coupled to each other - such as an host and a port (google.com, 80), or an image and its alt text.
A long list of coupled parameters is not necessarily the sign that a method does too much. Today's refactoring is a solution to simplify the signature and express the coupling between the current parameters: wrapping them into a Parameter Object.
Why writing code for a new class?
We shouldn't be afraid of adding classes, at the similar pace as we add methods. When parameters are almost always passed together, they will be written down together in each signature and each call: it's a form of duplication and as such can and should be eliminated.
Moreover, the duplication is not limited to a single method: the set of coupled arguments can be passed to multiple methods, each called in many more places which must be synchronized. For example, adding a parameter in the initial situation means modifying lots of different source files.
The parameters are often the sign of an hidden concept: an object modelling their union. You know you are on the right track if a name for this concepts comes up quickly; otherwise you will have to invent it.
This refactoring enables you to put more logic on an object representing this set of values, instead of passing them around in an array (Primitive Obsession) or, like in this case, always together as multiple parameters.
The end result is also a shorter and more clear parameter list, again reaching the goal of simplifying method calls as we are doing with lots of refactorings in this part of the series.
Steps
- Create a new class: an instance of this class should wrap the various values which are passed to it in the constructor. The Parameter Object will usually be a Value Object, with an immutable state and with getters for each of its fields (for now).
- Execute Add Parameter to pass in also the new object (created on the spot) together with the various parameters.
- For each of the old parameters, apply Remove Parameter on it and access it inside the method by calling a getter on the Parameter Object.
The tests should always pass between the steps. The final step enabled by this refactoring consists in moving logic onto the Parameter Object. If you're lucky, this even leads to some of the getters vanishing.
Example
We have an Invoice object, which accepts some rows in order to calculate a total. A copious amount of money covering the Value Added Tax is added to the net total.
<?php class IntroduceParameterObject extends PHPUnit_Framework_TestCase { public function testTaxesInformationDriveTheQuotationsTotalAndTypeFields() { $quotation = new Quotation(); $quotation->addRow(1000); $quotation->specifyTaxes(20, 'VATCODE'); $this->assertEquals(1200, $quotation->getTotal()); $this->assertEquals('Type: VATCODE', $quotation->getTypeOfService()); } } class Quotation { private $netTotal = 0; private $vatPercentage; private $vatCode; public function addRow($row) { // including just the logic to implement the test we have $this->netTotal += $row; } public function specifyTaxes($percentage, $code) { $this->vatPercentage = $percentage; $this->vatCode = $code; } public function getTotal() { return $this->netTotal * (1 + $this->vatPercentage / 100); } public function getTypeOfService() { return 'Type: ' . $this->vatCode; } }
We notice two things:
- one in the rest of the code (not shown here): the 20% percentage and its related code are passed together to many methods. It makes sense, as different codes (bread vs. cars) may imply different tax percentages.
- in the class itself, the fields storing tax informations have very similar names: $vatRate and $vatCode. This is a mild form of duplication.
So we want to transform the couple of parameters into a Parameter Object. We create the class we need:
class VatRate { private $rate; private $code; public function __construct($rate, $code) { $this->rate = $rate; $this->code = $code; } public function getRate() { return $this->rate; } public function getCode() { return $this->code; } }
Now we can add a parameter to the method, which is an instance of our Parameter Object:
<?php class IntroduceParameterObject extends PHPUnit_Framework_TestCase { public function testTaxesInformationDriveTheQuotationsTotalAndTypeFields() { $quotation = new Quotation(); $quotation->addRow(1000); $quotation->specifyTaxes(new VatRate(20, 'VATCODE'), 20, 'VATCODE'); $this->assertEquals(1200, $quotation->getTotal()); $this->assertEquals('Type: VATCODE', $quotation->getTypeOfService()); } } class Quotation { private $netTotal = 0; private $vatRate; private $vatPercentage; private $vatCode; public function addRow($row) { // including just the logic to implement the test we have $this->netTotal += $row; } public function specifyTaxes(VatRate $vatRate, $percentage, $code) { $this->vatRate = $vatRate; $this->vatPercentage = $percentage; $this->vatCode = $code; } public function getTotal() { return $this->netTotal * (1 + $this->vatPercentage / 100); } public function getTypeOfService() { return 'Type: ' . $this->vatCode; } }
Invoice objects have all the information they need, available from the VatRate instance. So let's remove $percentage, the first parameter wrapped into the Parameter Object:
class Quotation { private $netTotal = 0; private $vatRate; private $vatPercentage; private $vatCode; public function addRow($row) { // including just the logic to implement the test we have $this->netTotal += $row; } public function specifyTaxes(VatRate $vatRate, $code) { $this->vatRate = $vatRate; $this->vatCode = $code; } public function getTotal() { return $this->netTotal * (1 + $this->vatRate->getRate() / 100); } public function getTypeOfService() { return 'Type: ' . $this->vatCode; } }
And now we can also remove $code, the other parameter:
class Quotation { private $netTotal = 0; private $vatRate; public function addRow($row) { // including just the logic to implement the test we have $this->netTotal += $row; } public function specifyTaxes(VatRate $vatRate) { $this->vatRate = $vatRate; } public function getTotal() { return $this->netTotal * (1 + $this->vatRate->getRate() / 100); } public function getTypeOfService() { return 'Type: ' . $this->vatRate->getCode(); } }
We notice this refactoring has enabled a further step: moving the calculation of the tax into VatRate. This also means we can delete the getRate() method.
class Quotation { private $netTotal = 0; private $vatRate; public function addRow($row) { // including just the logic to implement the test we have $this->netTotal += $row; } public function specifyTaxes(VatRate $vatRate) { $this->vatRate = $vatRate; } public function getTotal() { return $this->vatRate->tax($this->netTotal); } public function getTypeOfService() { return 'Type: ' . $this->vatRate->getCode(); } } class VatRate { private $rate; private $code; public function __construct($rate, $code) { $this->rate = $rate; $this->code = $code; } public function getCode() { return $this->code; } public function tax($netAmount) { return $netAmount * (1 + $this->rate / 100); } }
Opinions expressed by DZone contributors are their own.
Comments