Вода
Будучи джуниор разработчиком на php я проходил множество собеседований. За это время я решил (и не решил) множество задач. Однако среди них есть одна, решая которую я был уверен, что делаю все правильно, но с опытом я понял, что это совсем не так. Конечно, не совсем корректно говорить "правильно" и "неправильно", скажем так - "не соответствует best practices". Сегодня я бы хотел поговорить об этой задаче, поделиться своими мыслями о ее решении и почему SOLID сложнее чем кажется на первый взгляд.
Задача
У вас есть 4 фигуры квадрат, прямоугольник, круг и треугольник. Необходимо создать классы для каждой фигуры.
Звучит очень абстрактно и просто. Штош, давайте попробуем решить эту задачу.
Решение
Первый раз на собеседовании я думал, что все очевидно и предложил такое решение:
<?php
abstract class Figure
{
// метод получения площади
abstract public function getS();
// метод получения периметра
abstract public function getP();
}
class Triangle extends Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
class Circle extends Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
class Rectangle extends Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
class Square extends Rectangle
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
Хоть многие из вас уже увидели нарушение SOLID, давайте по порядку:
Неправильное использование абстрактного класса.
Когда я писал этот код на собеседовании, я думал "Ну возможно у них есть общая логика, поэтому нужно использовать абстрактный класс". Такой подход неверен и даже опасен. Не просто так у нас есть принцип YAGNI («You Ain't Gonna Need It» или в переводе на русский — «Вам это не понадобится»). Разберемся подробнее. В данном случае, мы создаем абстрактный класс, который можно заменить интерфейсом.
Да, абстрактный класс может выполнять роль интерфейса, но у него есть существенный недостаток - наследование. Само по себе наследование отличный инструмент, однако сильная (при этом явно не нужная в данном примере) зависимость классов может усложнить ваш код и повысить сложность его расширения.Наследование квадрата от прямоугольника.
Такое наследование нарушает одно из правил SOLID - принцип подстановки Барбары Лисков и является классическим примером нарушения этого принципа. Проблема заключается в том, что высоту и ширину прямоугольника можно изменять независимо, а высоту и ширину квадрата можно изменять только вместе. Т.е. мы не сможем подставить класс потомок вместо класса родителя.
Отлично! С этим мы разобрались, ну теперь то мы точно напишем правильно, не так ли?

Второе решение
<?php
interface Figure
{
public function getS();
public function getP();
}
class Triangle implements Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
class circle implements Figure
{
public function getS()
{
// code
}
}
class Rectangle implements Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
class Square() implements Figure
{
public function getS()
{
// code
}
public function getP()
{
// code
}
}
Что же здесь не правильно на этот раз? Давайте разберемся:
Нарушение принципа SOLID - принципа разделения интерфейсов.
Для всех фигур мы можем найти площадь и периметр, следовательно у фигуры должны быть методы для их получения - звучит логично. Но это будет логично до тех пор пока вы не столкнетесь, например, с тем, что вам может не понадобиться искать периметр одной из фигур. В таком случае вам придется реализовывать метод, который вам не нужен, покрывать его тестами и поддерживать. Некоторые принимают ошибочное решение - выбрасывать исключение при вызове этого метода. Ошибочное, потому что в таком случае мы нарушим принцип подстановки Барбары Лисков (не сможем потомком подменить родителя).
Давайте напишем этот код еще раз, но теперь следуя принципу разделения интерфейсов
Третье решение
<?php
interface GetingS
{
public function getS();
}
interface GetingP
{
public function getP();
}
Классы наших фигур реализуют данные интерфейсы при необходимости. Если нам нужна реализация обоих методов в классе фигуры, то мы используем оба интерфейса, если только один - то один интерфейс.
Вывод
Третье решение - то, к чему я пришел. Оно мне кажется самым лучшим на данный момент, но я не исключаю вероятность того, что я изменю свое мнение с набором новых знаний и опыта. Это моя первая статья на habr и я надеюсь, что она будет полезна начинающим программистам.