Share
Embed
Introduction to Design Patterns with PHP
Observer, decorator, factory method, composite… All these words refer to design patterns. Design patterns are general reusable solutions for solving common redundant problems in software architecture. Getting inspiration on some well known design patterns and implementing them in PHP will make your application code easier to understand, maintain and evolve. Based on practical code samples, this talk will focus on the most useful patterns in order to help you design better PHP applications.
Hugo Hamon
November 19, 2013
Transcript
92 slides
-
Overview of OOP in PHP Objects and classes Abstract and
final classes Interfaces Abstract & final Magic methods Garbage collector Object cloning Single inheritance SPL and more… Encapsulation Overloading (a bit...) Classes autoloading Type hinting Reflection Namespaces Traits -
SOLID Principle Single Responsibility Principle Open Close Principle Liskov Substitution
Principle Interface Segregation Principle Dependency Injection Principle -
Single Responsability (SRP) « A class should have one, and
only one, reason to change. » Keep your classes small and focused on small responsabilities. -
class CsvDataImporter { public function import($file) { $records = array();
if (false !== $handle = fopen($file, 'r')) { while ($record = fgetcsv($handle)) { $records[] = $record; } } fclose($handle); try { $this->db->beginTransaction(); foreach ($records as $record) { $stmt = $this->db->prepare('INSERT INTO ...'); $stmt->execute($record); } $this->db->commit(); } catch (PDOException $e) { $this->db->rollback(); throw $e; } } } -
class DataImporter { private $loader; private $table; function __construct(FileLoader $loader,
TableGateway $table) { $this->loader = $loader; $this->table = $table; } public function import($file) { $records = $this->loader->load($file); return $this->table->insert($records); } } -
Open Close (OCP) Software entities should be open to extension
and close to modifications. « You should be able to extend a classe’s behavior, without modifying it. » -
Open Close $loader = new CsvFileLoader(); $loader = new XmlFileLoader();
$loader = new JsonFileLoader(); $importer = new DataImporter($loader, ...); $importer->import('foo.csv|xml|json'); Using a different file loader is now very easy without changing the importer class. -
Liskov Substitution A dependency of type T should be replaced
by another subtype S of T without breaking the code. « Derived classes must be substitutable for their base classes. » -
Liskov Substitution class TextFileLoader implements FileLoader { public function load($file)
{ // ... return [ ... ]; } } class CsvFileLoader extends TextFileLoader { public function load($file) { // ... return [ ... ]; } } -
Interface Segregation (ISP) Interface segregation split larger interfaces into smaller
ones so that clients will only know about the methods they need to work with. « Make fine grained interfaces that are client specific. » -
Interface Segregation interface FileLoader { public function load($file); public function
supports($type); } interface TableGateway { public function insert(array $records); } -
Interface Segregation class DataImporter { public function __construct(FileLoader $loader, TableGateway
$table) { // ... } public function import($file) { $ext = pathinfo($file, PATHINFO_EXTENSION); if (!$this->loader->supports($ext)) { throw new UnsupportedDataFile(); } $records = $this->loader->load($file); return $this->table->insert($records); } } -
Dependency Injection (DIP) D.I is where components are given their
dependencies through their constructors, methods, or directly into fields. « Depend on abstractions, not on concretions. » -
DI – The Wrong Way class DataImporter { private $loader;
private $table; public function __construct() { $this->loader = new CsvFileLoader(); $this->table = new TableGateway(); } } -
DI – The Right Way class DataImporter { private $loader;
private $table; function __construct(FileLoader $loader, TableGateway $table) { $this->loader = $loader; $this->table = $table; } } -
DI – Injecting dependencies $importer = new DataImporter( new CsvFileLoader(),
new TableGateway('data', new PDO(…)) ); $importer->import('foo1.csv'); $importer->import('foo2.csv'); $importer->import('foo3.csv'); -
Definition In software design, a design pattern is an abstract
generic solution to solve a particular redundant problem. -
Design patterns families Abstract Factory Builder Factory Method Lazy Initialization
Prototype Singleton Adapter Bridge Composite Decorator Facade Flyweight Proxy Chain of Responsability Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor Creational Structural Behavioral -
The Context A web store wants to sell hard and
digital products. Shipment fees are calculated based on the product caracteristics (volume, mass, type…) Products are shipped to customers. -
Common implementations 2. Treating XML nodes. 1. Representing categories trees.
3. Dealing with embedded forms. -
Concrete Example My web store must be able to sell
single items or grouped items. -
abstract class Product { protected $name; protected $price; public function
__construct($name, Money $price) { $this->name = $name; $this->price = $price; } abstract public function getMass(); abstract public function getVolume(); // ... a bunch of getters } -
class HardProduct extends Product { // ... private $mass; private
$volume; public function setPhysics(Mass $mass, Volume $volume) { $this->mass = $mass; $this->volume = $volume; } public function getMass() { return $this->mass; } public function getVolume() { return $this->volume; } } -
class DigitalProduct extends Product { public function getMass() { return
new Mass(0); } public function getVolume() { return new Volume(0); } } -
class Bundle extends Product { protected $products; public function __construct($name)
{ $this->name = $name; } public function setPrice(Money $price) { $this->price = $price; } public function add(ProductInterface $product) { $this->products[] = $product; } } -
class Bundle extends Product { public function getMass() { $mass
= new Mass(0); foreach ($this->products as $product) { $mass = $mass->add($product->getMass()); } return $mass; } public function getVolume() { $volume = new Volume(0); foreach ($this->products as $product) { $volume = $volume->add($product->getVolume()); } return $volume; } } -
class Bundle extends Product { public function getPrice() { if
($this->price) { return $this->price; } $price = $this->products[0]->getPrice(); for ($i = 1; $i < count($this->products); $i++) { $product = $this->products[$i]; $price = $price->add($product->getPrice()); } return $price; } } -
$b1 = new Bundle('C'); $b1->setPrice(new Money(39)); $b1->addProduct(new HardProduct('A', new Money(29)));
$b1->addProduct(new DigitalProduct('B', new Money(20))); echo $b1->getPrice(); // 39 EUR $b2 = new Bundle('D'); $b2->addProduct(new HardProduct('C', new Money(12))); echo $b2->getPrice(); // 12 EUR Usage: creating products -
$b1 = new Bundle('C'); $b1->setPrice(new Money(39)); $b1->addProduct(new HardProduct('A', new Money(29)));
$b1->addProduct(new DigitalProduct('B', new Money(20))); $b2 = new Bundle('D'); $b2->addProduct(new HardProduct('C', new Money(12))); $b3 = new Bundle('E'); $b3->addProduct($b1); $b3->addProduct($b2); echo $b3->getPrice(); // 39 + 12 = 51 EUR Usage: embedding bundles -
Common implementations 1. Adding some caching capability to a web
service client to avoid network calls if not needed. 2. Adding some logging features to an object in a dev environment. For instance, a database connection. -
Concrete Example We want to offer discounts on orders without
breaking existing domain model. Discount strategies may vary depending on the context. -
class Order implements OrderInterface { private $products = array(); public
function addProduct(Product $product) { $this->products[] = $product; } public function getAmount() { $total = new Money(0); foreach ($this->products as $product) { $total = $total->add($product->getPrice()); } return $total; } } -
abstract class OrderDecorator implements OrderInterface { protected $order; public function
__construct(OrderInterface $order) { $this->order = $order; } public function addProduct(Product $product) { return $this->order->addProduct($product); } } -
class BasicValueDiscount extends OrderDecorator { private $discount; function __construct(OrderInterface $order,
Money $discount) { parent::__construct($order); $this->discount = $discount; } function getAmount() { $amount = $this->order->getAmount(); return $amount->subtract($this->discount); } } -
$order = new Order(...); $order->addProduct(new DigitalProduct('A', new Money(15))); $order->addProduct(new DigitalProduct('B',
new Money(10))); // Substract 5 euros to the total $order1 = new BasicValueDiscount($order, new Money(5)); $order1->getAmount(); // 20 EUR // Substract 50% of the total $order2 = new BasicRateDiscount($order, 50); $order2->getAmount(); // 12.50 EUR // Combine discounts together $order3 = new BasicRateDiscount( new BasicValueDiscount($order, new Money(5)), 50 ); $order3->getAmount(); // 10 EUR Usage -
Other discount strategies… + Minimum purchase amount required + Minimum
ordered items required + Loyal clients only + Special first order discount + … -
Pros and cons + Easy way to extend an object
capabilities + No need to change the underlying code - Object construction becomes more complex - Difficulty to test the concrete object type -
The strategy pattern encapsulates algorithms of the same nature into
dedicated classes to make them interchangeable. Goal -
Common implementations 1. Switching to another storage engine to store
session data. 2. Comparing and sorting elements of an array. 3. Sending emails with SMTP, Sendmail or the mail() function. -
class Swift_Mailer { private $transport; public function __construct(Swift_Transport $transport) {
$this->transport = $transport; } public function send(Swift_Message $message) { // do stuff… $this->transport->send($message); } } Swiftmailer example -
class Swift_MailTransport extends Swift_Transport { public function send(Swift_Message $message) {
// use php mail() function to send a message } } class Swift_SmtpTransport extends Swift_Transport { public function send(Swift_Message $message) { // use an SMTP connection to send a message } } class Swift_NullTransport extends Swift_Transport { public function send(Swift_Message $message) { // fake email sending } } -
Concrete example We want to calculate the order shipment fees
depending on the chosen delivery strategy. The customer can choose between 3 delivery strategies. -
Concrete example (wrong!!!) class OrderService { function priceShippingCost(OrderInterface $order, $type)
{ switch ($type) { case 'warehouse-pickup': return new Money(0); case 'regular': return new Money(7); case 'express': return new Money(10); } } } -
Encapsulating algorithms interface ShippingStrategy { function calculateShippingCost(OrderInterface $order); } class
WarehousePickup implements ShippingStrategy { function priceShippingCost(OrderInterface $order) { return new Money(0); } } -
Encapsulating algorithms class RegularShipping implements ShippingStrategy { function priceShippingCost(OrderInterface $order)
{ if ($order->getAmount() > 100) { return new Money(0); } if (count($order->getProduct()) > 3) { return new Money(12); } return new Money(7); } } -
Encapsulating algorithms class ExpressShipping implements ShippingStrategy { function priceShippingCost(OrderInterface $order)
{ if ($order->getWeight() < 5) { return new Money(6); } if ($order->getWeight() >= 5 && $order->getWeight() < 10) { return new Money(9); } return new Money(15); } } -
class OrderService { public function priceShippingCost( OrderInterface $order, ShippingStrategy $shipping
) { // ... do stuff $fare = $shipping->priceShippingCost($order); $order->getAmount()->add($fare); // ... do stuff } } Strategy can be given as an argument of the client’s method. -
class OrderService { private $shippingStrategy; public function __construct(ShippingStrategy $shipping) {
$this->shippingStrategy = $shipping; } public function priceShippingCost(OrderInterface $order, $delivery) { // ... do stuff $fare = $this->shippingStrategy->priceShippingCost($order); $order->getAmount()->add($fare); // ... do stuff } } Or it can be injected in the constructor method of the client. -
Goal Define an interface for creating an object, but let
subclasses decide which class to instantiate. -
Concrete example We want to encapsulate the way we create
hard products, digital products and bundles. The goal is to keep instantiation processes in one single place. -
abstract class ProductFactory { public function __construct(ReferenceGenerator $generator) { $this->refGenerator
= $generator; } abstract protected function createProduct($name, $price, $mass = null, $volume = null); public function newProduct($name, $price, $mass = null, $volume = null) { $price = new Price($price, new Currency('EUR')); $product = $this->createProduct($name, $price, $mass, $volume); $product->setReference($this->refGenerator->generate($product)); return $product; } } Usage -
class HardProductFactory extends ProductFactory { protected function createProduct( $name, Price
$price, Mass $mass = null, Volume $volume = null ) { $product = new HardProduct($name, $price); $product->setPhysics($mass, $volume); return $product; } } -
class DigitalProductFactory extends ProductFactory { protected function createProduct( $name, Price
$price, Mass $mass = null, Volume $volume = null ) { return new DigitalProduct($name, $price); } } -
class BundleFactory extends ProductFactory { protected function createProduct( $name, Price
$price, Mass $mass = null, Volume $volume = null ) { return new Bundle($name, $price); } } -
$generator = new ReferenceGenerator(); $factory = new HardProductFactory($generator); $product =
$factory->newProduct('A', 50, 4, 10); $factory = new DigitalProductFactory($generator); $product = $factory->newProduct('B', 25); $factory = new BundleFactory($generator); $bundle = $factory->newProduct('C', 99); Usage -
Goal A subject, the observable, emits a signal to a
list of modules known as observers. -
class Customer { public function pay(Order $order, Money $amount) {
$order->setPaid(); if ($this->logger) { $this->logger->log('New order...'); } $mail = new Email(); $mail->recipient = $this->customer->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } } -
interface ObserverInterface { function notify(ObservableInterface $subject); } interface ObservableInterface {
function attach(ObserverInterface $observer); function notifyObservers(); } -
class LoggerHandler implements ObserverInterface { public $logger; public function notify(ObservableInterface
$subject) { $order = $subject->getLastOrder(); $reference = $order->getReference(); $this->logger->log('New order #'. $reference); } } -
class CustomerNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface
$subject) { $mail = new Email(); $mail->recipient = $subject->getEmail(); $mail->subject = 'Your order!'; $mail->message = 'Thanks for ordering...'; $this->mailer->send($mail); } } -
class SalesNotifier implements ObserverInterface { public $mailer; public function notify(ObservableInterface
$subject) { $mail = new Email(); $mail->recipient = '[email protected]'; $mail->subject = 'New order to ship!'; $mail->message = '...'; $this->mailer->send($mail); } } -
class Customer implements ObservableInterface { // ... private $observers; public
function attach(ObserverInterface $observer) { $this->observers[] = $observer; } public function notifyObservers() { foreach ($this->observers as $observer) { $observer->notify($this); } } } -
class Customer implements ObservableInterface { public function pay(Order $order) {
$order->setPaid(); $this->notifyObservers(); } } -
$client = new Customer(); $client->attach(new LoggerNotifier($logger)); $client->attach(new CustomerNotifier($mailer)); $client->attach(new SalesNotifier($mailer));
$order = new Order(); $client->pay($order, new Money(150));