Practical PHP Refactoring: Replace Array with Object
Join the DZone community and get the full member experience.
Join For FreeThis refactoring is a specialization of Replace Data Value with Object: its goal is to replace a scalar or primitive structure (in this case, an ever-present array) with an object where we can host methods that act on those data.
We have already seen a lightweight version of this refactoring in the code sample of that article: this time we go all the way to a real object, which has private fields representing the elements of the array.
Usually the target of the refactoring is an associative array, but it may also be a numeric one, with a limited number of elements.
When to introduce an object where a simple array already works?
A clue that points to the need for this refactoring in numerical arrays is the fact that the elements are not homogeneous: they may be in type (all strings or integers) but not in meaning. For example, if two or them are flipped the array loses meaning or becomes very strange:
array( 'FirstName LastName', 'address@example.com' )
For associative arrays, the refactoring is viable everytime the number of elements is strictly fixed:
array( 'name' => ... 'email' => ... )
Private fields are self-documenting, and they're easier to understand and maintain that the documentation of the keys of an array. Documentation on array structures always gets repeated in docblocks and doesn't have a real place to live in without a class; moreover, it's the death of encapsulation as nothing stops client code (even in the parts that should only pass the array to other methods) from accessing every single element of the array.
And of course, a class is a place where to put methods, while an array cannot host them.
Steps
The technique described by Fowler for this refactoring is composed of many little steps:
- create a new class: it should contain only a public field encapsulating a little the array.
- Change the client code to use this new class in place of the primitive variable.
- In an iterative cycle, add a getter and a setter for each field and change client code. At each step, the relevant tests should be run. The methods should still use internally the elements of the array.
- When this phase has been completed, make the array private and see if the code still works.
- Add private fields to substitute the elements of the array, and change getters and setters accordingly. This change now ripples only into the source code of the new class.
- When you're finished, delete the field storing the array.
Many little steps are often appropriate as the usage of the array spans over dozens of differente classes, and raises the risk of reaching an irreparably broken build.
After you have reached the final state, an object with getters and setters, you can go on and remove methods accordingly for immutability or encapsulation; or move Foreign Methods to the new class now that it has become a first class citizen.
Note that tests may encompass even end-to-end ones if the array was used on a large scale. For example, we replaced arrays with objects in the two upper layers of the application, forcing us to run tests at the end-to-end scale.
Example
In the initial state, a response is created by putting together an array. Client code is omitted for brevity, and only the creation part will be our target.
<?php class ReplaceArrayWithObjectTest extends PHPUnit_Framework_TestCase { public function testCanDefineAnHttpResponse() { $response = array( 'success' => true, 'content' => '{someJson:"ok"}' ); } }
The array is moved onto a public field of a new class.
<?php class ReplaceArrayWithObjectTest extends PHPUnit_Framework_TestCase { public function testCanDefineAnHttpResponse() { $response = new HttpResponse(array( 'success' => true, 'content' => '{someJson:"ok"}' )); } } class HttpResponse { public $data; public function __construct(array $data) { $this->data = $data; } }
We add setters (also getters in case we need them.)
class HttpResponse { public $data; public function __construct(array $data) { $this->data = $data; } public function setSuccess($boolean) { $this->data['success'] = $boolean; } public function setContent($content) { $this->data['content'] = $content; } }
The array becomes private, to check that only getters, setters and methods are really used externally.
<?php class ReplaceArrayWithObjectTest extends PHPUnit_Framework_TestCase { public function testCanDefineAnHttpResponse() { $response = new HttpResponse(array( 'success' => true, 'content' => '{someJson:"ok"}' )); $response->setSuccess(false); $response->setContent('{}'); $this->assertEquals(new HttpResponse(array( 'success' => false, 'content' => '{}' )), $response); } } class HttpResponse { private $data; public function __construct(array $data) { $this->setSuccess($data['success']); $this->setContent($data['content']); } public function setSuccess($boolean) { $this->data['success'] = $boolean; } public function setContent($content) { $this->data['content'] = $content; } }
Private fields replace the array elements. We can start move logic into methods on the new class.
class HttpResponse { private $success; private $content; public function __construct(array $data) { $this->setSuccess($data['success']); $this->setContent($data['content']); } public function setSuccess($boolean) { $this->success = $boolean; } public function setContent($content) { $this->content = $content; } }
Opinions expressed by DZone contributors are their own.
Comments