Рефакторинг: основные принципы и правила

Привет, DOU. Меня зовут Андрей Данильченко, я PHP-разработчик в Wikr Group. В этой статье расскажу о рефакторинге.

Как бы не хотелось, не всегда удается сразу писать код хорошего качества. Причинами могут быть нехватка знаний программиста или недостаток времени. К тому же иногда при выполнении задачи изменяются требования — и это тоже не лучшим образом отражается на качестве кода. Поэтому рефакторинг становится неотъемлемой частью процесса разработки. Мы выделяем на него, как правило, одну неделю раз в полтора месяца.

Когда нужен рефакторинг

Согласно «Википедии», рефакторинг — это процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения. Его цель — упростить понимание работы программы.

Итак, что значит «упростить понимание работы программы»? Конкретные цели рефакторинга могут быть такими:

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

Также рефакторинг помогает быстрее реализовать программные продукты. Повышается качество — и, соответственно, скорость разработки. Рефакторинг точно необходим, если к вам в команду приходит новый человек, и код в таком виде, в котором он существует, ему не понятен. Это говорит о том, что качество кода неудовлетворительно.

Для рефакторинга, во-первых, напишите хорошие тесты: unit, функциональные или интеграционные. Во-вторых, изменяйте код небольшими итерациями. На каждом шаге прогоняйте тесты. Для качественного рефакторинга полезно знать шаблоны проектирования. Без них будет сложнее проектировать и масштабировать большие проекты.

Что именно рефакторить

Рассмотрим, какие элементы кода затрудняют его восприятие, ухудшают качество и, соответственно, требуют рефакторинга.

Повторы

Допустим, у нас есть такой фрагмент:

$dto
    ->setId($data['id'])
    ->setTitle($data['title'])
    …
    ->setCreatedAt($data['createdAt']);

Решение — реализовать гидратор:

$hydrator->hydrate($data, $dto);

Метод гидратора:

public function hydrate(array $data, $object) : void
{
   foreach($data as $property => $value) {
       $setterName = 'set' . $property;
 
       if (method_exists($object, $setterName)) {
           $object->{$setterName}($value);
       }
   }
}

Комментарии

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

Пример:

$data = $this->getData($cursor);

// put data to csv
foreach ($data as $row) {
	fputcsv($file, $row);
}

Решение — переписать код, заменив комментарии вынесением кода в методы. Даже несколько строк кода лучше вынести в метод, чтобы не использовать комментарий:

$data = $this->getData($cursor);
$this->putDataToCsv($file, $data);

Условные выражения

Условные выражения запутывают. Конечно, по своей сути if, else, elseif, switch не плохи. Они становятся плохими, когда делают проект менее гибким. Чтобы избежать нагроможденности, стоит заменять условные выражения стратегией и/или спецификациями.

Пример:

class ErrorResponsePart implements ResponsePartInterface
{
   /**
    * Error response part data.
    *
    * @var ResponseDtoInterface $partData
    */
   private $partData;
 
   /**
    * {@inheritdoc}
    */
   public function addData(ResponseDtoInterface $data) : void
   {
       if ($data instanceof ErrorDtoInterface) {
           $this->partData = $this->format($data);
       }
   }
 
   /**
    * Prepares response data.
    *
    * @param ResponseDtoInterface $data
    */
   public function format(ResponseDtoInterface $data) : void
   {
        if ($data instanceof ListDataDtoInterface) {
            $formatted = $data->getListData();
        } elseif (/* some expressions */) {
            // some logic
        } elseif (/* some expressions */) {
            // some logic
        }
 
       return $formatted ?? $data;
   }
}

Решение — вынести if из метода addData в спецификацию. А метод format — в отдельный класс, применив стратегию:

class ErrorResponsePart implements ResponsePartInterface
{
   /**
    * Specification.
    *
    * @var SpecificationInterface $specification
    */
   private $specification;
 
   /**
    * Response part data.
    *
    * @var ResponseDtoInterface $partData
    */
   private $partData;
 
   /**
    * Formatter context.
    *
    * @var FormatterContext $formatterContext
    */
   private $formatterContext;
 
   /**
    * ErrorResponsePart constructor.
    *
    * @param SpecificationInterface $specification
    * @param FormatterContext $formatterContext
    */
   public function __construct(SpecificationInterface $specification, FormatterContext $formatterContext)
   {
       $this->specification = $specification;
       $this->formatterContext = $formatterContext;
   }
 
   /**
    * {@inheritdoc}
    */
   public function addData(ResponseDtoInterface $data) : void
   {
       if ($this->specification->isSatisfiedBy($data)) {
           $this->partData = $this->formatterContext->process($data);
       }
   }
}

Спецификация c бизнес-правилами:

class ErrorSpecification implements SpecificationInterface
{
   /**
    * {@inheritdoc}
    */
   public function isSatisfiedBy(ResponseDtoInterface $object): bool
   {
       return $object instanceof ErrorDtoInterface;
   }
}

С помощью контекст-стратегии можно передать несколько стратегий и тем самым уйти от множества if-ов:

class FormatterContext
{
   /**
    * Formatters.
    *
    * @var FormatterInterface[]
    */
   private $formatters;
 
   /**
    * FormatterContext constructor.
    *
    * @param FormatterInterface[] $formatters
    */
   public function __construct(array $formatters = [])
   {
       $this->formatters = $formatters;
   }
 
   /**
    * Format response data part.
    *
    * @param ResponseDtoInterface $data
    * @return mixed
    */
   public function process(ResponseDtoInterface $data)
   {
       foreach ($this->formatters as $formatter) {
           $data = $formatter->format($data);
       }
 
       return $data;
   }
}

Конкретная стратегия:

class ListDataFormatter implements FormatterInterface
{
   /**
    * Checks object for needed requirements.
    *
    * @var FormatterSpecificationInterface $specification
    */
   private $specification;
 
   /**
    * ListDataFormatter constructor.
    *
    * @param FormatterSpecificationInterface $specification
    */
   public function __construct(FormatterSpecificationInterface $specification)
   {
       $this->specification = $specification;
   }
 
  /**
   * {@inheritdoc}
   */
  public function format(ResponseDtoInterface $data)
   {
       if ($this->specification->isSatisfiedBy($data)) {
           $data = $data->getData();
       }
 
       return $data;
   }
}

Спецификация для форматера:

class ListDataSpecification implements FormatterSpecificationInterface
{
   /**
    * {@inheritdoc}
    */
   public function isSatisfiedBy(ResponseDtoInterface $object) : bool
   {
       return $object instanceof ListDataDtoInterface;
   }
}

Непонятные имена

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

Пример:

$fl = count($data) >= ExportBag::LIMIT_PER_FILE;

Заменяем на:

$isExceededLimitPerFile = count($data) >= ExportBag::LIMIT_PER_FILE;

Большие методы, классы

Слишком объемные структуры смотрятся громоздко и затрудняют понимание. Лучше выносить код в небольшие методы или классы. У себя мы приняли, что оптимальные для прочтения методы — это такие, которые имеют длину не более 10 строк.

Длинный список параметров

Избегайте большого списка аргументов в методах, конструкторах. Мы стараемся использовать до 5 аргументов в конструкторе.

Наследование

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

Цепочки сообщений: несоблюдение закона Деметры

Закон Деметры говорит, что любой метод любого объекта может вызывать методы только из:

  • своего объекта;
  • переданных ему параметров;
  • любого созданного им объекта;
  • автономных объектов, к которым у него есть прямой доступ.

Программный модуль должен взаимодействовать только с известными ему модулями-«друзьями» и не взаимодействовать с «незнакомцами». При этом мы получаем меньшую связность кода и не знаем о структуре «незнакомцев».

Пример:

$postService->getTemplateResolver()->getName();

Заменяем на:

$postService->getTemplateName();

Статика

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

Статика приводит к процедурному программированию, тогда как в объектно-ориентированной парадигме мы инстанцируем объекты и позволяем им управлять данными как и когда это нужно. При использовании статики невозможно проектировать на основе контрактов.

Внешние зависимости, оператор new

Все внешние зависимости передаются в конструктор через DI. Если нам нужны объекты с состоянием, которые не можем инжектить через DI (Newable, Transient objects), то используем фабрики. Фабрики инкапсулируют все сложные операции сборки объекта. При этом фабрику инжектим в класс через DI.

Пример:

$delimiter = '|';
new CsvFile(delimiter);

Заменяем на:

class CsvFileFactory implements FileFactoryInterface
{
   /**
    * {@inheritdoc}
    */
   public function createFromArray(array $params = []) : FileInterface
   {
        $delimiter = $params['delimiter'] ?? CsvFile::DEFAULT_DELIMITER;
 
        return new CsvFile(delimiter);
   }
}
 
$fileFactory->createFromArray(['delimiter' => '|']);

Универсальные объекты

Существуют такие универсальные объекты, которые имеют доступ к другим общим объектам. Например, они используются в таких шаблонах проектирования, как сервис локатор и реестр. Оба нарушают закон Деметры: они знают про всю группу объектов, тем самым провоцируя высокую связность системы.

Вместо этого инжектим только нужные нам зависимости.

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

Пример:

public function updateAction() : Response
{
    return $this->get('app.user_handler.service')->handle();
}

Заменяем на:

public function updateAction(UserHandler $userHandler) : Response
{
    return $userHandler->handle();
}

Вместо выводов

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

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

Похожие статьи:
У випуску: огляд алгоритму Timsort, підходи до тестування Postgres запитів в Python, заміна термінів у мові. Новини Microsoft announce Python integration into Power...
Wargaming — одна из самых успешных игровых компаний в мире. Ее история началась в 2000 году в Минске. А в 2011 году Wargaming договорилась...
Військовозобов’язаних фахівців ІТ-компаній Харківщини хочуть звільнити від мобілізації в армію. Також влада розглядає...
На YouTube-каналі DOU вийшов новий випуск Книжкового клубу — шоу для тих, хто ніяк не почне читати. Цього разу обговорюємо...
Львівський ІТ Кластер виграв інвестиційний конкурс на створення Дослідного центру для студентів інженерних...
Яндекс.Метрика