Practical PHP Refactoring: Change Value to Reference
Join the DZone community and get the full member experience.
Join For FreeClasses derived from data have often multiple copies of equal objects lying around: there is no functional difference between them. Examples of these classes go under the name of Value Object in DDD: PhoneNumber, Color, Date.
Other classes instead may have only one legitimate copy in the object graph for each different object, having an identity which must be unique. These Entities often have a field that is used for identification, substituting the comparison of all fields. User, UserGroup, ForumMessage are examples of this kind of classes, which are also called Reference objects in Fowler's book, which predates DDD.
Why should I introduce reference objects?
Sometimes we start with a simple design, and consider an object a Value Object: they are typically immutable and easier to treat. After we implement more stories in the system, this object may acquire an identity or some fields which vary in time, and so should be transformed into an Entity. This refactoring is about the Value->Entity direction of the change, which is the most laborious.
Steps
- First design decision: who provides access to the objects? We cannot create them anymore on the spot, and their unique reference should be injected from outside.
- Second design decision: use already objects with a longer lifetime, or create them when needed. In the first case, a simple injection works well; in the second, rarer case, the reference will probably be needed outside of the current object, or to be reachable via references, in both cases for the sake of persistence.
- Target the instantiation of the class, substituting the new operators with a parameter. You add the newly created Entity as a parameter in the methods which previously instantiated it.
In the cases where the parameter is already there, you need not "change anything": the case we want to address are the creation or duplication of objects (cloning or Factory Methods). When we pass around an object PHP 5 implementation will make sure no copies are made of it.
Example
We start from an Author Value Object composed by a Book one.
<?php class ChangeValueToReference extends PHPUnit_Framework_TestCase { public function testAuthorIsChanged() { $book = Book::fromTitleAndAuthor('Robots and empire', 'Unknown'); $book->changeAuthor('Asimov'); $this->assertEquals('Asimov', $book->getAuthor()->__toString()); } } class Book { private $title; private $author; /** * This is just a Factory Method providing a name to the constructor * for easier understanding. */ public static function fromTitleAndAuthor($title, $authorName) { return new self($title, new Author($authorName)); } private function __construct($title, $author) { $this->title = $title; $this->author = $author; } /** * Author is just a Value Object, so it can be created anywhere and * multiple copies of the same author be spread between objects. */ public function changeAuthor($newName) { $this->author = new Author($newName); } public function getAuthor() { return $this->author; } } /** * Author is a Value Object wrapping a string, although of course in reality * there would be other methods here, justifying its existence. */ class Author { private $name; public function __construct($name) { $this->name = $name; } public function __toString() { return $this->name; } }
We make the Book class accept an external Author object: now a single Author can be shared and persisted among all Book instances. We will need to add mutators or other methods to change the private fields to Author, too.
class Book { private $title; private $author; /** * This is just a Factory Method providing a name to the constructor * for easier understanding. This method cannot execute new anymore. */ public static function fromTitleAndAuthor($title, Author $author) { return new self($title, $author); } private function __construct($title, $author) { $this->title = $title; $this->author = $author; } /** * Author is now a Reference/Entity, so it cannot be created with new. The * unique copy of Asimov or Unknown should be passed from the outside. */ public function changeAuthor($newAuthor) { $this->author = $newAuthor; } public function getAuthor() { return $this->author; } }
Opinions expressed by DZone contributors are their own.
Comments