Practical PHP Refactoring: Encapsulate Collection
Join the DZone community and get the full member experience.
Join For FreeIn the scenario of today, a method returns an array (or a collection object) kept as a field on the object, or allows it to be set with a brand new instance.
This refactoring, Encapsulate Collection, favors encapsulation over exposing a primitive type: it substitutes the accessor/mutator couple with more specific methods, for example add() and remove() for changing the contents of the collection on element at the time.
In the PHP case, collections are mostly arrays, but also specialized SPL classes like ArrayObject or SplDoublyLinkedList and its derivatives; other infrastructure classes like implementations of Doctrine\Common\Collections\Collection are still considered collections by this refactoring.
Why hiding a collection?
Particularly in the case of SPL or vendor classes, the Api of the collection objects is really wide: they have dozens of methods. The refactoring hides many of these methods by keeping the object as a private field, and exposes only the necessary operations as public methods on the containing object (this should remind you of something). The only objects you can consider as already encapsulated are first-class collections that you have defined, and that do not expose unused methods like the library ones.
Another issue with exposing collection objects is the aliasing problem: when a collection object is returned, it can be subsequently modified by client code that maintains a reference to it. Only an array is safe in this case, being passed by value. The refactoring avoids this potential action at a distance by returning an array or a copy of the original object.
Steps
- Introduce new methods, such as add() and remove(). Each should take an element of the collection as argument.
- Valorize the collection field with an empty collection object if it's not already initialized.
- In the client code, replace calls to the setter method with code calling add() or remove().
- Still in the client code, replace calls to the getter method that then modify the collection with add() and remove().
- Modify the getter: deleting it is not necessary, but the method should return a read-only copy of the collection. A quick way to obtain this semantics is to expose an array copy (ArrayObject::getArrayCopy() or Collection::toArray()) or a Traversable object (ArrayObject::getIterator()).
Potentially, you can go on in moving client code that uses the getter inside the class itself, to further encapsulate the collection.
You can add further methods like clear() to delete all elements or removeByPosition($elementNumber); the point is to provide finer-grained operations which describe the collection's available operations instead of a catch-all setter/getter which will always push logic away in the client code.
Example
In the initial state, there are a setter and a getter on the containing object. We use an ArrayObject to execute this refactoring over an object instead of an array(), which is a primitive type passed by value; the procedure is still valid for Dotrine collections or other generic collection objects taken from libraries, or implemented on your own.
<?php class EncapsulateCollection extends PHPUnit_Framework_TestCase { public function testCollectionCanBePopulatedAndInspected() { $user = new User(); $groups = new ArrayObject(array( new Group('sysadmins'), new Group('developers'), new Group('economists') )); $user->setGroups($groups); $this->assertEquals(3, count($user->getGroups())); } } class User { private $groups; public function setGroups(ArrayObject $groups) { $this->groups = $groups; } public function getGroups() { return $this->groups; } } /** * A simple Value Object in this example, little more than a string. */ class Group { private $name; public function __construct($name) { $this->name = $name; } }
We introduced add(), the only method needed to implement the client code (our test). You should not add more methods than actually needed, throwing away encapsulation in the process.
In this step we also initialize the field with an empty instance.
class User { private $groups; public function __construct() { $this->groups = new ArrayObject(); } public function addGroup(Group $group) { $this->groups->append($group); } public function setGroups(ArrayObject $groups) { $this->groups = $groups; } public function getGroups() { return $this->groups; } }
Now we replace the setter usage with calls to add(). The setter can then be removed.
<?php class EncapsulateCollection extends PHPUnit_Framework_TestCase { public function testCollectionCanBePopulatedAndInspected() { $user = new User(); $user->addGroup(new Group('sysadmins')); $user->addGroup(new Group('developers')); $user->addGroup(new Group('economists')); $this->assertEquals(3, count($user->getGroups())); } } class User { private $groups; public function __construct() { $this->groups = new ArrayObject(); } public function addGroup(Group $group) { $this->groups->append($group); } public function getGroups() { return $this->groups; } }
Finally, we change also the getter semantics by forcing it to return an array copy (primitive type) instead of an object that can be used to modify the collection.
class User { private $groups; public function __construct() { $this->groups = new ArrayObject(); } public function addGroup(Group $group) { $this->groups->append($group); } /** * @return array */ public function getGroups() { return $this->groups->getArrayCopy(); } }
We could have returned
new ArrayObject($this->groups->getArrayCopy());
in case we needed to maintain an object as output.
Opinions expressed by DZone contributors are their own.
Comments