Download - Patterns in PHP
Patterns:
- Decorator .
- Composite.
Diego LewinSenior Developer at Netconcepts
Structural Patterns are Design Patterns that ease the design by identifying a simple way to realize relationships between entities.
* Adapter pattern. * Aggregate pattern. * Bridge pattern. * Composite pattern. * Decorator pattern. * Extensibility pattern. * Facade pattern. * Flyweight pattern. * Proxy pattern. * Pipes and filters. * Private class data pattern.
Structural Patterns
The Decorator Pattern
The decorator pattern provides us with a mechanism for adding functionality to objects at runtime, as an alternative mechanism to creating additional child classes.
The Problem
As an example we can use an E-comerce system, we have different modules like:
Related content (for products,categories,etc). Customer Reviews (for products).. Tag Clouds (for products,categories,etc).. MYOB integration (for products).. Custom functionality from a specific client, that we don't
want to have in the core classes.
The ”Classes”
Product
Customer_Review
get_custom_reviews()
Tag_Cloud
get_arr_tag_cloud()
Integration_MYOB
do_integration()
get_price()
Custom_Funtionality
is_available()
is_available()
Product
Customer_ReviewTag_Cloud Custom_FuntionalityIntegration_MYOB
get_custom_reviews()get_arr_tag_cloud()do_integration()
get_price()
is_available()
is_available()
How many extensions are TOO many?
class Product{
public function get_price();public function is_available();
}
class Integration_MYOB extends Product{
public function do_integration(){
.......}.........
}
class Tag_Cloud extends Integration_MYOB{
public function get_arr_tag_cloud(){
.......}.........
}
Some Code...
class Customer_Review extends Tag_Cloud {
public function get_arr_customer_review(){
.......}.........
}
class Custom_Functionality extends Customer_Review{
public function is_available(){
.......}.........
}
More Code....
Product
Customer_ReviewTag_Cloud Custom_FuntionalityIntegration_MYOB
get_custom_reviews()get_arr_tag_cloud()do_integration()
do_integration()
is_available()
is_available()
Category
But, not all the projects need the same functionality...
I need to change my Code.., Class EXPLOSION
Customer_Review_For_Product
get_arr_custom_review()
Category Productdo_integration()
is_available()
Customer_Review_For_Category
get_arr_custom_review()
Tag_Cloud_For_Product
get_arr_tag_cloud()
Tag_Cloud_For_Category
get_arr_tag_cloud()
Even, there is more...I should reuse my extended classes !!
get_arr_product()
What, if we ”extend” the class in a 'run time' dynamically.
Magic !!
Example :/**instantiation* we pass in the constructor, the object that we want to extend.. (decorate)*/$obj_decorated_product = new Customer_Review(new Custom_Functionality( new Product));
/** calling 'decorated' methods, the final reslut is the same as we extend the classes*/$arr_custom_review = $obj_decorated_product->get_arr_custom_review();$is_available = $obj_decorated_product->is_available();
The Decorator comes to save us !
Example 2://instantiation, with 4 decoratros$obj_decorated_product = new Integration_MYOB( new Tag_Cloud( new Customer_Review(new Custom_Functionality( new Product))));
//calling 'decorated' methods$arr_custom_review = $obj_decorated_product->get_arr_custom_review();$is_available = $obj_decorated_product->is_available();$arr_tag_cloud = $obj_decorated_product->get_arr_tag_cloud();
Example 3://instantiation, 'decorating' a product object and a category object with the same 'decorators'$obj_decorated_product = new Tag_Cloud( new Customer_Review( new Product));$obj_decorated_category = new Tag_Cloud( new Customer_Review( new Category));
More Examples !
//All the decorators have to extend the Abstract_Decorator class
class Customer_Review_Decorator extends Abstract_Decorator{
public function get_arr_customer_review(){
//insted of using parent::, we need to use $this->object_decorated$this->object_decorated
$entity_id = $this->object_decorated->id;$entity_id = $this->object_decorated->id; ............ ............
return ......;}
}
class Custom_Functionality_Decorator extends Abstract_Decorator{
public function is_available(){
//insted of using parent::, we need to use $this->object_decorated$this->object_decorated
$is_available = $this->object_decorated->is_available();$is_available = $this->object_decorated->is_available(); ............ ............
return ......;}
}
Concrete Decorators
Customer_ReviewTag_Cloud Custom_FuntionalityIntegration_MYOB
get_custom_reviews()get_arr_tag_cloud()do_integration() is_available()
Product
get_price()
is_available()
Decorator
__call()
__get()
__set()
UML Diagram
(PHP 5 !)obj_decorated
abstract class Abstract_Decorator{ /** * It is the object that we are decorating * and we must set it in the contructor */ protected $obj_decorated;
public function __construct( $obj_decorated ) { $this->obj_decorated = $obj_decorated; } /** * If a function is not found in this class the call * came here and we forward to the $this->obj_decorated object */ protected function __call($method,$arguments) { return call_user_func_array(array(&$this->obj_decorated, $method),$arguments); } /** * If a parameter that we are trying to read is not found in this class the call * came here and we forward to the $this->obj_decorated object */ protected function __get($property_name) { return $this->obj_decorated->$property_name; } /** * If a parameter that we are tring to write is not found in this class the call * came here and we forward to the $this->obj_decorated object */ protected function __set($property_name, $property_value) { return $this->obj_decorated->$property_name = $property_value; }
Product
get_price()
is_available()
discount_brand
get_amount()
discount_category
get_amount()
discount_buy_get
get_amount()
discount_highest_price
get_amount()
discount_custom
get_amount()
Refactoring Diary
Composite Pattern
Discount calculations.
Class Product {public function get_price()
{$price =...//now we apply the discounts
$obj_discount_brand = new Discount_Brand($this->id,$qty);$price -= $obj_discount_brand->get_brand_discount();
$obj_discount_category = new Discount_Category();
$obj_discount_category->set_product_id($this->id);$obj_discount_category->set_qty($qty);$price -= $obj_discount_category->get_discount();
$obj_discount_highest_price = new Discount_Highest_Price($this->id,$qty);$price -= $obj_discount_category->get_amount();
$obj_discount_buy_get = new Discount_Buy_Get;$obj_discount_custom1 = new Discount_Custom1;
$obj_discount_custom1 = new Discount_Customer_Big_Clients_EEUU; $obj_discount_custom1 = new Discount_Customer_EEUU; $obj_discount_custom1 = new Discount_Customer_NZ;
return $price;}
}
Class Product {
public function get_price() {$price =...//now we apply the discounts
$obj_discount_brand = new Discount_Brand();$obj_discount_brand->set_product_id($this->id)$obj_discount_brand->set_qty($this->qty)$price -= $obj_discount_brand->get_amount();
$obj_discount_category = new Discount_Category();$obj_discount_category->set_product_id($this->id)$obj_discount_category->set_qty($this->qty)
$price -= $obj_discount_category->get_amount();
$obj_discount_highest_price = new Discount_Highest_Price();............
...............
.............return $price;
}}
A Commun Interface for the Discount classes
and..what if the discounts have a commun interface...
But, if we have different discounts stategies for different projects, we need to updateour Product class.
Our Product class should be the same for all projects.It should be ”Open for extension, but closed for modification”
And which is the primary responsability of the Product class ? Is discount calculations one of these responsabilities? (One responsability Rule).
Open for extension, but closed for modification
Class Product{
public function get_price(){
$price =...;
$obj_discount = new Discount_Composite;$obj_discount->set_product_id($product->id);$obj_discount->set_qty($product->qty);$price -=$obj_discount->get_amount();
return $price}
}
We are still using the same interface for the discount class, that we were using for each strategy:->set_product_id($id)
->set_qty(qty)->get_amount()
The new 'stable' Product class
Decoupling the Discount from the Product class
Decoupling the Discount from the Product class
class Discount{
public function __constructor( ){
$this->_arr_obj_discount_strategy array(new Discount_ Brand,new Discount_Category, new...)}public function set_id($id){
foreach($this->_arr_obj_discount_strategy as &$obj_discount_strategy){
$obj_discount_strategy->set_id($id);}
}public function set_qty($qty){
foreach($this->_arr_obj_discount_strategy as &$obj_discount_strategy){
$obj_discount_strategy->set_qty($qty);}
}public function get_amount(){
foreach($this->_arr_obj_discount_strategy as $obj_discount_strategy){
$amount += $obj_discount_strategy->get_amount();}return $amount;
}}
class Discount_Composite{
public function add_strategy($obj_strategy){
$this->_arr_obj_discount_strategy[]= $obj_strategy;}public function set_id($id){
foreach($this->_arr_obj_discount_strategy as &$obj_discount_strategy){
$obj_discount_strategy->set_id($id);}
}public function set_qty($qty){
foreach($this->_arr_obj_discount_strategy as &$obj_discount_strategy){
$obj_discount_strategy->set_qty($qty);}
}public function get_amount(){
foreach($this->_arr_obj_discount_strategy as $obj_discount_strategy){
$amount += $obj_discount_strategy->get_amount();}return $amount;
}}
Compose objects into tree structures to represent whole-part hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. [GoF, p163]
The discount composite class
Class Product_Controller {
public function product(){
....................
....................$obj_discount = new Discount_Composite$obj_discount->add_strategy(new Discount_Brand);$obj_discount->add_strategy(new Discount_Category);........................................$obj_product->set_obj_discount($obj_discount);
}}
Class Product_Controller {
public function product(){
....................
....................$obj_discount = new Discount_Brand;........................................$obj_product->set_obj_discount($obj_discount);
}}
Class Product_Controller {
public function product(){
....................
....................$obj_discount_composite = new Discount_Composite$obj_discount_composite->add_strategy( new Discount_Brand);$obj_discount_composite->add_strategy( newDiscount_Category);........................................if(is_object($obj_customer)){
$obj_discount_composite_customer = new Discount_Composite$obj_discount_composite_customer->add_strategy('Discount_Seniors');$obj_discount_composite_customer->add_strategy('Discount_Students');
}$obj_discount_composite->add_strategy($obj_discount_composite_customer);
$obj_discount_composite->get_amount($obj_product ->get_id(),$obj_product ->get_qty());}
}
Discount_Interface
set_product_id()
set_qty()
get_amount()
Discount_Composite
add_strategy()
remove_strategy()
Concrete_Discount
Compose objects into tree structures to represent whole-part hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. [GoF, p163]
But, if we have different discounts stategies for different projects, we need to updateour Product class.
Or if a client ask for a different discount strategy, and they want to apply the highest discount only, again, we need to update the Product class;
Our Product class should be the same for our all projects.
It should be ”close to modification and open to extension”