Вода

Будучи джуниор разработчиком на 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, давайте по порядку:

  1. Неправильное использование абстрактного класса.
    Когда я писал этот код на собеседовании, я думал "Ну возможно у них есть общая логика, поэтому нужно использовать абстрактный класс". Такой подход неверен и даже опасен. Не просто так у нас есть принцип YAGNI («You Ain't Gonna Need It» или в переводе на русский — «Вам это не понадобится»). Разберемся подробнее. В данном случае, мы создаем абстрактный класс, который можно заменить интерфейсом.


    Да, абстрактный класс может выполнять роль интерфейса, но у него есть существенный недостаток - наследование. Само по себе наследование отличный инструмент, однако сильная (при этом явно не нужная в данном примере) зависимость классов может усложнить ваш код и повысить сложность его расширения.

  2. Наследование квадрата от прямоугольника.
    Такое наследование нарушает одно из правил 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
  }
}

Что же здесь не правильно на этот раз? Давайте разберемся:

  1. Нарушение принципа SOLID - принципа разделения интерфейсов.


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

Давайте напишем этот код еще раз, но теперь следуя принципу разделения интерфейсов

Третье решение

<?php

interface GetingS
{
  public function getS();
}

interface GetingP
{
  public function getP();
}

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

Вывод

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