General, reusable solution to a commonly recurring problem
Not a finished code snippet, but rather a template for solving a problem
Originated as an architectural concept by Christopher Alexander in 1977
Presented in a software design context in 1987 by Kent Beck & Ward Cunningham
Popularized in 1994 by the "GoF"
"Design Patterns: Elements of Reusable Object-Oriented Software" by the so-called "Gang of Four" (Gamma et al.)
Provides you with a tool to communicate
Provides you with a tool to think
One of the most important aspects of OOP
Controls coupling
Controls lifecycle
Causes direct coupling with the class to instantiate
Requires knowledge about and access to constructor arguments
Often seen as <class>::getInstance()
Causes direct coupling with the class to instantiate
Class serves two roles: its instantiation + its actual purpose
Can provide multiple, differing constructors
The Simple Factory creates objects without specifying the exact class to create
Can cause direct coupling with the factory class, but...
Can be injected
interface SocialNetwork {
public function render_feed(): string;
}
final class Twitter implements SocialNetwork {
public function render_feed(): string { /*...*/ }
}
final class Facebook implements SocialNetwork {
public function render_feed(): string { /*...*/ }
}
final class SocialNetworkFactory {
function create( string $type ): SocialNetwork {
switch ( $type ) {
case 'twitter': return new Twitter();
case 'facebook': return new Facebook();
/* [...] */
}
}
}
function get_social_media_feed( string $type ): string {
$social_network = ( new SocialNetworkFactory )->create( $type );
return $social_network->render_feed();
}
The Abstract Factory groups object factories that have a common theme
Will be injected (as an abstract class cannot be instantiated)
Allows the factory to be replaced
Ideally, you only use new
for the following cases:
Instantiating a "newable type"
— Value Objects
— Exceptions
— Pattern objects like Simple Factories, Specifications, ...
Inside of any type of factory
The Template Method defines the skeleton of an algorithm as an abstract class, allowing its subclasses to provide concrete behavior
Provide a skeleton for your extensible logic
Serves as documentation for available extension steps
abstract class PaymentGateway {
protected $logger;
function __construct( Logger $logger ) {
$this->logger = $logger;
}
function checkout(
Customer $customer,
Product $product
): Order {
$this->logger->info( 'Creating customer...' );
$token = $this->create_customer( $customer );
$this->logger->info( 'Adding payment data...' );
$this->add_payment_data( $token );
$this->logger->info( 'Processing charge...' );
return $this->charge( $product->price, $token );
}
abstract function create_customer(
Customer $customer
): Token;
abstract function add_payment_data(
Token $token,
Customer $customer
);
abstract function charge( Token $token, $price): Order;
}
final class PayPalGateway extends PaymentGateway {
function create_customer(
Customer $customer
): Token {
$this->customer = $customer;
return PayPal::create_customer( $customer );
}
function add_payment_data(
Token $token,
Customer $customer
) {
PayPal::add_payment_data( $token, [
'address' => $customer->address,
'credit_card' => $customer->credit_card,
] );
}
function charge( Token $token, $price): Order {
PayPal::charge( $token, Money::USD( $price ) );
}
}
Start by building two different implementations and then refactor
Starting with the abstraction can lead to impractical design
Think hard about method names and signatures
If you need to add lots of comments or documentation, you failed
Use Value Objects as arguments for further hardening
Input validation can be encapsulated into the Value Objects, leaving implementations free to concentrate on actual logic
Caching is what you would call a "cross-cutting concern".
Where would you put the caching logic for a remote data request?
... Single-Responsibility Principle ... Coupling to infrastructure ... Mixing technical implementation details with business logic ...
=> Ideally, neither the data producer, nor the data consumer should be aware of caching!
The Decorator adds responsibilities to an object without modifying the object itself
Can wrap existing code with additional logic to adapt its behavior
Interjects between producer and consumer with both being unaware
Does not require changes to producer or consumer
interface Data { public function get(); }
class RemoteData implements Data {
function get() {
// This expensive operation should be cached.
$data = $this->process_remote_request();
return $data;
}
}
class Renderer {
function __construct( Data $data ) {}
function render() {
$data = $this->data->get();
return "Data retrieved: {$data}";
}
}
// Bootstrapping code (for ex. in a DI container).
$data = new RemoteData();
$renderer = new Renderer( $data );
// Actual consuming logic in the application.
echo $renderer->render();
interface Data { public function get(); }
class RemoteData implements Data {
function get() {
// Check cache first and fall back to remote.
$data = Cache::get( $key );
if ( ! $data ) {
$data = $this->provess_remote_request();
Cache::set( $key, $data );
}
return $data;
}
}
class Renderer {
function __construct( Data $data ) {}
function render() {
$data = $this->data->get();
return "Data retrieved: {$data}";
}
}
// Bootstrapping code (for ex. in a DI container).
$data = new RemoteData();
$renderer = new Renderer( $data );
// Actual consuming logic in the application.
echo $renderer->render();
interface Data { public function get(); }
class RemoteData implements Data {
function get() {
// Just pure logic here.
$data = $this->process_remote_request();
return $data;
}
}
class CachedData implements Data {
function __construct( Data $data_to_cache ) {}
function get() {
$data = Cache::get( $key );
if ( ! $data ) {
$data = $this->data_to_cache->get();
Cache::set( $key, $data );
}
return $data;
}
}
class Renderer {
function __construct( Data $data ) {}
function render() {
$data = $this->data->get();
return "Data retrieved: {$data}";
}
}
$data = new RemoteData();
// The Decorator "wraps" the original object.
$cached_data = new CachedData( $data );
$renderer = new Renderer( $cached_data );
// Consuming logic still unchanged.
echo $renderer->render();
Use Decorators for adding cross-cutting concerns to existing objects
Typical cross-cutting concerns are caching, logging, ...
Decorators work best when coupling them with a central Dependency Injection Container
This lets you add Decorators transparently without requiring any changes outside of the DIC configuration
The Template View renders information into a presentable form (usually HTML) by replacing placeholders in a provided template
class View {
protected $path;
function __construct( string $name ) {
$path = PLUGIN_DIR . $name;
if ( ! is_readable( $path ) ) {
throw new RuntimeException( 'Invalid View name' );
}
$this->path = $path;
}
function render( array $context = [] ): string {
// Make sure the context is available within the
// included scope.
extract( $context );
// Capture template output using output buffering.
ob_start();
include $this->path;
return ob_get_clean();
}
}
<?php // View template file: templates/movie.php ?>
<h1 class="title"><?= $this->title ?></h1>
<h2 class="subtitle"><?= $this->sub_title ?></h2>
<p class="description"><?= $this->description ?></p>
$movie_data = [
'title' => 'The Compiling',
'subtitle' => 'All compile and no run '
. 'give Jack a sore bun',
'description' => 'Jack agrees to take care of an '
. 'isolated hotel during off-season in '
. 'order to work on his software '
. 'project. However, the network in '
. 'the hotel is so slow that compile '
. 'times are extremely long and slowly '
. 'drive Jack mad.'
];
$view = new View( 'templates/movie.php' );
echo $view->render( $movie_data );
Use external package for Views
Use relative paths with multiple locations
This allows for easy plugin -> child theme -> parent theme hierarchy
Allow for nested partials for flexible overriding
Ex.: Only override checkout-button
instead of entire shopping-cart
The Strategy pattern allows one of a family of algorithms to be selected on-the-fly at runtime
Change algorithms independently from clients that use them
Make algorithms reusable across multiple use cases
final class UserList {
private $users = [];
function add_user( $user_data ) { /* ... */ }
function get_sorted_by( string $criteria ) {
return usort( $this->users, function ( $a, $b ) {
switch ( $criteria ) {
case 'phone':
// Group by local numbers first.
if ( $a->phone === $b->phone ) { return 0; }
if ( $a->phone->is_local() ) {
return $b->phone->is_local()
? ( $a->phone < $b->phone ? -1 : 1 );
: -1;
}
if ( $b->phone->is_local() { return 1; }
return return $a->phone < $b->phone ? -1 : 1;
case 'email':
return strcmp( $a->email, $b->email );
case 'name':
default:
return strcmp( $a->name, $b->name );
}
} );
}
}
// Retrieve a list of users and add them to a user list.
$user_list = new UserList();
$file = fopen( 'users.csv', 'r' );
$user_data = fgetcsv( $file );
array_map( [ $user_list, 'add_user' ], $user_data );
// Print list of users sorted by grouped phone numbers.
$users = $user_list->get_sorted_by( 'phone' );
print_r( $users );
final class UserList {
private $users = [];
function add_user( $user_data ) { /* ... */ }
function get_sorted_by( SortOrder $criteria ) {
return usort(
$this->users,
[ $criteria, 'compare' ]
);
}
}
interface SortOrder {
function compare( User $a, User $b ): int;
}
// Retrieve a list of users and add them to a user list.
$user_list = new UserList();
$file = fopen( 'users.csv', 'r' );
$user_data = fgetcsv( $file );
array_map( [ $user_list, 'add_user' ], $user_data );
// Print list of users sorted by grouped phone numbers.
$users = $user_list->get_sorted_by( new SortByPhone );
print_r( $users );
final class SortByName implements SortOrder {
function compare( User $a, User $b ): int {
return strcmp( $a->name, $b->name );
}
}
final class SortByEmail implements SortOrder {
function compare( User $a, User $b ): int {
return strcmp( $a->email, $b->email );
}
}
final class SortByPhone implements SortOrder {
function compare( User $a, User $b ): int {
if ( $a->phone === $b->phone ) { return 0; }
if ( $a->phone->is_local() ) {
return $b->phone->is_local()
? ( $a->phone < $b->phone ? -1 : 1 );
: -1;
}
if ( $b->phone->is_local() { return 1; }
return $a->phone < $b->phone ? -1 : 1;
}
}
The Strategy pattern helps eliminate branching code
Look for switch
statements or long if/else
chains
Instead of injecting them, you can also combine them with a factory
Ex. ( new SortOrderFactory )->create( 'name' );
Handling error conditions detracts from actual logic
Branching for error handling can introduce logical bugs
Wrong return types can travel through code and break in unrelated parts
The Null Object turns an error condition into a valid, neutral object doing nothing
Null Object is a valid, usable result
It's behavior is "neutral" relative to its context
Allows for error handling without branching code
class HtmlView implements View {
public function render() {
return "<div>{$this->data}</div>";
}
}
class Presenter {
public function get_view( $data ) {
if ( ! $this->is_valid( $data ) ) {
return false; // Without valid data, we cannot instantiate a View, so return false.
}
return new HtmlView( $data ); // This returns an implementation of the View interface.
}
}
$view = ( new Presenter )->get_view( $data );
// This could throw an error, calling "render()" on "false"...
echo $view->render();
class HtmlView implements View {
public function render() {
return "<div>{$this->data}</div>";
}
}
class Presenter {
public function get_view( $data ) {
if ( ! $this->is_valid( $data ) ) {
return false; // Without valid data, we cannot instantiate a View, so return false.
}
return new HtmlView( $data ); // This returns an implementation of the View interface.
}
}
$view = ( new Presenter )->get_view( $data );
// We need branching here to take error condition into account.
echo $view ? $view->render() : '';
final class HtmlView implements View {
public function render() {
return "<div>{$this->data}</div>";
}
}
final class NullView implements View {
public function render() {
return '';
}
}
class Presenter {
public function get_view( $data ) {
if ( ! $this->is_valid( $data ) ) {
return new NullView(); // Without valid data, we return a View that just does nothing.
}
return new HtmlView( $data ); // This returns an implementation of the View interface.
}
}
$view = ( new Presenter )->get_view( $data );
// No branching needing, worst case scenario is now an empty output instead of a hard error.
echo $view->render();
Null Objects can typically combined with factories to get rid of exceptions
Ex. the default:
case in a factory could return a Null Object.
They are also a good candidate as the result of a catch()
clause
Ex. if ( ! is_readable( $view_path ) ) { return new NullView; }
The Value Object is a domain object whose equality isn't based on identity
Object-oriented extension of the language's type system
Does not have an ID per object, as opposed to Entities
Contains its own validation logic, as opposed to scalar types
Prefer immutability, to avoid aliasing bugs
final class EmailAddress {
private $value;
public function __construct( $value ) {
$filtered_value = filter_var(
$value, FILTER_VALIDATE_EMAIL
);
if ( $filtered_value === false ) {
throw new InvalidArgumentException(
"Invalid email address: {$value}"
);
}
$this->value = $filtered_value;
}
public function get_local_part() {
return array_unshift( explode( '@', $this->value ) );
}
public function get_domain() {
return array_pop( explode( '@', $this->value ) );
}
public function __toString() { return $this->value; }
}
class EmailSender {
protected $provider;
protected $addresses = [];
public function __construct( MailProvider $provider ) {
$this->provider = $provider;
}
public function add( EmailAddress $address ) {
// Only valid addresses pass the type declaration.
$this->addresses[] = $address;
return $this;
}
public function send( $message ) {
foreach ( $addresses as $address ) {
MailProvider::send( (string) $address, $message );
} }
}
( new EmailSender( new AmazonSESProvider ) )
->add( new EmailAddress( 'user_a@example.com' ) )
->add( new EmailAddress( 'user_b@example.com' ) )
->send( $message );
// We want to consider Doc Brown's version of 1955.
$point_in_time = new DateTime( 'November 5, 1955' );
$doc_brown->set_time( $point_in_time );
// Add a time jump of 30 years to consider Marty's "present" version.
$time_jump = DateInterval::createfromdatestring( '+ 30 years' );
$marty->set_time( $point_in_time->add( $time_jump ) );
echo 'Doc B: ' . $doc_brown->get_time()->format( 'M d, Y' ) . "\n";
echo 'Marty: ' . $marty->get_time()->format( 'M d, Y' ) . "\n";
Doc B: November 5, 1985
Marty: November 5, 1985
// We want to consider Doc Brown's version of 1955.
$point_in_time = new DateTimeImmutable( 'November 5, 1955' );
$doc_brown->set_time( $point_in_time );
// Add a time jump of 30 years to consider Marty's "present" version.
$time_jump = DateInterval::createfromdatestring( '+ 30 years' );
$marty->set_time( $point_in_time->add( $time_jump ) );
echo 'Doc B: ' . $doc_brown->get_time()->format( 'M d, Y' ) . "\n";
echo 'Marty: ' . $marty->get_time()->format( 'M d, Y' ) . "\n";
Doc B: November 5, 1955
Marty: November 5, 1985
Use them with abundance, especially as type declarations
They not only make some types of bugs impossible, they also drastically improve the quality of a static analysis, immediately pin-pointing design issues
Prefer immutability
As shown, making changes to object instances can introduce aliasing bugs
Most scalar values can be replaced by either constants or Value Objects
Only use scalar values if that is the actual concept you want to represent
The Proxy provides a placeholder for another object to control access, reduce cost, and reduce complexity
interface Database {
public function query( $sql ): array;
}
final class MySQLDatabase implements Database {
public function __construct() {
$this->do_very_expensive_initialization();
}
public function query( $sql ): array;
}
class Plugin {
private $database;
public function __construct( Database $database ) {
$this->$database = $database;
}
public function do_stuff( $stuff ) {
$this->process( $stuff );
}
public function run_query( $sql ) {
echo $this->database->query( $sql );
}
}
// As we need to inject the Database dependency into
// the Plugin object, we immediately incur the
// expensive initialisation overhead.
$database = new MySQLDatabase();
$plugin = new Plugin( $database );
// We ignore whether the methods will actually
// be called.
add_action( 'stuff_to_do', [ $plugin, 'do_stuff' ] );
add_action( 'request', [ $plugin, 'run_query' ] );
interface Database {
public function query( $sql ): array;
}
final class MySQLDatabase implements Database {
public function __construct() {
$this->do_very_expensive_initialization();
}
public function query( $sql ): array;
}
final class DatabaseProxy implements Database {
private $database;
public function query( $sql ): array {
if ( $this->database === null ) {
$this->database = new MySQLDatabase();
}
return $this->database->query( $sql );
}
}
class Plugin {
private $database;
public function __construct( Database $database ) {
$this->$database = $database;
}
public function do_stuff( $stuff ) {
$this->process( $stuff );
}
public function run_query( $sql ) {
echo $this->database->query( $sql );
}
}
// By using a Proxy object instead we can both fulfil
// the dependency and avoid the initialisation
// overhead until it is actually needed.
$database = new DatabaseProxy();
$plugin = new Plugin( $database );
// We ignore whether the methods will actually
// be called.
add_action( 'stuff_to_do', [ $plugin, 'do_stuff' ] );
add_action( 'request', [ $plugin, 'run_query' ] );
interface Database {
public function query( $sql ): array;
}
final class MySQLDatabase implements Database {
public function __construct() {
$this->do_very_expensive_initialization();
}
public function query( $sql ): array;
}
final class DatabaseProxy implements Database {
public function query( $sql ): array {
return $this->database->query( $sql );
}
public function __get( $property ) {
if ( $property === 'database' ) {
$this->database = new MySQLDatabase();
}
}
}
class Plugin {
private $database;
public function __construct( Database $database ) {
$this->$database = $database;
}
public function do_stuff( $stuff ) {
$this->process( $stuff );
}
public function run_query( $sql ) {
echo $this->database->query( $sql );
}
}
// By using a Proxy object instead we can both fulfil
// the dependency and avoid the initialisation
// overhead until it is actually needed.
$database = new DatabaseProxy();
$plugin = new Plugin( $database );
// We ignore whether the methods will actually
// be called.
add_action( 'stuff_to_do', [ $plugin, 'do_stuff' ] );
add_action( 'request', [ $plugin, 'run_query' ] );
Use proxies to make an entire collection of subsystems "load-on-demand"
Put at the root of an entire object tree, they can have a big impact
Don't code them by hand, use a mature and tested library
The Observer is a publish/subscribe pattern which allows a number of observer objects to be notified of an event
Subjects Observers is similar to Publishers Subscribers
Decouples the notifier from the object to be notified through a common interface
The Publish-Subscribe decouples publishers of messages from subscribers by categorizing messages into classes
Decouples the publisher from the subscriber
Provides a grouping mechanism
The Singleton ensures a class has only one instance, and provides a global point of access to it
Breaks the S ingle Responsibility Principle
Breaks the O pen/Closed Principle
Breaks the L iskov Substitution Principle
Skips the I nterface Segregation Principle (doesn't have an interface)
Breaks the D ependency Inversion Principle
More at: https://www.alainschlesser.com/singletons-shared-instances/
Design Patterns are a concept and tool, not a specific piece of code
Design Patterns simplify communication
Design Patterns shape your thinking
Start with these:
Simple Factory
Template Method
Decorator
Template View
Design Patterns: Elements of Reusable Object-Oriented Software (GOF)
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994)
Patterns of Enterprise Application Architecture (PEAA)
Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford (2002)
I'm Alain Schlesser.
Follow me on Twitter:
@schlesseraOr visit my Personal Blog:
www.alainschlesser.com