Practical PHP Refactoring: Replace Data Value with Object
Join the DZone community and get the full member experience.
Join For FreeOne of the rules of simple design is the necessity to minimize the number of moving parts, like classes and methods, as long as the tests are satisfied and we are not accepting duplication or feeling the lack of an explicit concept. Thus, a rule that aids simple design is to use primitive types unless a field has already some behavior attached: we don't create a class for the user's name or the user's password; we just use some strings.
As we make progress, however, we must be able to revise our decisions via refactoring: if a field gains some logic, this behavior shouldn't be modelled by methods in the containing class, but by a new object. The code in this new class can be reused, while the containing object will change from case to case and you will end up duplicating the same methods.
Transforming a scalar value into an object is the essence of the Replace Data Value with Object refactoring. In most of the cases, a Value Object or a Parameter Object come out as a result: while DDD pursue Value Objects as concepts in the domain layer, this refactoring is more general and can be applied anywhere. For instance, in a project we started introducing Data Transfer Objects to model the data sent by the controller to a Service Layer.
Data values in PHP
In PHP, all scalar values are by nature data values as they cannot host methods:
- string, integers, and booleans are proper scalar.
- arrays are not scalar in the Perl or mathematical sense, but they are still a primitive type.
On the borderline, we find some simple objects used as data containers in PHP:
- ArrayObjects.
- SplHeap and other SPL data structures.
The classes on the borderline may host methods, but the original class is out of reach for modification, and an indirection has to be introduced."Local Extension"
Steps
- Create the new class: it should contain as a private field just the value you want to substitute. The methods you immediately need have to be chosen between a constructor, getters, and setters (where needed).
- Change the field in the containing class. Update the constructor to also create the new object and populate the field, or accept injection (a rarer case).
- Update the original getter to delegate to the new one.
- Update the original setter to delegate to the new one (where present) or to create a new object.
- Run tests at the functional level; the changes should be propagated to the construction phases, while the external usage should not change very much.
Example
In the initial state, magic arrays are passed around. It's very easy to build an array where a key is missing or is called incorrectly.
<?php class ReplaceDataValueWithObject extends PHPUnit_Framework_TestCase { public function testUserCanSetANewPassword() { $userService = new UserService(/* other dependencies*/); $userService->newPassword(array( 'userId' => 42, 'oldPassword' => 'gismo', 'newPassword' => 'supersecret', 'repeatNewPassword' => 'supersecret' )); $this->markTestIncomplete('This refactoring is about the introduction of an object; it suffices that the test does not explode.'); } } class UserService { public function newPassword($changePasswordData) { /* it's not interesting to do something here */ } }
After the introduction of an ArrayObject extension, a little type safety is ensure and we gained a place to put methods at a little cost.
<?php class ReplaceDataValueWithObject extends PHPUnit_Framework_TestCase { public function testUserCanSetANewPassword() { $userService = new UserService(/* other dependencies*/); $userService->newPassword(new ChangePasswordCommand(array( 'userId' => 42, 'oldPassword' => 'gismo', 'newPassword' => 'supersecret', 'repeatNewPassword' => 'supersecret' ))); $this->markTestIncomplete('This refactoring is about the introduction of an object; it suffices that the test does not explode.'); } } class UserService { public function newPassword(ChangePasswordCommand $changePasswordData) { /* it's not interesting to do something here */ } } class ChangePasswordCommand extends ArrayObject { }
We add methods to implement logic on this object; in this case, validation logic; in general cases, any kind of code that should not be duplicated by the different clients.
For a stricter implementation, wrap an array or another data structure (scalars, SPL objects) instead of extending ArrayObject as you gain immutability and encapsulation (but this kind of objects need little encapsulation.)
class ChangePasswordCommand extends ArrayObject { public function __construct($data) { if (!isset($data['userId'])) { throw new Exception('User id is missing.'); } parent::__construct($data); } public function getPassword() { if ($this['newPassword'] != $this['repeatNewPassword']) { throw new Exception('Password do not match.'); } return $this['newPassword']; } }
Being this a refactoring however, this is the less invasive kind of introduction of objects you can make as the client code can still use the ArrayAccess interface and treat the object as a scalar array.
Opinions expressed by DZone contributors are their own.
Comments