Encapsulation means the hiding of data and behavior from a client. It is a key object-oriented concept and, in some ways, the key to object-oriented programming. Since our goal is to make each part as independent as possible from anything external, classes and methods should receive as much information as is needed to satisfy the task they are meant for.
Anyone interested in good object-oriented development cares about encapsulation and is likely concerned with hiding or exposing the best data structure in their object, depending on the needed architecture. In this chapter we'll learn how to unlock or prevent access to data with proper interfaces, how to couple the data with the behavior processing them and how to manage type-oriented code in the proper way, leading to a better organized structure.
Problem: “Accessing a field directly can lead to bad maintainability and errors.”
Solution: “Create getter and setter methods.”
The most important scenario that will make you need this refactoring is when a super class directly accessing an attribute is extended by a subclass needing to override the access to that attribute with added logic. With a self-encapsulated attribute you are free to manage logic all along the hierarchy.
This is a very easy refactoring. Let's see the class and the unit test:
class Sale
{
public $product_price = 10;
public $amount;
public function getPrice()
{
return $this->amount * $this->product_price;
}
}
class SaleTest extends PHPUnit_Framework_TestCase
{
public function testGetPrice()
{
$sale = new Sale();
$sale->amount = 10;
$this->assertEquals(100, $sale->getPrice());
}
}
Now we apply the refactoring steps and obtain the refactored class, still passing tests.
class Sale
{
protected $product_price = 10;
public $amount;
public function getProductPrice()
{
return $this->product_price;
}
public function getPrice()
{
return $this->amount * $this->getProductPrice();
}
}
The benefit becomes clearer if we have to extend the Sale
class in DiscountedSale
.
class DiscountedSale extends Sale
{
public function getProductPrice()
{
return $this->product_price * 0.9;
}
}
This passes the following test:
public function testExtendedGetPrice()
{
$sale = new DiscountedSale();
$sale->amount = 10;
$this->assertEquals(90, $sale->getPrice());
}
Problem: “Some data needs additional behavior.”
Solution: “Turn the data into an object.”
You start developing a class trying to keep things simple, and you just represent data with simple data types. As the software grows, though, some of the simple data attributes you wrote need to get more complex. A simple username is just a string at the very beginning, but as you add firstname, lastname, address, e-mail, etc. this data set gets a sense of its own and likely starts showing the need to acquire independence and get some encapsulated behavior. Turning this data into an object solves the problem.
We have the Order
class with its private attribute $customer
.
class Order
{
private $customer;
public function __construct($customer)
{
$this->setCustomer($customer);
}
public function setCustomer($customer)
{
$this->customer = $customer;
}
public function getCustomer()
{
return $this->customer;
}
}
We can write a very simple test to check this class's behavior.
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testGetCustomer()
{
$order = new Order('Edwin Moses'),
$this->assertEquals('Edwin Moses', $order->getCustomer());
}
}
After verifying that the test is OK, start refactoring the class. First, we create the Customer
class:
class Customer
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
Thereafter we change Order's setter to create an instance of Customer
and the getter to reference Customer
's name getter:
class Order
{
private $customer;
public function __construct($customer)
{
$this->setCustomer($customer);
}
public function getCustomer()
{
return $this->customer->getName();
}
public function setCustomer($customer)
{
$this->customer = new Customer($customer);
}
}
As you may have noticed already we created a Customer
object for each Order, and we didn't reference a unique Customer
object shared among many Order
objects. This is because we are treating the customer as a value object. If you want to give the customer her own identity, the next refactoring technique “Change Value to Reference” addresses this need.
Problem: “You have many equal instances of a class that you want to replace with a single object.”
Solution: “Turn the object attribute into a reference object.”
Objects can be often grouped in two well-defined categories: reference objects and value objects.
Reference objects are those standing for a given object in the real world. They are defined by identity, and that identity is checked when testing for those objects to be equal. In domain-driven design [Evans] they are called entities. User accounts in a system or a purchase order are typical examples.
Value objects have no identity. The intention of a value object is to represent something by its attributes only. Two value objects are identical if they have identical attributes. However, not having any value other than by virtue of their attributes, they can be freely copied around. Dates, money, and RGB colors are usually represented as value objects.
The decision to change a value to a reference comes after having a small initial value grow in complexity as long as development goes on. At some point you want to add some mutable data to that value and have the same changes shared across objects using that value: you then change value to reference.
We'll be using “Replace Data Value with Object” as a starting point for this example. We have a Customer
class:
class Customer
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
This is used to create customer instances as value objects in an Order
class:
class Order
{
private $customer;
public function __construct($customer)
{
$this->setCustomer($customer);
}
public function getCustomer()
{
return $this->customer->getName();
}
public function setCustomer($customer)
{
$this->customer = new Customer($customer);
}
}
We follow with a test checking its correctness:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testGetCustomer()
{
$order = new Order('Edwin Moses'),
$this->assertEquals('Edwin Moses', $order->getCustomer());
}
}
We can now use “Replace Constructor with Factory Method.”
class Customer
[...]
public static function getInstance($name)
{
return new Customer($name);
}
private function __construct($name)
{
$this->name = $name;
}
[...]
And replace the call to the Customer constructor in the Order
setter method with the factory method:
public function setCustomer($customer)
{
$this->customer = Customer::getInstance($customer);
}
The factory method also facilitates testing the classes. In the real world, the Customer
factory method would probably talk with the DB mapper or with an identity map to retrieve new or preloaded instances of the Customer
class. In our case we simplify the reading using a Customer
stub class to test the Order
class. Our stub preloads a small set of Customer
instances and the factory method will be changed to retrieve one of those preloaded instances.
class Customer
{
private $name;
private static $customers;
public static function loadCustomers()
{
self::$customers = array(
'Edwin Moses' => new Customer('Edwin Moses'),
'John Foo' => new Customer('John Foo')
);
}
public static function getInstance($name)
{
return self::$customers[$name];
}
private function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
Add this setup method in our unit test:
class OrderTest extends PHPUnit_Framework_TestCase
{
[...]
public function setUp()
{
Customer::loadCustomers();
}
[...]
}
Problem: “A reference object is immutable and hard to manage.”
Solution: “Turn it into a value object.”
The definition of a value object is in its consistent and not mutable state. This means that any time you call a method on a value object you will always get the same output in return. This property makes value objects interchangeable, thus many clients invoking a query on a reference object and getting the same result can easily be linked to many different objects representing the same value. On the other hand, if in changing a value you don't have to ensure every other object using that value must be updated, again there is no problem in using a value object.
If working with a reference becomes awkward for any reason and it can be represented by many value objects, then you can make things simpler with “Change Reference to Value.”
Problem: “You have an array containing elements meaning different things.”
Solution: “Replace the array with an object featuring an attribute for each array element.”
Since the very beginning of computer science, arrays were meant to store collections of homogenous data. As the years passed, modern languages let us freely aggregate several types of data. PHP arrays are very flexible and provide a single construct to represent arrays, hashes, structs, and records (a few well-known examples in other languages). By the way, classes can sometimes be more expressive, and, above all, they provide the fittest ground to make behavior grow. As soon as this data is processed by very dedicated code, this is likely to be encapsulated in a class along with that data. Replacing arrays with objects, then, is the way to go.
We consider a simple Order
class using some data about the customer:
class Order{
private $customer;
public function __construct($customer_data)
{
$this->customer = $customer_data;
}
public function getShippingAddress()
{
return $this->customer[0].', '.$this->customer[1];
}
}
This is tested with the following:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testGetCustomer()
{
$order = new Order(array('Gerardo Rossi', 'BeckerStrasse 12, Utopia'));
$this->assertEquals('Gerardo Rossi, BeckerStrasse 12, Utopia', $order->getShippingAddress());
}
}
We can then apply the refactoring steps. First we create a new class with a public array field and make the Order
class use it:
class Customer
{
public $data;
}
class Order
{
private $customer;
public function __construct($customer_data)
{
$this->customer = new Customer;
$this->customer->data = $customer_data;
}
public function getShippingAddress()
{
return $this->customer->data[0].', '.$this->customer->data[1];
}
}
At this point, tests are strictly green. Then we create accessors for the two fields:
class Customer
{
private $data;
public function getName()
{
return $this->data[0];
}
public function getAddress()
{
return $this->data[1];
}
}
class Order
{
public function __construct($customer_data)
{
$this->customer = new Customer;
$this->customer->setName($customer_data[0]);
$this->customer->setAddress($customer_data[1]);
}
public function getShippingAddress()
{
return $this->customer->getName().', '.$this->customer->getAddress();
}
}
Now we've set the $data field private in the Customer
class. Last we turn the array elements into Customer
class fields and get rid of the array:
class Customer
{
private $name, $address;
public function getName()
{
return $this->name;
}
public function getAddress()
{
return $this->address;
}
public function setName($name)
{
$this->name = $name;
}
public function setAddress($address)
{
$this->address = $address;
}
}
Tests continue to show that everything was changed within safe boundaries.
Problem: “You have two classes needing each other's features, but there is only a one-way link.”
Solution: “Add a backward link and change the setter and getter methods to update both sets.”
You have a class attribute referring to another class. As development goes on you find that an instance of the referred class needs to get the object referring to it. The referred class features no reference to the referring class, so we have to set up a two-way reference.
This time we have to unit test more than a class—we have to test a relationship, and it would require building correct stubs for both sides of the association. It would not be hard and, indeed, we could just arrange the right stub set in minutes, but for the sake of clarity here we will see another kind of test: integration tests. They are based on more than one real class and will be useful here to keep the example concise.
Please note how a simple dependency like the one we are about to model and test makes things lots harder to code and test, giving us strong clues about how harmful dependencies can be for our architecture.
Let's now dive into the “Change Unidirectional Association to Bidirectional” technique. Let's start from a simple version of the one Customer–many Orders relationship:
class Customer
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Order
{
private $customer;
public function __construct($customer)
{
$this->setCustomer($customer);
}
public function getCustomer()
{
return $this->customer;
}
public function setCustomer(Customer $customer)
{
$this->customer = $customer;
}
}
class OrderTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->customer = new Customer('Edwin Moses'),
}
public function testGetCustomer()
{
$order = new Order($this->customer);
$this->assertEquals($this->customer, $order->getCustomer());
}
}
Following the refactoring steps, we add an attribute to the Customer
class to hold the back link to its Order
objects, and we also add a couple of helper methods here, having decided the Order
class will be controlling the association:
class Customer
{
private $name;
private $orders = array();
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function getOrders()
{
return $this->orders;
}
public function setOrders($orders)
{
$this->orders = $orders;
}
}
Now we have to change the setter method in the Order
class.
public function setCustomer(Customer $customer)
{
if (!is_null($this->customer))
{
$this->customer->setOrders(array_diff($this->customer->getOrders(), array($this)));
}
$this->customer = $customer;
$this->customer->setOrders(array_merge($this->customer->getOrders(), array($this)));
}
Then create a test for the Customer
class:
class CustomerTest extends PHPUnit_Framework_TestCase
{
public function testGetOrders()
{
$customer = new Customer('Edwin Moses'),
$order1 = new Order($customer);
$order2 = new Order($customer);
$this->assertEquals(array($order1, $order2), $customer->getOrders());
}
}
After running both tests, they should be green now.
Problem: “Two classes reference each other but one class no longer needs other class's features.”
Solution: “Make the two-way association one-way.”
Bidirectional associations are sometimes needed but they always introduce complexity. This complexity comes in the form of maintaining the two-way links and avoiding errors while writing the code to manage them.
Bidirectional associations also force an interdependency between two classes, leading to a more coupled system that is harder to maintain or change without cascading and unpredictable side effects.
Thus bidirectional associations should be used only when needed. If a bidirectional association is no longer providing its benefit, make it unidirectional.
The hardest step while performing this refactoring is the first step: being sure the bidirectional association is not needed anymore. If a reference to the object that the reference we are killing points to is still needed by some method, we can opt to pass that object as an argument of that method.
Let's consider an example where the Order can be discounted on a per-Customer basis.
class OrderTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->customer = new Customer('Edwin Moses'),
}
public function testGetCustomer()
{
$order = new Order();
$this->customer->addOrder($order);
$this->assertEquals($this->customer, $order->getCustomer());
}
This integration test is satisfied by these two classes:
class Customer
{
private $name;
private $orders = array();
public $discount;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function addOrder(Order $order)
{
$order->setCustomer($this);
}
public function getOrders()
{
return $this->orders;
}
public function setOrders($orders)
{
$this->orders = $orders;
}
}
class Order
{
private $customer;
private $price = 100;
public function getCustomer()
{
return $this->customer;
}
public function setCustomer(Customer $customer)
{
if (!is_null($this->customer))
{
$this->customer->setOrders(array_diff($this->customer->getOrders(), array($this)));
}
$this->customer = $customer;
$this->customer->setOrders(array_merge($this->customer->getOrders(), array($this)));
}
public function getDiscountedPrice()
{
return $this->price * $this->customer->discount;
}
}
We want to remove the reference to Customer
in the Order
class. We need that reference in the getDiscountedPrice()
method, so we decide to pass it as an argument:
public function getDiscountedPrice($customer)
{
return $this->price * $customer->discount;
}
We change the test accordingly, modifying testGetDiscountedPrice()
:
public function testGetDiscountedPrice()
{
$this->customer->discount = 0.7;
$order = new Order();
$this->customer->addOrder($order);
$this->assertEquals(70, $order->getDiscountedPrice($this->customer));
}
Now it will pass green.
We can then remove the testGetCustomer()
method from the test and remove the getter from the Order
class. The tests are still green.
Now we want to remove the setter method, but it's still used in the addOrder()
method in the Customer
class. We perform “Substitute Algorithm” on it:
public function addOrder(Order $order)
{
$this->orders[] = $order;
}
We are now free to remove the setCustomer()
method from the Order
class along with the $customer
attribute, keeping tests green.
class Order
{
private $price = 100;
public function getDiscountedPrice($customer)
{
return $this->price * $customer->discount;
}
}
Problem: “You have a literal meaningful number.”
Solution: “Create a meaningful constant and replace the number with it.”
Magic numbers are those usually-not-obvious numbers with special values. The gravitational constant and pi are good examples of magic numbers in the fields of physics and mathematics.
Hardcoding magic numbers leads to less maintainable code. First, they are harder to change, and second, it's a lot harder to read the code and understand it. Declaring constants in a PHP class is easy, and they are cheap enough from a performance point of view, providing a great improvement in maintainability and readability.
This a very simple case to exemplify. Let's have a look at a class called Circle
that stores its radius providing a method to compute its circumference.
class Circle
{
public $radius;
public function getCircumference()
{
return $this->radius * 2 * 3.1416;
}
}
class CircleTest extends PHPUnit_Framework_TestCase
{
public function testGetCircumference()
{
$circle = new Circle();
$circle->radius = 2;
$this->assertEquals(12.5664, $circle->getCircumference());
}
}
We apply the simple steps just described and we get:
class Circle
{
const PI = 3.1416;
public $radius;
public function getCircumference()
{
return $this->radius * 2 * self::PI;
}
}
And it still passes the test.
Problem: “A class exposes a public attribute.”
Solution: “Make the attribute private and add accessor methods.”
As we have already seen in “Self-Encapsulate Field,” accessing a field directly from within a class can be a choice subject to many arguments. The issue becomes even worse as we face this situation regarding some public field used by some foreign client. Encapsulation is one of the fundamentals of object-oriented programming: never make your data public. Making an attribute public means that other objects can edit the attribute with the owning object not knowing this, then making its state unknown.
This leads to distributed logic that harms the modularity of the software. Data and behavior should go together in a close relationship because it is easier to change the code in just one place rather than across the whole application.
The technique “Encapsulate Field” hides the data and adds the accessors, paving the way for one or more uses of “Move Method” to bring scattered logic into one place.
We start from where we left with the technique “Replace Magic Number with Symbolic Constant,” and we apply the steps to refactor the Circle
class, beginning with adding the radius setter:
public function setRadius($radius)
{
$this->radius = $radius;
}
fixing the test to make it green again:
public function testGetCircumference()
{
$circle = new Circle();
$circle->setRadius(2);
$this->assertEquals(12.5664, $circle->getCircumference());
}
Green tests allow us to make the $radius
attribute private:
class Circle
{
const PI = 3.1416;
private $radius;
public function setRadius($radius)
{
$this->radius = $radius;
}
public function getCircumference()
{
return $this->radius * 2 * self::PI;
}
}
Problem: “You have an immutable type code affecting the class behavior.”
Solution: “Replace the type code with subclasses.”
Quite often we have to write conditional code based on the value of a type code. Switches and if-then-else constructs are the spots to look for this kind of behavior, where we test the value of the type code to execute different code depending on that value. This conditional code needs to be refactored with the technique “Replace Conditional with Polymorphism.” For this refactoring to be performed we need the type code to be replaced by a class hierarchy that will structure the polymorphic behavior, featuring a subclass for each type code.
The simplest way to create this structure is to use the technique “Replace Type Code with Subclasses.” You take the class featuring the type code and extend it once for each type code. As long as the type code is immutable and the parent class is not already subclassed for another reason, you can apply this refactoring. If one of these conditions is not verified then you should perform “Replace Type Code with State/Strategy.”
Another reason to use “Replace Type Code with Subclasses” is the presence of type code–relevant behavior. After this refactoring you can use “Push Down Method” and “Push Down Field” to make specific data closer to specifically related behavior. This will free clients of the source class from managing variants on their side, leaving you free to add new behavior without the clients even knowing it: all you need to do is add a subclass.
Let's consider an Order
class and its test:
class Order:
{
const GOLD = 0;
const SILVER = 1;
const BRONZE = 2;
protected $promotion;
public function __construct($promotion)
{
$this->promotion = $promotion;
}
}
class TestableOrder extends Order
{
public function getPromotion()
{
return $this->promotion;
}
}
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotionType()
{
$order = new TestableOrder(Order::GOLD);
$this->assertEquals(Order::GOLD, $order->getPromotion());
$order = new TestableOrder(Order::SILVER);
$this->assertEquals(Order::SILVER, $order->getPromotion());
$order = new TestableOrder(Order::BRONZE);
$this->assertEquals(Order::BRONZE, $order->getPromotion());
}
}
Note how we extended the tested class to facilitate its testing with a public method to probe its internal state. Since we are passing the type code into the constructor, we create a factory method after using “Self-Encapsulate Field” type code:
class Order
{
const GOLD = 0;
const SILVER = 1;
const BRONZE = 2;
protected $promotion;
static public function create($promotion)
{
return new Order($promotion);
}
private function __construct($promotion)
{
$this->promotion = $promotion;
}
public function getPromotion()
{
return $this->promotion;
}
}
We need to change tests accordingly, and we can get rid of the TestableOrder
class since we introduced a type code accessor in the tested class, too:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotionType()
{
$order = Order::create(Order::GOLD);
$this->assertEquals(Order::GOLD, $order->getPromotion());
$order = Order::create(Order::SILVER);
$this->assertEquals(Order::SILVER, $order->getPromotion());
$order = Order::create(Order::BRONZE);
$this->assertEquals(Order::BRONZE, $order->getPromotion());
}
}
We can now start creating the subclasses for the first type:
class BronzeOrder extends Order
{
public function getPromotion()
{
return Order::BRONZE;
}
}
Now we edit the factory method:
class Order
{
[...]
static public function create($promotion)
{
if ($promotion == self::BRONZE)
{
return new BronzeOrder($promotion);
}
else
{
return new Order($promotion);
}
}
[...]
}
And tests continue to tell us that everything is OK. We can then create other subclasses related to SILVER and BRONZE promotions until we get to the end:
abstract class Order
{
const GOLD = 0;
const SILVER = 1;
const BRONZE = 2;
static public function create($promotion)
{
if ($promotion == self::BRONZE)
{
return new BronzeOrder($promotion);
}
elseif ($promotion == self::SILVER)
{
return new SilverOrder($promotion);
}
elseif ($promotion == self::GOLD)
{
return new GoldOrder($promotion);
}
else
{
throw new Exception('Invalid promotion'),
}
}
private function __construct($promotion)
{
$this->promotion = $promotion;
}
abstract public function getPromotion();
}
class BronzeOrder extends Order
{
public function getPromotion()
{
return Order::BRONZE;
}
}
class SilverOrder extends Order
{
public function getPromotion()
{
return Order::SILVER;
}
}
class GoldOrder extends Order
{
public function getPromotion()
{
return Order::GOLD;
}
}
At this point you are free to move behavior up and down the hierarchy to avoid duplication or enhance specialization.
Problem: “You have a type code affecting the class behavior, but you cannot use subclassing.”
Solution: “Replace the type code with a state object.”
This is similar to the technique “Replace Type Code with Subclasses” but can be used in more general cases—if the type code changes after having created the typed class instance or if another reason prevents subclassing. State and strategy patterns [GoF] are similar and any distinction among the two is not useful here.
We start from the now usual Order
class to compute a discount policy depending on the Order type
class Order
{
const GOLD = 0;
const SILVER = 1;
const BRONZE = 2;
private $price = 100;
private $promotion;
public function __construct($promotion)
{
$this->promotion = $promotion;
}
public function getFinalPrice()
{
$price = $this->price;
switch ($this->promotion)
{
case self::GOLD:
$price *= 0.7;
break;
case self::SILVER:
$price *= 0.8;
break;
case self::BRONZE:
$price *= 0.9;
break;
}
return $price;
}
}
We also provide tests to verify the method's correctness:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotion()
{
$order = new Order(Order::GOLD);
$this->assertEquals(70, $order->getFinalPrice());
$order = new Order(Order::SILVER);
$this->assertEquals(80, $order->getFinalPrice());
$order = new Order(Order::BRONZE);
$this->assertEquals(90, $order->getFinalPrice());
}
}
We proceed with self-encapsulating the promotion field.
class Order
{
[...]
public function __construct($promotion)
{
$this->setPromotion($promotion);
}
public function getPromotion()
{
return $this->promotion;
}
public function setPromotion($promotion)
{
$this->promotion = $promotion;
}
public function getFinalPrice()
{
$price = $this->price;
switch ($this->getPromotion())
{
[...]
}
return $price;
}
}
We have now to declare the state class now. We will name it Promotion
abstract class Promotion
{
abstract public function getCode();
}
Now we can now start creating subclasses.
class GoldPromotion extends Promotion
{
public function getCode()
{
return Order::GOLD;
}
}
class SilverPromotion extends Promotion
{
public function getCode()
{
return Order::SILVER;
}
}
class BronzePromotion extends Promotion
{
public function getCode()
{
return Order::BRONZE;
}
}
We run tests and everything is fine. Now is the moment to link the Order
class to the Promotion
hierarchy.
class Order
{
[...]
public function getPromotion()
{
return $this->promotion->getCode();
}
public function setPromotion($promotion)
{
switch ($promotion)
{
case self::GOLD:
$this->promotion = new GoldPromotion();
break;
case self::SILVER:
$this->promotion = new SilverPromotion();
break;
case self::BRONZE:
$this->promotion = new BronzePromotion();
break;
default:
throw new Exception('Invalid promotion code'),
}
}
[...]
}
Tests are still green! Now we can polish things a bit by putting all the code using the type codes in the new class.
class Order
{
private $price = 100;
private $promotion;
public function __construct($promotion)
{
$this->setPromotion($promotion);
}
public function getPromotion()
{
return $this->promotion->getCode();
}
public function setPromotion($promotion)
{
$this->promotion = Promotion::create($promotion);
}
public function getFinalPrice()
{
$price = $this->price;
switch ($this->getPromotion())
{
case Promotion::GOLD:
$price *= 0.7;
break;
case Promotion::SILVER:
$price *= 0.8;
break;
case Promotion::BRONZE:
$price *= 0.9;
break;
}
return $price;
}
}
abstract class Promotion
{
const GOLD = 0;
const SILVER = 1;
const BRONZE = 2;
static public function create($promotion)
{
switch ($promotion)
{
case self::GOLD:
return new GoldPromotion();
case self::SILVER:
return new SilverPromotion();
case self::BRONZE:
return new BronzePromotion();
default:
throw new Exception('Invalid promotion code'),
}
}
abstract public function getCode();
}
class GoldPromotion extends Promotion
{
public function getCode()
{
return Promotion::GOLD;
}
}
class SilverPromotion extends Promotion
{
public function getCode()
{
return Promotion::SILVER;
}
}
class BronzePromotion extends Promotion
{
public function getCode()
{
return Promotion::BRONZE;
}
}
And we update tests to reflect this little change:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotion()
{
$order = new Order(Promotion::GOLD);
$this->assertEquals(70, $order->getFinalPrice());
$order = new Order(Promotion::SILVER);
$this->assertEquals(80, $order->getFinalPrice());
$order = new Order(Promotion::BRONZE);
$this->assertEquals(90, $order->getFinalPrice());
}
}
Problem: “You have subclasses that vary only in methods that return constant data.”
Solution: “Change the methods to super class fields and eliminate the subclasses.”
Constant methods are those returning a hardcoded value. Though they can be very useful in subclasses returning different values for an accessor defined in the super class, it is sometimes true that a subclass made only of constant methods is not worth its own existence. You can then remove those subclasses, move fields up to the super class, and get a less complex architecture with no subclassing.
We have an abstract Promotion
class extended by some subclasses defining the promotion type:
abstract class Promotion
{
abstract public function isGold();
abstract public function isSilver();
abstract public function getCode();
}
class GoldPromotion extends Promotion
{
public function isGold()
{
return true;
}
public function isSilver()
{
return false;
}
public function getCode()
{
return 0;
}
}
class SilverPromotion extends Promotion
{
public function isGold()
{
return false;
}
public function isSilver()
{
return true;
}
public function getCode()
{
return 1;
}
}
The following tests certify their correctness:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotion()
{
$order = new GoldPromotion();
$this->assertEquals(0, $order->getCode());
$this->assertTrue($order->isGold());
$this->assertFalse($order->isSilver());
$order = new SilverPromotion();
$this->assertEquals(1, $order->getCode());
$this->assertFalse($order->isGold());
$this->assertTrue($order->isSilver());
}
}
Now we can move on, applying the first step described in the mechanics. We perform “Replace Constructor with Factory Method.”
abstract class Promotion
{
[...]
static public function createGoldPromotion()
{
return new GoldPromotion();
}
static public function createSilverPromotion()
{
return new SilverPromotion();
}
}
Then edit tests accordingly:
class OrderTest extends PHPUnit_Framework_TestCase
{
public function testPromotion()
{
$order = Promotion::createGoldPromotion();
$this->assertEquals(0, $order->getCode());
$this->assertTrue($order->isGold());
$this->assertFalse($order->isSilver());
$order = Promotion::createSilverPromotion();
$this->assertEquals(1, $order->getCode());
$this->assertFalse($order->isGold());
$this->assertTrue($order->isSilver());
}
We can add fields to manage each different kind of promotion:
abstract class Promotion
{
private $is_gold = false;
private $is_silver = false;
private $code;
[...]
}
Now we define a protected constructor in the Promotion
parent class and we use it in subclass constructors:
abstract class Promotion
{
[...]
protected function __construct($code)
{
if (0 === $code)
{
$this->is_gold = true;
}
elseif (1 === $code)
{
$this->is_silver = true;
}
else
{
throw new Exception('Invalid promotion code.'),
}
$this->code = $code;
}
}
We also do the following:
class GoldPromotion extends Promotion
{
public function __construct()
{
parent::__construct(0);
}
[...]
}
class SilverPromotion extends Promotion
{
public function __construct()
{
parent::__construct(1);
}
[...]
}
At this point we can run tests to verify our code's health. Thus we can move constant methods from subclasses up to the parent class, obtaining the following:
abstract class Promotion
{
private $is_gold = false;
private $is_silver = false;
private $code;
public function isGold()
{
return $this->is_gold;
}
public function isSilver()
{
return $this->is_silver;
}
public function getCode()
{
return $this->code;
}
[...]
}
And do the following:
class GoldPromotion extends Promotion
{
public function __construct()
{
parent::__construct(0);
}
}
class SilverPromotion extends Promotion
{
public function __construct()
{
parent::__construct(1);
}
}
We must remember to run tests after moving each of the three methods to check everything is right while we refactor our code, keeping ourselves in the green zone.
We just have to perform “Inline Method” on the subclass constructors, moving the subclasses' creation directly into the parent class's factory methods.
class Promotion
{
[...]
static public function createGoldPromotion()
{
return new Promotion(0);
}
static public function createSilverPromotion()
{
return new Promotion(1);
}
}
Now we can get rid of the subclasses.
Objects are defined by data with some behavior attached. Though the most important side of their design lies in the interface they expose, still, the data they carry plays a crucial role. Objects' inner structure can influence the interface they expose and the right choice can make a developer's life a lot easier. In this chapter we saw a few techniques to better deal with data held inside our objects. In the next chapter we'll be back focusing on our methods, discovering ways to simplify our conditional expressions.
18.222.167.161