PHP Profi

Почему не стоит злоупотреблять абстракцией Перевод

php oop ооп архитектура

Некоторое время назад я начал работать над уже существующим проектом. Прежде чем погрузиться в него, я прочитал документацию. В самом начале файла contributing.md мне встретилось следующее высказывание: "Абстракция везде, где это возможно". Вскоре я заметил, что в этом проекте гораздо больше абстрактных классов, чем обычно. Зачастую это приводит к появлению кода, который невозможно изменять и обладающего лишними зависимостями.

 

В этом посте я объясню, почему "абстракция везде, где это возможно" далеко не лучший совет. Это касается не только PHP, а и остальных языков программирования.

Сначала я расскажу об ошибках при работе с абстрактными классами. После этого покажу, когда создавать их будет ОК )

Что не так в использовании абстрактных классов?

Я приведу несколько примеров с кодом.

 

Сверхсложность абстрактных методов

Первый пример—базовый класс со сверхусложненными методами:

 

abstract class AbstractRepository
{
    public function __construct(Model $models)
    {
        $this->models = $models;
    }

    public function get(array $options = [])
    {
        if (isset($options['sort'])) {
            //...
        }

        if (isset($options['filters'])) {
            //...
        }

        return $this->models->get();
    }
}

 

Что здесь происходит: в базовом классе есть метод get, который возвращает список моделей. Чем больше возникает вариантов использования этого базового класса, тем менее гибким он становится. Метод get получает все больше и больше опциональности и приобретает новый аргумент $options. А вместе с ростом количества применений, растет и количество строк кода. В конце концов, в методе get куча багов, его трудно тестировать и тяжело адаптировать, не нарушая его структуры. Разработчики, которым понадобится немного другое поведение AbstractRepository просто переопределят get в более простой, в соответствии со своими потребностями. В итоге: более простой метод с меньшим количеством фич, следовательно теряется возможность переиспользования функционала.

Как решить эту проблему? При вышеописанном подходе возникает ситуация, когда для внесения новой функциональности вам приходится искать "то самое" место в коде и постоянно дописывать этот функционал именно тут. Постарайтесь замечать такие моменты (появление массива $options —верный признак этого) и делайте рефакторинг путем перемещения реализаций в дочерние классы. Позвольте им самим позаботиться о своих собственных кейсах, таких как сортировка.

Если сортировка должна быть добавлена ​​более чем в одном хранилище, просто для начала продублируйте код, а не делайте сразу абстракции. После того как вы закончили дублировать код и все работает, можно начать добавлять специализированные классы для сортировки и по их использованию задать ей логику. В идеале применить сортировку для какого-либо хранилища займет всего несколько строк.

 

Абстрактный класс, определяющий зависимости для всех своих потомков

Второй пример—базовая реализация, определяющая зависимости для своих подклассов, даже если эти зависимости не используются в абстрактном классе.

 

abstract class CsvExporter
{
    protected $repostory;

    protected $file;

    protected $writer;

    protected $parameters;

    public function __construct(RepositoryInterface $repository, TransformerInterface $transformer, array $parameters)
    {
        $this->repository = $repository;
        $this->parameters = $parameters;
        $this->file = new SplTempFileObject();

        $this->writer = Writer::createFromFileObject($this->file);
        $this->writer->insertOne($transformer->getHeaders());
        $this->writer->addFormatter([$transformer, 'transform']);
    }
}

Это базовый класс, позволяющий подклассам сбрасывать данные из хранилища в файл CSV. Он предусматривает переиспользование, но что если вам когда-нибудь понадобится делать дамп данных не происходящих из репозитория, а откуда-то еще (например, из списка файлов в каталоге)? Этот базовый класс знает о классе, пишущем в CSV, о хранилище и о преобразователе. Не слишком ли много?

Какое решение тут подойдет лучше? Конечно же, трейты! Каждый класс, делающий экспорт в CSV , должен иметь доступ к SplFileObject и объекту Writer (из league/csv), все остальное здесь — детали реализации. Вынесем в трейт оба свойства - $file и $writer.

trait CsvWriter
{
    private $file;
    private $writer;
}	

Да и, вообще, чтоб не заморачиваться с инициализацией этих свойств, вынесем в трейт заодно и setup

trait CsvWriter
{
    private $file;
    private $writer;

    private function setup()
    {
        $this->file = new SplTempFileObject();
        $this->writer = Writer::createFromFileObject($this->file);
    }
}

Если класс, использующий трейт, не хочет самостоятельно форматировать данные для CSV экспорта, можно добавить трансформер, задающий заголовки, вставляемые в первую строку файла. И напоследок переиспользуемый метод с трейтом:

private function setTransformer(TransformerInterface $transformer)
{
    $this->writer->insertOne($transformer->getHeaders());
    $this->writer->addFormatter([$transformer, 'transform']);
}

Теперь класс-экспортер легко использует класс Writer (league/csv) c помощью трейта

class UserExporter
{
    use CsvWriter;

    public function (User $users, UserTransformer $transformer)
    {
        $this->setup();
        $this->setTransformer($transformer);
        $this->users = $users;
    }

    public function getFile()
    {
        $this->users->chunk(100, function ($users) {
            $this->writer->insertAll($users);
        });

        return $this->file;
    }
}

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

Примечание: это всё реальные примеры. Они работают и решают задачи, для которых предназначены. Хотя, при изменении к ним требований, некоторые из приведенных решений адаптируются не так легко, как хотелось бы.

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

Когда абстрактные классы - это хорошо?

Они хороши для создания частичных реализаций интерфейсов с несколькими методами, которые могут иметь реализацию — вот тогда абстрактные классы подходят для 90% реализаций интерфейса.

namespace League\Event;
interface ListenerInterface
{
    /**
     * Handle an event.
     *
     * @param EventInterface $event
     *
     * @return void
     */
    public function handle(EventInterface $event);
    /**
     * Check whether the listener is the given parameter.
     *
     * @param mixed $listener
     *
     * @return bool
     */
    public function isListener($listener);
}

Это класс ListenerInterface из пакета league/event. Почти для каждой реализации Listnerа, метод isListner имеет одинаковую реализацию, поэтому в пакет league/event отправляется и этот класс AbstractListener

namespace League\Event;
abstract class AbstractListener implements ListenerInterface
{
    /**
     * {@inheritdoc}
     */
    public function isListener($listener)
    {
        return $this === $listener;
    }
}

Если по какой-то причине вы хотите использовать существующий класс (уже являющийся частью собственного дерева наследования и, таким образом, имеющий свой собственный родительский класс), чтобы он стал listner’ом, и осуществлял ListenerInterface, вам нужно будет реализовать собственный метод isListener. В качестве альтернативы вы можете создать и использовать простенький трейт вроде этого:

trait PartialListener
{
    /**
     * {@inheritdoc}
     */
    public function isListener($listener)
    {
        return $this === $listener;
    }
}

Резюмируем

  • Метод абстрактного класса, который разрастается вместе с ростом числа случаев его использования—это плохо, нужен рефакторинг.
  • Абстрактный класс с конструктором—антипаттерн
  • Если вы завязли в коде с существующим деревом наследования, то трейт (для частичных реализаций) возможно, сможет решить ваши проблемы.
2015-10-18 irul Поделиться: оригинал