👨💻SOLID
The Basics
Single Responsibility
Class should have one, and only one, reason to change.
<?php
class SalesReporter
{
private $repository;
public function __construct(SalesRepositoryInterface $repository)
{
$this->repository = $repository;
}
public function between(
$startDate, $endDate,
SalesOutputInterface $formatter
)
{
$sales = $this->repository->between($startDate, $endDate);
return $formatter->output($sales);
}
}
interface SalesRepositoryInterface {
public function between(Carbon $startDate, Carbon $endDate);
}
class CollectionSalesRepository implements SalesRepositoryInterface
{
public function between(Carbon $startDate, Carbon $endDate)
{
return collect([[
'created_at' => new Carbon('2021-03-19 14:43:40'),
'charge' => '2111'
]])
->whereBetween('created_at', [$startDate, $endDate])
->sum('charge') / 100;
}
}
interface SalesOutputInterface {
public function output(float $sales);
}
class SalesHtmlOutput implements SalesOutputInterface
{
public function output(float $sales)
{
return "<h1>Sales: $sales</h1>";
}
}
Open-closed principals
Entitles should be open for extension but closed for modification.
Avoid code rot.
Separate extensibility behavior behind an interface, and flip the dependencies.
<?php
interface Shape
{
public function area();
}
class Circle implements Shape
{
protected $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return $this->radius * $this->radius * 3.141592;
}
}
class Square implements Shape
{
protected $width;
protected $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function area()
{
return $this->width * $this->height;
}
}
class AreaCalculate
{
public function calculate($shapes)
{
foreach ($shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
Liskov Substitution
Derivable classes must be substitutable for their base classes.
<?php
interface LessonRepositoryInterface
{
public function getAll(): array;
}
class FileLessonRepository implements LessonRepositoryInterface
{
public function getAll(): array
{
return [];
}
}
class DbLessonRepository implements LessonRepositoryInterface
{
public function getAll(): array
{
return DB::table('lessons')->get()->toArray();
}
}
Interface Segregation
A client should not be force to implement an interface that doesn't use.
<?php
interface WorkableInterface
{
public function work();
}
interface SleepableInterface
{
public function sleep();
}
interface ManageableInterface
{
public function beManaged();
}
class HumanWorker implements WorkableInterface, SleepableInterface, ManageableInterface
{
public function work()
{
}
public function sleep()
{
}
public function beManaged()
{
$this->work();
$this->sleep();
}
}
class AndroidWorker implements WorkableInterface, ManageableInterface
{
public function work()
{
}
public function beManaged()
{
$this->work();
}
}
class Captain
{
public function manage(ManageableInterface $worker)
{
$worker->beManaged();
}
}
Dependency Inversion
Depends on abstraction, not concretion.
Dependency inversion does not equal dependency injection.
That example of bad architecture, because high level module (PasswordReminder
) depends on low level module (MysqlConnection
)
<?php
// Bad example
class PasswordReminder
{
protected MysqlConnection $dbConnection;
public function __construct(MysqlConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
// Good example
interface ConnectionInterface
{
public function connect();
}
class DbConnection implements ConnectionInterface
{
public function connect()
{
}
}
class PasswordReminder
{
protected ConnectionInterface $dbConnection;
public function __construct(ConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Digging Deeper
Implementing the SOLID in Laravel. A Comprehensive Example
Examples get from article
Single Responsibility Principle (SRP)
This principle states that a class should have only one reason to change. In other words, a class should have only one responsibility. Example: Consider a class called Order
. Instead of having all the functionality related to an order in one class, we can break it down into smaller classes like OrderCalculator
, OrderRepository
, and OrderMailer
.
class OrderCalculator
{
public function calculateTotal(Order $order): float
{
// Calculate total price of order
}
}
class OrderMailer
{
public function sendEmail(Order $order, User $user)
{
// Send email to customer with order details
}
}
Open-Closed Principle (OCP)
This principle states that a class should be open for extension but closed for modification. In other words, we should be able to add new functionality without modifying existing code. Example: Consider a class called PaymentGateway
. Instead of modifying this class every time we add a new payment method, we can create a new class for each payment method that extends the PaymentGateway
class.
interface PaymentGateway
{
public function pay(Order $order): bool;
}
class PayPalGateway implements PaymentGateway
{
public function pay(Order $order): bool
{
// Process payment using PayPal API
}
}
class StripeGateway implements PaymentGateway
{
public function pay(Order $order): bool
{
// Process payment using Stripe API
}
}
class OrderProcessor
{
private PaymentGateway $gateway;
public function __construct(PaymentGateway $gateway)
{
$this->gateway = $gateway;
}
public function process(Order $order): void
{
// Process order using PaymentGateway
}
}
// In application service provider
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Liskov Substitution Principle (LSP)
This principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. Example: Consider a class called Shape
with subclasses Circle
, Rectangle
, and Square
. If we have a function that takes an object of type Shape
, we should be able to pass in objects of type Circle
, Rectangle
, or Square
without affecting the behavior of the function.
interface OrderRepository
{
public function save(Order $order): void;
}
class DatabaseOrderRepository implements OrderRepository
{
public function save(Order $order): void
{
// Save order to database
}
}
class InMemoryOrderRepository implements OrderRepository
{
public function save(Order $order): void
{
// Save order to in-memory cache
}
}
class OrderService
{
private OrderRepository $repository;
public function __construct(OrderRepository $repository)
{
$this->repository = $repository;
}
public function placeOrder(Order $order): void
{
// Place order and save to repository
}
}
// In application service provider
$this->app->bind(OrderRepository::class, DatabaseOrderRepository::class);
Interface Segregation Principle (ISP)
This principle states that clients should not be forced to depend on methods they do not use. Example: Consider an interface called PaymentMethod
with methods like pay
, refund
, and getTransactions
. Instead of having all these methods in one interface, we can create separate interfaces for each method and have classes implement only the interfaces they need.
interface OrderTotalCalculator
{
public function calculateTotal(Order $order): float;
}
interface OrderEmailSender
{
public function sendEmail(Order $order, User $user);
}
class OrderProcessor
{
private OrderTotalCalculator $calculator;
private OrderEmailSender $mailer;
public function __construct(
OrderTotalCalculator $calculator,
OrderEmailSender $mailer
)
{
$this->calculator = $calculator;
$this->mailer = $mailer;
}
public function process(Order $order, User $user): void
{
$total = $this->calculator->calculateTotal($order);
$this->mailer->sendEmail($order, $user);
// Process order using total and mailer
}
}
Dependency Inversion Principle (DIP)
This principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Example: Consider a class called Order
that depends on a class called OrderRepository
. Instead of directly instantiating OrderRepository
in Order
, we can use dependency injection to inject an instance of OrderRepository
into Order
.
interface PaymentGateway
{
public function pay(Order $order): bool;
}
interface OrderRepository
{
public function save(Order $order): void;
}
interface OrderTotalCalculator
{
public function calculateTotal(Order $order): float;
}
interface OrderEmailSender
{
public function sendEmail(Order $order, User $user);
}
class OrderProcessor
{
private PaymentGateway $gateway;
private OrderRepository $repository;
private OrderTotalCalculator $calculator;
private OrderEmailSender $mailer;
public function __construct(
PaymentGateway $gateway,
OrderRepository $repository,
OrderTotalCalculator $calculator,
OrderEmailSender $mailer
) {
$this->gateway = $gateway;
$this->repository = $repository;
$this->calculator = $calculator;
$this->mailer = $mailer;
}
public function process(Order $order, User $user): void
{
$total = $this->calculator->calculateTotal($order);
$this->mailer->sendEmail($order, $user);
$this->gateway->pay($order);
$this->repository->save($order);
// Process order using gateway, repository, calculator, and mailer
}
}
Last updated