Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 31.05.2014 | нет комментариев

Тема 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 getItemsCount(){/*...*/}
	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 getItemsCount(){/*...*/}
	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->getDns(), $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, тот, что будет реализовывать соответствующими класса MySQLOrderSourceApiOrderSource и так дальше.

Интерфейс 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){/*...*/}
}

interace 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 removeItem($item){
		if(is_null($this->currentOrder)){
			return false;
		}
		return $this->currentOrder = new Order();
	}
}

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 removeItem($item){
		if(is_null($this->currentOrder)){
			return false;
		}
		return $this->currentOrder = new Order();
	}
}

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

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

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

Шпаргалка

Резюмируя всё выше высказанное, хотелось бы сделать следующую шпаргалку

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB