Архитектура и ООП
Изначально этот материал планировался как урок в PHP-курсе по полиморфизму. Но он, в конце концов, перерос сам урок, и я решил сделать из него отдельную статью. В ней практически ничего PHP-специфичного, поэтому рекомендуется для прочтения всем без исключения.
Напомню, что модель классов PHP взята из Java. Наличие интерфейсов и всех сопутствующих элементов очень сильно влияет на способ организации кода в PHP. Этот способ часто отличается от того, как организуется код в JavaScript, Ruby или Python. И ещё больше отличается от таких языков, как Clojure или Elixir. И всё это на фоне того, что в каждом из этих языков есть ООП.
ООП в этих языках настолько разное, что PHP-программисты, попадающие в Ruby или JavaScript, не понимают, как так можно писать, ведь многие подходы противоречат их представлениям о мире. То же самое происходит и в обратной ситуации.
Так где же правда? Правда в том, что есть вещи, которые действительно определяют архитектуру кода. И это не структура классов, не наличие интерфейсов и не использование полиморфизма.
Возьмём тот же MVC. В нём говорится о слоях, об их задачах (зонах ответственности) и способе взаимодействия друг с другом. Это крайне важно для модульности. В модульной системе отсутствуют циклические зависимости. В MVC ничего не говорится про классы и ООП в целом, потому что между этими понятиями нет связи. Реализовать MVC можно в любом языке общего назначения, каким бы он ни был. То же самое можно сказать обо всех других архитектурных шаблонах.
Архитектура опирается на особенности среды, в рамках которой она применяется, а не на конструкции языка. Например, в вебе господствует HTTP, который построен вокруг концепции «запрос-ответ». Именно поэтому микрофреймворки разных языков выглядят так похоже, независимо от того, есть там ООП или нет: в каждом микрофреймворке есть запрос, ответ и обработчик ответа.
В современной разработке ООП стало чем-то вроде культа карго. Это негативно влияет на неокрепшие умы. Если погуглить по запросу «какой паттерн применить», то можно найти много интересного и грустного:
Подобная ситуация есть в другом месте кода, что в итоге мне не очень нравится, так как выглядит очень коряво. Чувствую, что нужен паттерн, но вот какой сюда применить даже не знаю. Мысль закралась в сторону стратегии, но не уверен, как ее сюда применить. Гуру паттернов, посоветуйте, как быть.
Подобные вопросы появляются только от непонимания того, что человек делает, и попытка заткнуть проблему паттерном дорого обойдётся проекту.
Это не значит, что ООП и паттерны знать не нужно. Нужно, особенно если вы работаете в классовых языках, но это всего лишь одна из многих и не самых критичных частей. Те же ООП-паттерны (это не архитектурные паттерны, например MVC — не ООП-паттерн) чаще применяются в локальных ситуациях. То есть их влияние на архитектуру сильно ограничено.
Архитектура кода
Ниже попробуем разобраться с тем, как всё же стоит писать код, на что обращать внимание и что за чем следует в порядке важности.
Основные части приложения
По каждому указанному ниже аспекту можно написать целую книгу. Найти множество исключений и особых ситуаций, в которых эти подходы неприменимы. Либо применимы, но со множеством уточнений… Прошу понять и простить.
Модель предметной области
В ядре любой системы лежит модель предметной области. Это алгоритмическая часть программы (то место, где происходят вычисления), которая отражает бизнес-задачу. Например, на Хекслете предметная область — это образовательная система. Внутри этой области есть понятия, такие как «курс», «урок», «профессия», «студенты». Все они могут взаимодействовать по определённым правилам.
Перед тем, как приниматься за кодирование предметной области, хорошо бы её понимать, представлять набор сущностей, с которыми она работает, и связи между ними. Однако, в вебе модель предметной области не проектируется «от и до» перед кодированием. Обычно берётся только тот кусок, над которым идёт работа прямо сейчас, и только он переносится на код. В дальнейшем, по мере надобности, добавляются и другие части.
Сущности в коде представляются разными структурами, здесь уже всё зависит от языка. В некоторых языках это структуры, в других — объекты, в третьих — записи. Но какой бы способ и язык не были выбраны, пользователь останется пользователем, а курс — курсом! И для этого не обязательно иметь объекты.
Эта часть, в идеале, не знает ничего про среду использования, находимся мы в браузере или на бекенде или внутри банкомата. Таким образом достигается модульность, система разбита на слои, которые замкнуты относительно своих операций и не знают про окружение. На практике добиться полной изоляции сложно и, скорее всего, не нужно. Но нужно избегать «протекания абстракции», то есть слой верхнего уровня знает про слой лежащий под ним (и использует его), а вот нижний слой не может знать про то, как им пользуются. В примере с Хекслетом, код внутри слоя, отвечающего за операции с курсом, не может знать про существование http и веб-фреймворка.
Среда
Следующая важная часть — среда исполнения. Она определяет основную архитектуру приложения. Если мы работаем в браузере, то это событийная модель, если в бекенде по http — то «запрос-ответ», а в командной строке — прямой запуск кода на исполнение. Есть и другие среды со своими особенностями. Для каждой из них наработано большое количество архитектурных подходов, которые придумывать не нужно — они уже реализованы во фреймворках. В первую очередь это MVC. Причём, в зависимости от среды, — либо MVC1, либо MVC2.
Для хорошего понимания правил работы в данном слое нужно знать операционные системы и сети. Например, невозможно построить хорошее API, не зная протокол HTTP, не имея понятия об идемпотентности и гарантиях доставки сообщений в распределённых системах.
Основные принципы структурирования кода
Изолируйте побочные эффекты от чистого кода
Всё, что связано с вводом/выводом, должно быть не внутри, а, желательно, на самом верхнем уровне. Причём чаще всего в начале работы программы происходит чтение необходимых данных, потом — большой блок основной логики (чистый код) и на выходе — снова побочный эффект, например, запись в файл. В вебе это «запрос-ответ».
Следите за идемпотентностью
Если операцию возможно реализовать так, что она может быть перезапущена в случае ошибки и это не приведёт к проблемам, то операция должна быть реализована именно так. В первую очередь это относится к периодическим и асинхронным задачам.
Используйте автоматное программирование
Программирование на флагах — индикатор кода, который нужно переписывать. Автоматы можно применять крайне широко. Фактически, любой процесс, протекающий внутри системы — потенциальный конечный автомат. Например:
Регистрация пользователя (ожидает подтверждения емейла, подтвержден, забанен)
Публикация статьи (черновик, опубликованная, удаленная)
Избегайте глобальных переменных
Ими могут выступать и объекты, и классы, имеющие внутреннее состояние, которое может поменяться в процессе жизни приложения.
Избегайте ненужного состояния и разделяемого состояния (shared state)
Первое особенно часто проявляется тогда, когда объекты наделяются внутренним состоянием в ситуациях, где это не нужно. Например, при сохранении промежуточных данных между разными вызовами. Используйте формальный метод для проверки того, нужно ли в данной ситуации внутреннее состояние или нет. Проверьте, можно ли объект, выполняющий операцию, заменить функцией? И если ответ «да» — то состояния (кроме конфигурации) быть не должно.
Выделяйте абстракции по необходимости
Одно из ключевых правил в обучении программированию: не делайте ничего лишнего до тех пор, пока не начнёт болеть. Разбивайте и выделяйте, только когда почувствовали, что текущее положение дел мешает быть эффективным. Только в этом случае придёт понимание, когда стоит что-то делать, а когда — нет. В ином случае очень легко перейти черту и превратиться в «архитектурного астронавта» (overengineering)
Нужно ли выносить функцию в отдельный файл? Нет.
Нужно ли делать много маленьких функций? Нет.
Нужно ли разбивать компонент на компоненты? Нет.
Нужно ли вешать индекс в базу данных на всякий случай, ведь потом данных будет много? Нет.
Возможно, это контринтуитивно. Но гораздо проще изменять код, который находится в одном месте и не разбит на множество мелких частей. Разбивать имеет смысл тогда, когда архитектура «устаканилась» и все граничные случаи учтены. А до этого момента пусть оно будет неделимым.
Изолируйте технический долг
Не любой технический долг растёт. Если абстракция хороша и не протекает, то не принципиально, как написан код внутри. Его можно будет переписать, когда придёт время. Иногда приходит время — и код просто удаляется за ненадобностью. Простой пример: функция сортировки массива.
Разбивайте приложение на слои
Используйте уровневое проектирование. Нижние слои не должны знать про сущности верхних слоев, а верхние слои работают на базе нижних. Пример разбиения.
Не ставьте производительность во главу угла
Перед тем как говорить про производительность, прочитайте optimization.guide