Задача с геометрическими фигурами (ООП)

Вода

Будучи джуниор разработчиком на 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 и я надеюсь, что она будет полезна начинающим программистам.

Tags:
ООП, solid, задача, собеседование

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.