Может войдёшь?
Черновики Написать статью Профиль

6 принципов создания поддерживаемого кода

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

Все проекты по разработке ПО начинаются с лучшими намерениями. Никто не планирует писать код, с которым будет страшно работать, трудно понять или невозможно изменить.

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

В мире разработки существует множество подходов, помогающих избежать попадания в эту ловушку. У многих из них даже есть лёгкие для запоминания акронимы (DRY, SOLID, YAGNI).

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

В книге Проблемно-ориентированное проектирование: Сложность отбора в сердце программного обеспечения Эрик Эванс (Eric Evans) выделяет несколько шаблонов и техник, помогающих избежать попадания в эту ловушку.

Один из важнейших аспектов Проблемно-ориентированного проектирования — возможность постоянного рефакторинга кода посредством итеративного процесса исследования. Написание жёсткого кода, который не в состоянии развиваться, приведёт к остановке этого процесса исследования.

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

1. Интерфейс, определяющий намерения

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

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

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

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

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

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

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

Хороший способ проектирования объекта — начать с его интерфейса, не особо беспокоясь о реализации. Начиная снаружи и работая внутрь, вы получите интерфейс объекта, который точно соответствует его намерениям. Также написание теста перед реализацией заставит вас думать об объекте как заказчик.

Например, у нас может быть следующий класс PHPFile и метод PHPsave():

PHP
// Создать новый файл
$file = new File;

// Сохранить его!
$file->save();

Как заказчику этого класса, нам не нужно знать как именно сохраняется файл или где он сохраняется. Метод PHPsave() соответствует тому, что происходит, и поэтому нам не нужно смотреть исходный код, чтобы понять его.

2. Функции без побочных эффектов

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

При работе с кодом вы обычно либо задаёте вопрос, либо вызываете команду.

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

Один из способов избежания побочных эффектов — использование объектов-значений, а не сущностей. Преимущество использования объектов-значений вместо сущностей в том, что объекты-значения неизменны, в то время как у сущностей есть жизненный цикл. Это означает, что при изменении значения объекта-значения, весь объект должен быть уничтожен и заменён новым объектом. Больше об этом читайте в статье В чём отличия между Сущностями и Объектами-значениями?.

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

Например, мы можем работать с концепцией Денег в приложении электронной коммерции. Деньги — классический пример объекта-значения. Это значит, что работать с объектами PHPMoney очень безопасно, потому что объекты неизменны, и поэтому не имеют побочных эффектов:

PHP
// Сложение двух значений Money
$one = new Money(1000, new Currency('USD'));
$two = new Money(500, new Currency('USD'));

$total $one->add($two);

Когда мы складываем PHP$one и PHP$two, создаётся новый объект PHP$total, потому что объекты-значения неизменны.

3. Утверждения

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

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

Однако, две конкретные реализации интерфейса могут иметь два различных следствия при вызове одного метода!

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

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

Типичный юнит-тест обычно описывает ожидание и затем проверяет, что ожидание корректно:

PHP
// Проверить, что пользователь активирован
public function should_active_user()
{
    
$user = new User;
    
$user->activate();

    
$this->assertTrue($user->isActive());
}

4. Контуры концепции

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

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

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

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

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

Это должно привести к таким интерфейсам и объектам, которые важны для бизнеса, и которые могут быть объединены для соответствия важным концепциям предметной области.

Вы сможете достичь такого просветления только путём исследования и постоянного рефакторинга по мере постижения тонкостей моделируемой предметной области.

Например, возможно календарные периоды важны для вашего приложения финансового планирования. Вместо работы с отдельными датами вы должны определить объект PHPPeriod, который важен для бизнеса:

PHP
// Создать новый Period
$period = new Period('first day of 2014''last day of 2014');

В этом примере мы не думаем об отдельном объекте PHPDate, мы заботимся только о PHPPeriod. Поэтому нам не нужна сложность работы с отдельными объектами.

5. Автономные классы

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

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

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

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

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

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

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

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

Например, у нас может быть заказчик PHPCustomer, у которого есть зависимость от PHPAddress. Мы можем смоделировать PHPAddress как объект-значение, чтобы инкапсулировать контактные данные заказчика PHPCustomer. Используя неизменный объект-значение, нам не придётся работать с вложенными зависимостями:

PHP
// Создать Address
$address = new Address('123 Sesame Street');

// Передать Address как зависимость
$customer = new Customer('Mr. Snuffleupagus'$address);

6. Замыкание операций

В математике закрытой операцией называется такая операция над элементом набора, которая всегда возвращает другой элемент набора. Например, 1 + 1 = 2. И 1, и 2 — числа, поэтому сложение двух чисел возвращает число.

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

Хороший пример — работа с коллекциями. Когда вы изменяете коллекцию, вам должна быть возвращена новая коллекция, содержащая изменённые элементы:

PHP
$collection = new Collection(['Homer''Marge''Bart''Lisa''Maggie']);

// Добавление фамилии к каждому элементу:
$family $collection->map(function ($person) {
    return 
"$person Simpson";
});

$family// Collection
$family->get(0); // 'Homer Simpson'
$family->get(1); // 'Marge Simpson'
$family->get(2); // 'Bart Simpson'
$family->get(3); // 'Lisa Simpson'
$family->get(4); // 'Maggie Simpson'

В этом примере мы прошлись по каждому элементу коллекции и вернули новую коллекцию изменённых элементов.

Заключение

Шесть принципов, которые я описал в этой статье, — небольшая часть множества хороших шаблонов и практик, которые важны для написания гибкого и поддерживаемого ПО.

Я думаю, все мы время от времени грешим написанием неидеального кода. Все мы люди и принимаем резкие необдуманные решения, когда оказываемся в тупике.

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

Как вы считаете, полезен ли этот материал? Да Нет

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.