Fullstack Software Engineer @ PandaDoc
0,0
рейтинг
8 января 2014 в 13:31

Разработка → Шпаргалка по SOLID-принципам с примерами на PHP

Тема SOLID-принципов и в целом чистоты кода не раз поднималась на Хабре и, возможно, уже порядком изъезженная. Но тем не менее, не так давно мне приходилось проходить собеседования в одну интересную IT-компанию, где меня попросили рассказать о принципах SOLID с примерами и ситуациями, когда я не соблюл эти принципы и к чему это привело. И в тот момент я понял, что на каком-то подсознательном уровне я понимаю эти принципы и даже могут назвать их все, но привести лаконичные и понятные примеры для меня стало проблемой. Поэтому я и решил для себя самого и для сообщества обобщить информацию по SOLID-принципам для ещё лучшего её понимания. Статья должна быть полезной, для людей только знакомящихся с SOLID-принципами, также, как и для людей «съевших собаку» на SOLID-принципах.


Для тех, кто знаком с принципами и хочет только освежить память о них и их использовании, можно обратиться сразу к шпаргалке в конце статьи.

Что же такое SOLID-принципы? Если верить определению Wikipedia, это:
аббревиатура пяти основных принципов дизайна классов в объектно-ориентированном проектировании — Single responsibility, Open-closed, Liskov substitution, Interface segregation и Dependency inversion.


Таким образом, мы имеем 5 принципов, которые и рассмотрим ниже:
  • Принцип единственной ответственности (Single responsibility)
  • Принцип открытости/закрытости (Open-closed)
  • Принцип подстановки Барбары Лисков (Liskov substitution)
  • Принцип разделения интерфейса (Interface segregation)
  • Принцип инверсии зависимостей (Dependency Invertion)


Принцип единственной ответственности (Single responsibility)


Итак, в качества примера возьмём довольно популярный и широкоиспользуемый пример — интернет-магазин с заказами, товарами и покупателями.

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

Рассмотрим следующее описание класса для представления заказа в интернет-магазине:
class Order
{
	public function calculateTotalSum(){/*...*/}
	public function getItems(){/*...*/}
	public function getItemCount(){/*...*/}
	public function addItem($item){/*...*/}
	public function deleteItem($item){/*...*/}

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

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


Как можно увидеть, данный класс выполняет операций для 3 различный типов задач: работа с самим заказом(calculateTotalSum, getItems, getItemsCount, addItem, deleteItem), отображение заказа(printOrder, showOrder) и работа с хранилищем данных(load, save, update, delete).
К чему это может привести?
Приводит это к тому, что в случае, если мы хотим внести изменения в методы печати или работы хранилища, мы изменяем сам класс заказа, что может привести к его неработоспособности.
Решить эту проблему стоит разделением данного класса на 3 отдельных класса, каждый из которых будет заниматься своей задачей

class Order
{
	public function calculateTotalSum(){/*...*/}
	public function getItems(){/*...*/}
	public function getItemCount(){/*...*/}
	public function addItem($item){/*...*/}
	public function deleteItem($item){/*...*/}
}

class OrderRepository
{
	public function load($orderID){/*...*/}
	public function save($order){/*...*/}
	public function update($order){/*...*/}
	public function delete($order){/*...*/}
}

class OrderViewer
{
	public function printOrder($order){/*...*/}
	public function showOrder($order){/*...*/}
}


Теперь каждый класс занимается своей конкретной задачей и для каждого класса есть только 1 причина для его изменения.

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


Данный принцип гласит — "программные сущности должны быть открыты для расширения, но закрыты для модификации". На более простых словах это можно описать так — все классы, функции и т.д. должны проектироваться так, чтобы для изменения их поведения, нам не нужно было изменять их исходный код.
Рассмотри на примере класса OrderRepository.
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){/*...*/}
}


В данном случае хранилищем у нас является база данных. например, MySQL. Но вдруг мы захотели подгружать наши данные о заказах, например, через API стороннего сервера, который, допустим, берёт данные из 1С. Какие изменения нам надо будет внести? Есть несколько вариантов, например, непосредственно изменить методы класса OrderRepository, но этот не соответствует принципу открытости/закрытости, так как класс закрыт для модификации, да и внесение изменений в уже хорошо работающий класс нежелательно. Значит, можно наследоваться от класса OrderRepository и переопределить все методы, но это решение не самое лучше, так как при добавлении метода в OrderRepository нам придётся добавить аналогичные методы во все его наследники. Поэтому для выполнения принципа открытости/закрытости лучше применить следующее решение — создать интерфейc IOrderSource, который будет реализовываться соответствующими классами MySQLOrderSource, ApiOrderSource и так далее.

Интерфейс IOrderSource и его реализация и использование
class OrderRepository
{
	private $source;

	public function setSource(IOrderSource $source)
	{
		$this->source = $source;
	}

	public function load($orderID)
	{
		return $this->source->load($orderID);
	}
	public function save($order){/*...*/}
	public function update($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){/*...*/}
}



Таким образом, мы можем изменить источник и соответственно поведение для класса OrderRepository, установив нужный нам класс реализующий IOrderSource, без изменения класса OrderRepository.

Принцип подстановки Барбары Лисков (Liskov substitution)


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

Пример иерархии прямоугольника и квадрата и вычислении их площади
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 ???



Очевидно, что такой код явно выполняется не так, как от него этого ждут.
Но в чём проблема? Разве «квадрат» не является «прямоугольником»? Является, но в геометрических понятиях. В понятиях же объектов, квадрат не есть прямоугольник, поскольку поведение объекта «квадрат» не согласуется с поведением объекта «прямоугольник».

Тогда же как решить проблему?
Решение тесно связано с таким понятием как проектирование по контракту. Описание проектирования по контракту может занять не одну статью, поэтому ограничимся особенностями, которые касаются принципа Лисков.
Проектирование по контракту ведет к некоторым ограничениям на то, как контракты могут взаимодействовать с наследованием, а именно:
  • Предусловия не могут быть усилены в подклассе.
  • Постусловия не могут быть ослаблены в подклассе.


«Что ещё за пред- и постусловия?» — можете спросите Вы.
Ответ: предусловия – это то, что должно быть выполнено вызывающей стороной перед вызовом метода, постусловия – это то, что, гарантируется вызываемым методом.

Вернёмся к нашему примеру и посмотрим, как мы изменили пред- и постусловия.
Предусловия мы никак не использовали при вызове методов установки высоты и ширины, а вот постусловия в классе-наследнике мы изменили и изменили на более слабые, чего по принципу Лисков делать было нельзя.
Ослабили мы их вот почему. Если за постусловие метода setWidth принять (($this->width == $width) && ($this->height == $oldHeight)) ($oldHeight мы присвоили вначале метода setWidth), то это условие не выполняется в дочернем классе и соответственно мы его ослабили и принцип Лисков нарушен.

Поэтому, лучше в рамках ООП и задачи расчёта площади фигуры не делать иерархию «квадрат» наследует «прямоугольник», а сделать их как 2 отдельные сущности:
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 segregation)


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

Вернёмся к примеру с интернет-магазином.
Предположим наши товары могут иметь промокод, скидку, у них есть какая-то цена, состояние и т.д. Если это одежда то для неё устанавливается из какого материала сделана, цвет и размер.
Опишем следующий интерфейс
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);
}


Данный интефейс плох тем, что он включает слишком много методов. А что, если наш класс товаров не может иметь скидок или промокодов, либо для него нет смысла устанавливать материал из которого сделан (например, для книг). Таким образом, чтобы не реализовывать в каждом классе неиспользуемые в нём методы, лучше разбить интерфейс на несколько мелких и каждым конкретным классом реализовывать нужные интерфейсы.

Разбиение интерфейса IItem на несколько
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 Book implemets IItem, IDiscountable
{
    public function setCondition($condition){/*...*/}
    public function setPrice($price){/*...*/}
    public function applyDiscount($discount){/*...*/}
    public function applyPromocode($promocode){/*...*/}
}

class KidsClothes implemets IItem, IClothes
{
    public function setCondition($condition){/*...*/}
    public function setPrice($price){/*...*/}
    public function setColor($color){/*...*/}
    public function setSize($size){/*...*/}
    public function setMaterial($material){/*...*/}
}



Принцип инверсии зависимостей (Dependency Invertion)


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

Для примера рассмотрим оплату заказа покупателем.

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){/*...*/}
}


Всё кажется вполне логичным и закономерным. Но есть одна проблема — класс Customer зависит от класса OrderProcessor (мало того, не выполняется и принцип открытости/закрытости).
Для того, чтобы избавится от зависимости от конкретного класса, надо сделать так чтобы Customer зависел от абстракции, т.е. от интерфейса IOrderProcessor. Данную зависимость можно внедрить через сеттеры, параметры метода, или Dependency Injection контейнера. Я решил остановится на 2 методе и получил следующий код.

Инвертирование зависимости класса Customer
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){/*...*/}
}



Таким образом, класс Customer теперь зависит только от абстракции, а конкретную реализацию, т.е. детали, ему не так важны.

Шпаргалка


Резюмируя всё выше изложенное, хотелось бы сделать следующую шпаргалку
  • Принцип единственной ответственности (Single responsibility)
    «На каждый объект должна быть возложена одна единственная обязанность»
    Для этого проверяем, сколько у нас есть причин для изменения класса — если больше одной, то следует разбить данный класс.
  • Принцип открытости/закрытости (Open-closed)
    «Программные сущности должны быть открыты для расширения, но закрыты для модификации»
    Для этого представляем наш класс как «чёрный ящик» и смотрим, можем ли в таком случае изменить его поведение.
  • Принцип подстановки Барбары Лисков (Liskov substitution)
    «Объекты в программе могут быть заменены их наследниками без изменения свойств программы»
    Для этого проверяем, не усилили ли мы предусловия и не ослабили ли постусловия. Если это произошло — то принцип не соблюдается
  • Принцип разделения интерфейса (Interface segregation)
    «Много специализированных интерфейсов лучше, чем один универсальный»
    Проверяем, насколько много интерфейс содержит методов и насколько разные функции накладываются на эти методы, и если необходимо — разбиваем интерфейсы.
  • Принцип инверсии зависимостей (Dependency Invertion)
    «Зависимости должны строится относительно абстракций, а не деталей»
    Проверяем, зависят ли классы от каких-то других классов(непосредственно инстанцируют объекты других классов и т.д) и если эта зависимость имеет место, заменяем на зависимость от абстракции.


Надеюсь, моя «шпаргалка» поможет кому-нибудь в понимании принципов SOLID и даст толчок к их использованию в своих проектах.
Спасибо за внимание.

P.S. В комментариях посоветовали хорошую книгу — Роберт Мартин «Быстрая разработка программ». Там очень подробно и с примерами описаны принципы SOLID.
Андрей Нестер @andrewnester
карма
56,2
рейтинг 0,0
Fullstack Software Engineer @ PandaDoc
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (63)

  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      спасибо, поправлю.
      первый вариант предпочтительнее, я думаю, поскольку лаконичнее и тем не менее понятнее. да и вроде как Мартин Фаулер в «Чистый код» советовал писать как в первом вариант
      • +7
        В «Чистом коде» советует Роберт Мартин, а не Мартин Фаулер :)
  • +1
    public function removeItem($item){
        if(is_null($this->currentOrder)){
            return false;
        }
        return $this->currentOrder = new Order();
    }

    Улыбнул метод удаления продукта из заказа.

    А так, спасибо за статью. Пригодится!
    • 0
      действительно интересный способ)
      спасибо, что заметили, уже исправляю
  • –2
    В общем и целом согласен со всем. НО…
    Есть несколько моментов, которые меня смутили.

    1) Не
    $this->config->getDns()
    , а
    $this->config->getDsn()


    2)
    … создать интерфейc IOrderSource, который будет реализовывать соответствующими класса MySQLOrderSource, ApiOrderSource и так далее...
    Кажется я не понял. У нас интерфейс с реализацией?

    Вообще можно налепить большую кучу абстракций и чем их больше и чем они меньше связанны между собой — тем лучше будет реализовываться заменяемость. А смысл такой: классы вообще ничего не должны знать о классах, которые будут их использовать и должны знать только доступные методы тех классов, которые будут использоваться в них (целостность можно поддержать интерфейсами или абстрактными классами), т.е. их связность должна стремиться к нулю. И надо не забывать о том, что в одном классе мы можем реализовывать много интерфейсов, поэтому есть смысл дробить интерфейсы на более узкоспециализированные.

    Как-то так;)
    • +2
      извиняюсь, это всё опечатки, как мог вычитывал, но глаза замылились совсем, спасибо, что замечаете их.
      … создать интерфейc IOrderSource, который будет реализовывать соответствующими класса MySQLOrderSource, ApiOrderSource и так далее...

      должно быть
      … создать интерфейc IOrderSource, который будет реализовываться соответствующими классами MySQLOrderSource, ApiOrderSource и так далее...
    • –2
      И лучше, наверно, юзера и пароль внести в DSN, а не пихать их отдельными атрибутами в конструкторе PDO.
  • +21
    Все проблемы программирования можно решить дополнительным слоем абстракции… кроме проблемы избыточной абстракции

    © David Wheeler
  • 0
    Небольшое дополнение к сабжу: From STUPID to SOLID Code!
    • +6
      Я из-за этой статьи уволился с предыдущей работы.
  • +1
    Проверяем, зависят ли классы от каких-то других классов(непосредственно инстанцируют объекты других классов и т.д) и если эта зависимость имеет место, заменяем на зависимость от абстракции.

    Это называется «статическая зависимость».

    Судя по тексту, с LSP вы сами до конца не разобрались. Описание этого принципа выглядит очень сумбурно и не содержит информации о том, какой же профит можно получить ему следуя.
    • 0
      c LSP, на мой взгляд, ситуация следующая (хотя и со всеми принципами так, да и не только в программировании) — мы не столько что-то получаем, соблюдая принцип, а что-то теряем и получаем негативный эффект, когда не соблюли его. В случае с несоблюдением LSP мы получаем изменчивость в поведении там, где мы её не ждали.

      Если можно, бытовой пример — к примеру, Вы чистите зубы по утрам каждый день и Ваши зубы здоровы. Но если бы Вы не чистили зубы, у Вас мог образоваться кариес, т.е. не соблюдение какого-то принципа приводит к негативному эффекту. Хотя можно на это посмотреть и с другой стороны, что соблюдение принципа поддерживает Ваши зубы в здоровом состоянии. Но тогда можно сказать, что профит от LSP — поддержание кода в «здоровом» состоянии :)
      • 0
        Это принцип «поддержание кода в здоровом состоянии», а не LSP.
        Оценивать тот или иной подход нужно, сравнивая с конкурентами, а не с абстракцией.
        Т.е. да, чистить зубы хорошо, но чистить их можно тысячей способов и вот их нужно сравнивать. А вы сравниваете нечистку зубов с чисткой именно зубной пастой.
      • 0
        В случае с несоблюдением LSP мы получаем изменчивость в поведении там, где мы её не ждали.


        Можно хоть один пример исходя из вашего описания?
        Ибо у вас все очень сумбурно и с путаницей в описаниях.
        • 0
          Это, как раз, в тексте есть (см. «Пример иерархии прямоугольника и квадрата и вычислении их площади»). Метод calculateRectangleSquare работает с фигурами рассчитывая на то, что их поведение не будет отличаться, а это не так. Чтоб добиться правильного поведения, нужно править метод и реализовывать различную логику для квадрата и прямоугольника.

          Но описано это все, как я писал выше, очень сумбурно.
      • 0
        Что касается примера. Простейший пример нарушения LSP:

        sub processOperation
        {
        	if ($operation->type eq 'refund') {
        		$operation->amount = $operation->amount * -1;
        	}
                   …
        	$operation->process();
        }

        Нарушение LSP ведет к нарушению OCP т.к. появление еще одной операции, аналогичной возврату, потребует внесения изменений в метод processOperation.

        Мой и ваш примеры изложены и подробно разжеваны в книге Дяди Боба «Быстрая разработка программ», в которой SOLID посвящен целый раздел. Можете порекомендовать ее в своем посте и прочесть, если еще этого не сделали.
        • 0
          я конечно извиняюсь, но — каким образом Ваш пример отражает принцип Лисков? Возможно, Вы не привели его до конца или что-то не досказали?
          возможно, моя сумбурность в объяснении связана с тем, что у меня более математический склад ума, а сам по себе принцип Лисков более математичен, поэтому приведу его математическое описание из статьи Лисков:
          Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T


          Так вот, где в Вашем пример подтип для processOpeartion? В контексте одного класса нельзя говорить о принципе Лисков.

          Возвращаясь к приведённому мною в статье примеру (к слову, в приведённой Вами книги как раз описан такой же пример с квадратом и прямоугольником)

          Предположим, разработчик знает о наличии классов Square и Rectangle, что они связаны в иерархию и меют методы для получения/установки высоты/ширины.

          И наш программист создаёт следующий метод, который вполне логичен:

          function myRectangle(Rectangle $rectangle)
          {
              $rectangle->setWidth(4);
              $rectangle->setHeight(5);
              return $rectangle->getHeight * $rectangle->getWidth == 20;
          }
          


          И выполняет этот метод дважды следующим образом
          myRectangle(new Rectangle()) // true;
          myRectangle(new Square()) // false;
          


          И теперь возвращаемся к принципу Лисков:
          тип Т — класс Rectangle
          подтип S — класс Square
          свойство q — произведение width и height равно 20

          но мы получаем, что для подтипа S (класса Square) свойство q не верно.
          цитирую, что написано в Вашей книге
          Пример с функцией показывает, что могут существовать функции, которые работают с указателями или ссылками на объекты Rectangle, но не могут оперировать с объектами Square. Таким образом, при использовании таких функций объект Square не является заменяемым для Rectangle, взаимосвязь между такими объектами нарушает принцип подстановки Лискоу


          Надеюсь, сейчас пояснил чуть яснее.

          Кроме того, это всё можно выразить в терминах контрактного программирования — пост и предусловия, инварианты.

          И кроме того, чуть дальше в Вашей книге в этом разделе есть подтема «Неправильное поведение объекта», это как раз то, о чём я сумбурно рассказал, приводить данный раздел целиком не буду (страница 193 — «Неправильное поведение объекта»)
          • 0
            Пример показывает, что бывает, если ему не следовать :) Возвращаясь к вашему. Когда разработчик замечает проблему, он первым делом не меняет иерархию, а переписывает функцию примерно так:

            function calculateRectangleSquare(Rectangle $rectangle, $width, $height)
            { 
                if (get_class($rectangle) == "Square") {
                    $rectangle->setWidth($width);
                } else {
                    $rectangle->setWidth($width);
                    $rectangle->setHeight($height);
                }
                return $rectangle->getHeight * $rectangle->getWidth;
            }
            

            Теперь calculateRectangleSquare работает корректно, но нарушает OCP, а причиной этого стало нарушение классом Square принципа LS.
            • 0
              Простите меня, но где вы тут увидели нарушение OCP?
              По одному методу то, когда принцип касается правила использования классов.
      • 0
        по примерам не понятно, как считать площадь фигур, в итоге, и каким образом преобразованный код иллюстрирует LSP, если ни наследования, ни полиморфизма в нем не осталось?
        • 0
          в рамках примера — наследования и полиморфизма не осталось, потому что с точки зрения поведения Square не является Rectangle, а связь IS-A как раз имеет прямое отношение к поведению объектов — поэтому в данном конкретном случае решение — избавление от наследования и связи двух этих классов.
    • +4
      Большинство и не задумывается над профитом, умные дяди сказали — хорошо, значит все правильно!
      • 0
        К сожалению, очень верно. И делают всё через сотни слоёв абстракции там, где она не нужна совсем или нужна по минимуму.
        • 0
          На мой взгляд, в принципах SOLID нет каких-то лишних и не нужных абстракций (если я где-то привёл в примерах что-то лишнее, что можно было бы сделать проще, то пишите, мне на самом деле всегда интересно знать другие решения)

          Я же как разработчик прекрасно понимаю, что лишняя абстракция — это лишний шаг другого разработчика, который читает мой код, на пути понимания то, что я написал. И именно поэтому я немного скептически отношусь к паттернам проектирования и совсем скептически к использованию паттернов ради паттернов.
          • –1
            Зависит от того, насколько фанатично применять. Примеры сами по себе к практике мало отношения имеют и нужны для иллюстрации принципов, так что с ними всё нормально.
      • +2
        большинство не делают ни чего из того, о чем вы написали)
        • +1
          Не делать и делать фанатично вредно. Фанатичность, наверное, даже чуть вреднее.
  • 0
    andrewnester, у меня к вам вопрос — у вас в примере используется функция вне класса. Не могли бы вы прокомментировать ваше отношение к таким функциям: используете ли, где они расположены, как они взаимодействуют с классами и объектами.
    В примере про заказы интересует, в какой момент вы заполняете поле config — если в чем-то вроде контроллеров, то можно ли пример такого контроллера в контексте SOLID. Спасибо.
    • 0
      функции вне класса использую довольно редко, в примере она использована просто для того, чтобы показать использование классов Square и Rectangle.

      обычно использую просто функции в случаях
      1) код написан с использованием парадигмы функционального программирования и не очень хорошо, мешать ООП с ФП. часто это задачи связаны с реализацией какого-нибудь алгоритма.
      2) при написании относительных небольших скриптов для командной строки, где разного рода абстрагирования и иерархии — лишнее
      3) когда-то при написании плагинов для Wordpress (при написании более-менее больших плагинов всё-таки переходил к ООП)
      4) часто использую анонимные функции в качестве callback-функций

      по поводу поля config.
      если продолжать в рамках исходного примера, я бы сделал что-нибудь такое
      // точка входа - index.php
      
      //...
      
      
      $config = new Config('path/to/conf');
      
      $app = new Application($config);
      $app->run();
      
      
      // example controller
      
      class ExampleController extends BaseController
      {
      	//...
      
      	public function saveAction()
      	{
      		$config = $this->getApplication()->getConfig();
      		$respository = $config->getServices()->getRepositoryManager()->getRepository('Order');
      			
      		//...
      	}
      }
      
      // если хотим изменить источник, можно написать что-нибудь такое
      
      public function saveAction()
      {
      	$config = $this->getApplication()->getConfig();
      	$respository = $config->getServices()->getRepositoryManager()->getRepository('Order');
      	$repository->setSource(new ApiOrderSource($config->getSources()->getApi()));
      
      	//...
      }
      
      
      // сам файл конфигурации может выглядеть как-нибудь так
      
      //...
      {
      	'services' : {
      		'RepositoryManager' : {
      			'class' => 'RepositoryManager',
      			'options' => {
      				//...
      			}
      		},
      		//...
      	},
      	'sources' : {
      		'MySQL' : {
      			'host' => 'host'
      			'username' => 'name',
      			'password' => 'password',
      			'dbname' => 'dbname'
      		},
      		'API' : {
      			'getaway' => 'http://apipath.com/api',
      			'oauth_key' => '',
      		}
      	}  
      }
      //...
      
      • 0
        Ну вот смотрите, разве saveAction выполняет одну функцию? Т.е. если обобщить до предела, то да, он загружает, допустим, контент для конкретного URL. Но таким образом можно обобщить и все ваши классы Order в один и сказать, что он выполняет одну функцию — работа с заказом. Расскажите, как вы обходите это противоречие и как это вписывается в SOLID.
        • 0
          можно дойти до крайности и в другую сторону и сказать, что цикл for выполняет не одну функцию, а 2 — и инкрементирует значение, и производит сравнение :)

          по сути вопроса:
          лично я основываюсь на таком вот способе определения выполняет ли метод 1 функцию или нет. Он приведён в книге Мартина «Чистый код» в разделе «Правило одной операции»:
          Если функция выполняет только те действия, которые находятся на одном уровне под объявленным именем, то эта функция выполняет одну операцию


          Для себя я в голове делаю так: прикидываю, как без привязки к какой-либо системе, архитектуре, коду в целом должна происходить описываемая мной операция, например, получение хранилища для данных, создание объекта, заполнение его данными, сохранение, возврат результата выполнения и далее на основе этих шагов описываю её с помощью кода.
          • 0
            Для себя я в голове делаю так: прикидываю, как без привязки к какой-либо системе, архитектуре, коду в целом должна происходить описываемая мной операция, например, получение хранилища для данных, создание объекта, заполнение его данными, сохранение, возврат результата выполнения и далее на основе этих шагов описываю её с помощью кода.

            Это речь об абстракции, а не о единой операции.
            можно дойти до крайности и в другую сторону и сказать, что цикл for выполняет не одну функцию, а 2 — и инкрементирует значение, и производит сравнение :)

            Именно так, поэтому я и уточняю, раз вы говорите, что разобрались с этим вопросом, как вы определяете одну операцию?
            Если функция выполняет только те действия, которые находятся на одном уровне под объявленным именем, то эта функция выполняет одну операцию

            Что за уровни?
            • 0
              Что за уровни?

              Это речь об абстракции, а не о единой операции.

              Уровни абстракции/ответственности, поэтому я и завёл речь об абстракциях.

              Именно так, поэтому я и уточняю, раз вы говорите, что разобрались с этим вопросом, как вы определяете одну операцию?


              Каждая функция/метод выполняет только те действия, которые находятся на одном уровне абстракции/ответственности и соответственно причина для её/его изменении может быть только одна
              • 0
                Каждая функция/метод выполняет только те действия, которые находятся на одном уровне абстракции/ответственности и соответственно причина для её/его изменении может быть только одна

                Не логично, на одном уровне абстракции множество объектов и операций. Функция может выполнять множество действий, взятых из этого уровня абстракции. И тогда причин для изменений будет практически столько же сколько действий?

                И еще вопрос, каким образом вы храните id заказа во всех классах, в виде ссылки на класс Order?
                • 0
                  И тогда причин для изменений будет практически столько же сколько действий?


                  Цитирую Мартина (снова):
                  So a responsibility is a family of functions that serves one particular actor. (Robert C. Martin)

                  An actor for a responsibility is the single source of change for that responsibility. (Robert C. Martin)


                  Вы почему-то связываете причины изменения с конкретными функциями/методами, я же Вам говорю, что причины связаны с уровнями абстракции и ответственности. Если Вы выделяете каждую функцию/метод в свой уровень абстракции/ответственности, то да, причин будет столько, сколько функций/методов. Я же так не выделяю. Я выделяю слой представления данных, слой бизнес-логики и т.д., каждый из этих общих слоев может быть разбит ещё на какие-то исходя из задач и целей проекта конкретного проекта (например, бизнес-логику зачастую удобно разбивать на абстракции на основании данных из предметной области)

                  А вообще функция/метод должны делать ни больше, ни меньше чем указано в их названии.

                  И еще вопрос, каким образом вы храните id заказа во всех классах, в виде ссылки на класс Order?

                  Ну, например, в случае с классом Customer я использовал композицию объекта Order. Других классов у себя я не нашёл в примере.
                  Встречный вопрос — какое это отношение имеет к принципам SOLID? это уже детали реализации либо я Вас неправильно понял
  • 0
    На более простых словах это можно описать так — все классы, функции и т.д. должны проектироваться так, чтобы для изменения их поведения, нам не нужно было изменять их исходный код.


    Во-первых, не все программные сущности. 100% расширяемости достичь невозможно, поэтому замкнутость должна быть стратегической. Об этом написано у Uncle Bob. То есть программист заранее должен позаботиться и спроектировать свою программу так, чтобы она была расширяема только для определенного вида изменений.

    Во-вторых, так интерпретировать OCP не совсем корректно. OCP говорит не об изменении поведения конкретного класса или функции, а добавлении нового поведения в программу. Например, если у нас уже есть класс, интерфейс которого не устраивает, то чтобы не нарушить OCP принцип, мы можем сделать для него адаптер с новым интерфейсом, при этом старый класс останется без изменения.
    • 0
      да, Вы абсолютно правы — абсолютной расширяемости невозможно достичь, всё рассматривается в рамках определённой задачи и определённых требований.

      По поводу второго пункта с Вами не согласен. Цитата из статьи Мартина «The Open-Closed Principle»:
      1. They are “Open For Extension”.
      This means that the behavior of the module can be extended. That we can make
      the module behave in new and different ways as the requirements of the application
      change, or to meet the needs of new applications.

      2. They are “Closed for Modification”.
      The source code of such a module is inviolate. No one is allowed to make source
      code changes
      to it.


      Так что, как раз тут то, о чём я говорю — мы должны проектировать классы так, чтобы могли изменить их поведение, без изменения его исходного кода.
      Использование же адаптера — это просто изменение интерфейса на более удобный, поведение в целом остаётся таким же.
      • 0
        etyumentcev прав. Советую почитать автора принципа Б.Мэйера. Книжку ниже я вам уже посоветовал.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      допустим метод printOrder имеет у нас следующий вид

      public function printOrder($order)
      {
      	$fields = $this->prepareOrderFields($order);
      	//...
      	return $this->printFields($fields);
      }
      


      И допустим нам захотелось поменять метод prepareOrderFields, чтобы он как-то по-другому обрабатывал поля заказа. Но окажется, что данный метод prepareOrderFields используется ещё в методе сохранения заказа и таким образом мы повлияем на сохранение заказа, сами того не ожидая
      • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    А я правильно понимаю, что в итоговом примере с Dependency Invertion класс Customer остался зависим от класса Order, но в данной ситуации этого не избежать?
    • 0
      не совсем. Класс Customer не зависит от какой-то конкретной реализации класса Order, т.е. объектом currentOrder может быть и экземпляр, например, класса FreeShippingOrder, и класса LimitedItemCountOrder (классы немного надуманы, не обращайте внимания).
      • +1
        Если честно, то не совсем понятно.
        В методе ведь используется конкретная реализация, а не абстракция.
        public function addItem($item){
                if(is_null($this->currentOrder)){
                    $this->currentOrder = new Order();
                }
                return $this->currentOrder->addItem($item);
            }
        


        Я предположил бы, что такая реализация была бы более под стать принципа
        //...
        public function addItem(IOrder $currentOrder, $item){
                return $currentOrder->addItem($item);
            }
        //...
        
        //...
        $oOrder = $oCustomer->currentOrder ?: new Order();
        $oCustomer->addItem($oOrder, $oItem);
        

        Что скажете?
        • +2
          да, всё верно, значит в первом случае Вы были правы.
          это я упустил из виду, что не добавил метод или параметр для установки подтипа заказа.

          вообще строго говоря такие нюансы(должны ли быть какие-то подтипы для Order и т.д.) — это детали конкретного проекта, в учебном примере я решил показать на примере класса OrderProccessor как избавиться от зависимости от конкретной реализации и перейти к зависимости от абстракции. и такой принцип может быть применён ко всем другим зависимостям, что Вы успешно и сделали в примере выше, спасибо :)
          • +1
            Да, конечно, в каждом конкретном случае все зависит от деталей. Просто хотелось понять сам принцип, и в каких случая его стоит применять.
            • +2
              Вы верно всё поняли — вы как раз и заменил конкретную реализацию, на абстракцию и классы теперь у Вас связаны, строго говоря, слабее и Вы всегда можете добавить новый класс, реализующий IOrder, и без труда использовать его
              • +1
                Спасибо за разъяснения и за статью, конечно.
  • +1
    Немного опоздал, но вот здесь человек разжовывает каждый из СОЛИД принципов с примерами кода.
    blog.byndyu.ru/2009/10/solid.html
  • 0
    calculateRectangleSquare(new Square, 4, 5); // 25 ???
    

    Ваш пример абсолютно не показал нарушения LSP. Ваш объект Square успешно используется и более того, он вычисляется правильно.
    Дальше, почему вы используете функцию, которая изменяет объект, а потом вычисляет некоторое значение? Это вообще не попадает не под одну парадигму, ни под функциональную ни под Объектно ориентированную.
    И если мы уж говорим о ООП, то давайте не смешивать два подхода и удивляться, что что-то пошло не так, по вашему мнению
    К чему я веду, да к тому что принцип подстановки Барбары Лисков не раскрыт чуть больше чем полностью. Вы говорите вообще не о том. Весь принцип сводится к интерфейсу класса.

    P.S.
    По поводу сеттеров, я бы советовал поменьше их использовать в принципе, если все свойства объекта имеют сеттеры, то скорее всего это не должно быть объектом, а должно быть набором данных, которые используются для создания объектов. Т.к. если можно менять всё, то о какой бизнес-логике можно вести речь?

    class Rectangle
    {
        protected $width;
        protected $height;
    
        public function __construct($width, $height ) {
            $this->width = $width;
            $this->height = $height;
        }
    
        public function getWidth()
        {
            return $this->width;
        }
    
        public function getHeight()
        {
            return $this->height;
        }
    
        public function calculateSquare()
        {
           return $this->height * $this->width;
        }
    }
    
    class Square extends Rectangle
    {
        public function __construct($size) {
            parent::__construct($size, $size);
        }
    }
    


    class TwoPointsData
    {
         private $width;
         private $height;
    
         public function getWidth() 
         { 
            return $this->width; 
         }
    
         public function setWidth($width) 
         { 
            $this->width = $width; 
         }
    
         public function getHeight() 
         { 
            return $this->height; 
         }
    
         public function setHeight($height) 
         { 
            $this->height = $height; 
         }
    }
    
    class Rectangle 
    {
        public function __construct(TwoPointsData $data) 
        {
            ...
        }
    
        public function calculateSquare()
        {
           return $this->height * $this->width;
        }
    }
    
    class Square extends Rectangle
    {
        public function __construct(TwoPointsData $data) 
        {
            ...
        }
    }
    
    • 0
      Дело не в правильности или неправильности вычисления площади квадрата, дело в замене поведения методов класса
      Принцип гласит — «Объекты в программе могут быть заменены их наследниками без изменения свойств программы»

      Мы ожидаем от программы (в данном случае метода calculateRectangleSquare) одно поведение, получаем другое. Нарушение принципа на лицо

      В Вашем примере я не увидел
      1) замену объекта его наследником
      2) ваш объект класса TwoPointsData нигде не используется
      • 0
        Дело не в правильности или неправильности вычисления площади квадрата, дело в замене поведения методов класса
        Принцип гласит — «Объекты в программе могут быть заменены их наследниками без изменения свойств программы»

        Принцип гласит:
        Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

        То что вы понимаете под свойством им не является. Поведение методов не изменено. Оно изменено в следующих случаях, при перегрузке:
        — бросает исключение, которые не бросает перегружаемый метод
        — количество обязательных параметров контракта методов изменен
        — тип обязательных параметров не соответствует типу перегруженного метода

        Извините, но вы не понимаете LSP
        • 0
          Забыл про тип возвращаемого значения. Тип возвращаемого значения должен быть такой же как и тип перегружаемого метода.
        • 0
          Чем это
          «Объекты в программе могут быть заменены их наследниками без изменения свойств программы»

          отличается от
          Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

          кроме языка на котором это написано?

          Если я не понимаю LSP, то приведу другой пример
          Удовлетворяет ли данному принципу следующий код и почему?
          class DataCollector
          {
          	protected $data = array();
          
          	public function add($item)
          	{
          		$this->data[] = $item;
          	}
          
          	public function getData()
          	{
          		return $this->data;
          	}
          }
          
          class AnotherDataCollector extends DataCollector
          {
          	public function add($item)
          	{
          		$this->data[$item] = true;
          	}
          }
          
          • 0
            В том виде, в котором написано — да.
            Подставьте вместо DataCollector AnotherDataCollector, поведение программы не изменится. Изменится результат.

            Вопрос, что результат будет вас не удовлетворять — лежит совершенно в другой плоскости.
            • 0
              Принцип не соблюдён.
              Пост-условие контракта для метода add($item)
              $this->data[count($this->data) - 1] == $item
              В переопределённом методе данное условие не выполняется(изменено)
              • 0
                Где вы этого бреда нахватались. Что ещё за пост-условие контракта?
                В контракте у вас описано, что возвращается итератор. Он как возвращался, так и возвращается. Беда PHP в том что списки и словари объединены, но это отдельная тема. У вас же концептуальное не понимание, что такое свойство контракта.
                Контракт это не мифические вещи, которые программист придумал и описал в юнит-тесте, как ему вздумалось, а условия переданные путем языковых конструкций.

                Если грубо, то при замене класса его наследником программа должна дальше работать без ошибок. Либо выбрасывать ошибки такого же типа, что кидала и с родительским классом. Всё остальное — это не понимание LSP.

                Советую почитать Б.Мэйера «Объектно-ориентированное конструирование программных систем»
        • 0
          Извините, но вы не понимаете LSP

          Если можно, попрошу вас привести пример нарушения принципа LSP, хотел бы разобраться
          • 0
            Чуть выше, случайно умудрился ответить :)
          • 0
            class A
            {
                 protected $data;
                 public function add($item)
                 {
                    $this->data[] = $item;
                 }
            }
            
            class B extends A
            {
                 public function add($item)
                 {
                    if ($item < 52) {
                       throw new Exception("Итем не может быть меньше 52");
                    }
                    $this->data[] = $item;
                 }
            }
            

            Вот тут идет нарушение. Класс наследник в методе add бросает исключение, которое не бросает класс родитель. Это нарушение LSP.

            Вообще SOLID стараются как можно больше перенести в язык(на сколько это возможно), т.к тогда можно контролировать на уровне компилятора(интерпретатора). Но естественно, это не всегда возможно.
    • 0
      И если мы уж говорим о ООП, то давайте не смешивать два подхода и удивляться, что что-то пошло не так, по вашему мнению
      К чему я веду, да к тому что принцип подстановки Барбары Лисков не раскрыт чуть больше чем полностью. Вы говорите вообще не о том.


      Если бы я создал класс, такой как пример ниже, то что-то принципиально бы изменилось?
      class Calculator
      {
      	private $width;
      	private $height;
      
      	public function setWidth($width)
      	{
      		$this->width = $width;
      	}
      
      	public function setHeight($height)
      	{
      		$this->height = $height;
      	}
      
      	public function calculate(Rectangle $rect)
      	{
      		$rect->setWidth($this->width);
      		$rect->setHeight($this->height);
      		return $rect->calculateSquare();
      	}
      }
      


      Весь принцип сводится к интерфейсу класса.

      Не понял, что вы имеете ввиду
      • 0
        Если бы я создал класс, такой как пример ниже, то что-то принципиально бы изменилось?

        Да, тут явно видны две ошибки.
        1. Нарушен Принцип единственной обязанности (класс изменяет объект и что-то вычисляет, используя его как параметр)
        2. Нельзя менять входящие параметры в методе.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.