Delegate Pattern

Imagine you're a developer building an e-commerce platform. Your Product class holds information about various items and their functionalities. But displaying a product on a search result page or a dedicated product page requires slightly different formatting and data manipulation. How can you achieve this flexibility without bloating the Product class with context-specific logic?

This is where the Delegate Design Pattern comes in. It allows you to delegate specific tasks to specialized helper objects, promoting loose coupling and separation of concerns in your code. In our example, you could create a ProductDisplayDelegate interface that outlines methods for formatting product information (price, description, etc.). Different concrete classes like SearchResultDisplay and ProductPageDisplay can then implement this interface, handling the specific formatting logic for each context.

The beauty lies in the loose coupling achieved. The Product class doesn't depend on the specific display implementations. It simply relies on the interface, making it easier to add new display formats in the future without modifying the core product logic. Additionally, separation of concerns keeps the Product class focused on product data and functionality, while formatting logic resides in dedicated classes, promoting cleaner and more maintainable code.

The Delegate Pattern shines in various scenarios. For instance, validation logic can be delegated to specific validator classes based on data type or context. Data access can be handled by different repository classes depending on the data source (database, API). Similarly, event handling can be delegated to listener objects based on the event type.

However, like any design pattern, delegation isn't a one-size-fits-all solution. For trivial tasks, the overhead of creating interfaces and delegate objects might outweigh the benefits. Additionally, if the delegator and delegate objects are tightly coupled and need to share a lot of state, inheritance might be a better choice.

Let's solidify our understanding with a practical example. Here's how the Delegate Pattern can be implemented for product display in our e-commerce platform:

// Interface for product display formatting
interface ProductDisplayDelegate
{
  public function formatPrice(Product $product): string;
  public function formatDescription(Product $product): string;
}

// Concrete class for search result display
class SearchResultDisplay implements ProductDisplayDelegate
{
  public function formatPrice(Product $product): string
  {
    return number_format($product->getPrice(), 2) . ' USD';
  }

  public function formatDescription(Product $product): string
  {
    return substr($product->getDescription(), 0, 100) . '...';
  }
}

// Concrete class for product page display
class ProductPageDisplay implements ProductDisplayDelegate
{
  public function formatPrice(Product $product): string
  {
    return $product->getPriceWithDiscount() . ' (discounted)';
  }

  public function formatDescription(Product $product): string
  {
    return $product->getDescription();
  }
}

// Product class with delegate property
class Product
{
  private $delegate;

  public function __construct(ProductDisplayDelegate $delegate)
  {
    $this->delegate = $delegate;
  }

  public function getPrice(): float
  {
    // ... product price logic
  }

  public function getDescription(): string
  {
    // ... product description logic
  }

  public function getPriceWithDiscount(): string
  {
    // ... discount logic
  }

  public function display()
  {
    echo $this->delegate->formatPrice($this);
    echo '<br>';
    echo $this->delegate->formatDescription($this);
  }
}

// Usage
$searchResultDisplay = new SearchResultDisplay();
$productPageDisplay = new ProductPageDisplay();

$product = new Product($searchResultDisplay);
$product->display();  // Output formatted for search results

$product->setDelegate($productPageDisplay);  // Change delegate
$product->display();  // Output formatted for product page

In this example, the Product class can delegate product display formatting to different concrete classes (SearchResultDisplay and ProductPageDisplay) based on the assigned delegate. This allows for dynamic behavior and context-specific formatting without modifying the core product logic.

By understanding the Delegate Design Pattern's strengths and weaknesses, you can leverage it effectively to create well-structured, adaptable, and maintainable PHP applications. It empowers you to delegate specific functionalities to specialized objects, promoting loose coupling and a clean separation of concerns in your codebase. So, the next time you encounter a situation requiring flexible behavior based on context, consider the Delegate Design Pattern as a powerful tool in your object-oriented programming arsenal.