Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 637 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Refactoring: Replace Array with Object

08.24.2011
| 6845 views |
  • submit to reddit

This 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:

  1. create a new class: it should contain only a public field encapsulating a little the array.
  2. Change the client code to use this new class in place of the primitive variable.
  3. 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.
  4. When this phase has been completed, make the array private and see if the code still works.
  5. 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.
  6. 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;
    }
}
Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)