В предыдущем посте мы рассмотрели принципы, которых следует избегать при написании кода. Вы можете прочитать это здесь". В этом посте мы рассмотрим принципы проектирования SOLID, стандарты программирования, которые все разработчики должны хорошо понимать, чтобы создавать хорошую архитектуру.

SOLID — это мнемоническая аббревиатура:

  • Единая ответственность
  • Открыто закрыто
  • Замена Лискова
  • Разделение интерфейса
  • Инверсия зависимости

Единая ответственность

  • Каждому объекту должна быть назначена одна единственная ответственность.
  • Конкретный класс должен решать конкретную задачу.

ГЛУПЫЙ пример

class Order
{
    public function calculateTotalSum() {...}
    public function getItems() {...}
    public function getItemCount() {...}
    public function addItem() {...}
    public function deleteItem() {...}

    public function load() {...}
    public function save() {...}
    public function update() {...}
    public function delete() {...}

    public function printOrder() {...}
    public function showOrder() {...}
}

НАДЕЖНЫЙ пример

class Order
{
    public function calculateTotalSum() {...}
    public function getItems() {...}
    public function getItemCount() {...}
    public function addItem() {...}
    public function deleteItem() {...}
}

class OrderRepository
{
    public function load() {...}
    public function save() {...}
    public function update() {...}
    public function delete() {...}
}

class OrderViewer
{
    public function printOrder() {...}
    public function showOrder() {...}
}

Открыто закрыто

  • Программные объекты должны быть открыты для расширения, но закрыты для модификации.
  • Все классы, функции и т.п. должны быть спроектированы так, чтобы для изменения их поведения не требовалось изменять их исходный код.

ГЛУПЫЙ пример

class OrderRepository
{
    public function load($orderId)
    {
        $pdo = new PDO($this->config->getDsn(), $this->config->getDBUser(), $this->config->getDBPassword());
        $statement = $pdo->prepare('SELECT * FROM `orders` WHERE id=:id');
        $statement->execute(array(':id' => $orderId));
        return $query->fetchObject('Order');
    }
    public function save($order) {...}
    public function update($order) {...}
    public function delete($order) {...}
}

НАДЕЖНЫЙ пример

{
    private $source;
    
    public function setSource(IOrderSource $source)
    {
        $this->source = $source;
    }
    
    public function load($orderId)
    {
        return $this->source->load($orderId);
    }

    public function save($order)
    {
        return $this->source->save($order);
    }

    public function update($order) {...};
    public function delete($order) {...};
}

interface IOrderSource
{
    public function load($orderId);
    public function save($order);
    public function update($order);
    public function delete($order);
}

class MySQLOrderSource implements IOrderSource
{
    public function load($orderId) {...};
    public function save($order) {...}
    public function update($order) {...}
    public function delete($order) {...}
}

class ApiOrderSource implements IOrderSource
{
    public function load($orderId) {...};
    public function save($order) {...}
    public function update($order) {...}
    public function delete($order) {...}
}

Замена Лискова

  • Объекты в программе могут быть заменены их наследниками без изменения свойств программы.
  • При использовании наследника класса результат выполнения кода должен быть предсказуемым и не изменять свойства метода.
  • Должна быть возможность замены любого подтипа базового типа.
  • Функции, использующие ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

ГЛУПЫЙ пример

class Rectangle
{
    protected $width;
    protected $height;

    public setWidth($width)
    {
        $this->width = $width;
    }

    public setHeight($height)
    {
        $this->height = $height;
    }

    public function getWidth()
    {
        return $this->width;
    }

    public function getHeight()
    {
        return $this->height;
    }
}

class Square extends Rectangle
{
    public setWidth($width)
    {
        parent::setWidth($width);
        parent::setHeight($width);
    }

    public setHeight($height)
    {
        parent::setHeight($height);
        parent::setWidth($height);
    }
}

function calculateRectangleSquare(Rectangle $rectangle, $width, $height)
{
    $rectangle->setWidth($width);
    $rectangle->setHeight($height);
    return $rectangle->getHeight * $rectangle->getWidth;
}

calculateRectangleSquare(new Rectangle, 4, 5); // 20
calculateRectangleSquare(new Square, 4, 5); // 25 ???

НАДЕЖНЫЙ пример

class Rectangle
{
    protected $width;
    protected $height;

    public setWidth($width)
    {
        $this->width = $width;
    }

    public setHeight($height)
    {
        $this->height = $height;
    }

    public function getWidth()
    {
        return $this->width;
    }

    public function getHeight()
    {
        return $this->height;
    }
}

class Square
{
    protected $size;

    public setSize($size)
    {
        $this->size = $size;
    }

    public function getSize()
    {
        return $this->size;
    }
}

Разделение интерфейса

  • Много специализированных интерфейсов лучше, чем один универсальный.
  • Соблюдение этого принципа необходимо для того, чтобы клиентские классы, использующие/реализующие интерфейс, знали только о тех методах, которые они используют, что приводит к уменьшению количества неиспользуемого кода.

ГЛУПЫЙ пример

interface IItem
{
    public function applyDiscount($discount);
    public function applyPromocode($promocode);
    
    public function setColor($color);
    public function setSize($size);
    
    public function setCondition($condition);
    public function setPrice($price);
}

НАДЕЖНЫЙ пример

interface IItem
{
    public function setCondition($condition);
    public function setPrice($price);
}

interface IClothes
{
    public function setColor($color);
    public function setSize($size);
    public function setMaterial($material);
}

interface IDiscountable
{
    public function applyDiscount($discount);
    public function applyPromocode($promocode);
}

Инверсия зависимости

  • Зависимости внутри системы основаны на абстракциях.
  • Модули верхнего уровня не зависят от модулей нижнего уровня.
  • Абстракции не должны зависеть от деталей.
  • Детали должны зависеть от абстракций.

ГЛУПЫЙ пример

class Customer
{
    private $currentOrder = null;

    public function buyItems()
    {
        if (is_null($this->currentOrder)) {
            return false;
        }

        $processor = new OrderProcessor(); // !!!
        return $processor->checkout($this->currentOrder);
    }
    
    public function addItem($item)
    {
        if (is_null($this->currentOrder)) {
            $this->currentOrder = new Order();
        }

        return $this->currentOrder->addItem($item);
    }

    public function deleteItem($item)
    {
        if (is_null($this->currentOrder)) {
            return false;
        }

        return $this->currentOrder->deleteItem($item);
    }
}

class OrderProcessor
{
    public function checkout($order) {...}
}

НАДЕЖНЫЙ пример

class Customer
{
    private $currentOrder = null;

    public function buyItems(IOrderProcessor $processor)
    {
        if (is_null($this->currentOrder)) {
            return false;
        }

        return $processor->checkout($this->currentOrder);
    }

    public function addItem($item){
        if (is_null($this->currentOrder)) {
            $this->currentOrder = new Order();
        }

        return $this->currentOrder->addItem($item);
    }

    public function deleteItem($item) {
        if (is_null($this->currentOrder)) {
            return false;
        }

        return $this->currentOrder->deleteItem($item);
    }
}

interface IOrderProcessor
{
    public function checkout($order);
}

class OrderProcessor implements IOrderProcessor
{
    public function checkout($order) {...}
}

Резюме

Принцип единой ответственности

Каждому объекту должна быть назначена одна ответственность». Для этого проверяем, сколько у нас причин для смены класса — если больше одной, то этот класс нужно разбить.

Принцип открытости/закрытости (Open-closed)

Программные объекты должны быть открыты для расширения, но закрыты для модификации». Для этого мы представим наш класс как «черный ящик» и посмотрим, сможем ли мы изменить его поведение в этом случае.

Принцип подстановки Варвары Лисков (подстановка Лисков)

Объекты в программе могут быть заменены их наследниками без изменения свойств программы». Для этого проверяем, усилили ли мы предусловия и ослабили ли постусловия. Если это происходит, то принцип не соблюдается.

Разделение интерфейса

Многие специализированные интерфейсы лучше одного универсального. Мы проверяем, насколько интерфейс содержит методы и насколько разные функции накладываются на эти методы, и при необходимости разбиваем интерфейсы.

Принцип инверсии зависимости

Зависимости должны строиться на абстракциях, а не на деталях». Мы проверяем, зависят ли классы от каких-то других классов (непосредственно инстанцируют объекты других классов и т. д.), и если такая зависимость имеет место, заменяем ее зависимостью от абстракции.

Первоначально опубликовано на https://it.badykov.com 14 марта 2020 г.