[Перевод] Разработка ПО действительно так сложна? Или это мы делаем ее такой?
Ты начал свою карьеру как разработчик программного обеспечения. Выучил основы и продолжаешь учиться новому по мере продвижения. Как ответственный разработчик, который хочет стать мастером своего дела, ты читаешь рекомендованные многими книги, такие как «Чистый код», «Рефакторинг» и т.д.
Ты также стараешься улучшить свои навыки, изучая TDD (Test Driven Development), DDD (Domain Driven Design), архитектуру Clean/Hexagonal/Onion/Ports&Adapter. Ты определенно чувствуешь, что становишься лучше в разработке программного обеспечения.
Хотя ты ясно видишь преимущества некоторых из этих техник, ты все еще не очень уверен в некоторых из них. Ты думаешь, что, возможно, в этом виновато твое недопонимание, что ты еще недостаточно созрел, чтобы осознать эти великие идеи. Но ты не сдаешься. Каждые несколько недель/месяцев ты снова пробуешь применить эти идеи, чтобы увидеть, понимаешь ли ты их теперь. Но ты всё еще не уверен в полезности некоторых из этих практик или техник.
И ты снова думаешь, что всему виной твое отсутствие навыков, из-за которого ты не можешь увидеть преимуществ этих практик. Проходят годы… ты все ещё пытаешься понять и снова терпишь неудачу… проходят десятилетия, ты пытаешься понять, но снова терпишь неудачу.
Ты спрашиваешь у экспертов, и они отвечают: «Ваш вопрос неверный» или «Ваш пет-проект недостаточно сложный, чтобы увидеть преимущества». Ты удивляешься, ведь хоть это и пет-проект, случаи использования такие же, как и в реальных проектах, над которыми ты уже работал!
И в один прекрасный день ты осознаешь, что, возможно, некоторые из этих практик на самом деле не были такими уж грандиозными либо были неприменимы к твоей работе. Затем ты начинаешь внимательнее присматриваться к ним и понимаешь, что многие энтузиасты предусмотрительно «забыли» сказать о том, когда эти практики следует применять, а когда нет.
К тому же некоторые идеалисты доводят эти идеи до абсурда, заявляя, что есть только один «верный способ» сделать что-либо и что, если ты не придерживаешься упомянутых ранее практик, ты не являешься профессиональным разработчиком.
Но ты слишком боишься сказать это вслух, ведь люди могут счесть тебя глупцом. Однако, всему есть предел, и однажды ты говоришь, «мне не нравятся некоторые из этих практик». Я не собираюсь винить себя в том, что не вижу преимуществ в использовании некоторых из них, если я этих преимуществ так и не увидел даже после многолетних попыток.
Знакомо? Это мое текущее состояние.
И знаешь что? Не только ты так себя чувствуешь. Многие другие испытывают те же чувства, но слишком боятся сказать об этом, потому что люди слишком склонны осуждать тебя, если ты не принадлежишь к их «элите». Но опять-таки, ты достиг той точки, когда тебе глубоко все равно, будут ли люди осуждать тебя на основании их «глубоких убеждений» и их узкого мышления по принципу «есть только один правильный путь».
Давайте посмотрим на то, о чем именно я говорю.
Поговорим о TDD
Я люблю автоматизированное тестирование. Я предпочитаю запустить автоматический тест, чтобы убедиться в том, что определенная функциональность работает (или не работает) вместо того, чтобы запускать приложение, искать нужный мне путь через многочисленные экраны и отправлять форму, предварительно заполнив вручную кучу полей, каждый раз.
Но утверждение, что «ты должен всегда сначала писать тест, прежде чем писать код для продакшена, потому что в противном случае ты не соблюдаешь TDD» — это то, с чем я не согласен.
Когда я реализую некую функциональность, я читаю задачу, анализирую ее, глубоко продумываю, как я собираюсь её реализовывать и затем начинаю писать код. Я создаю класс (сразу в продакшен коде), пишу метод с аргументами, основанный на моем текущем понимании, затем реализую очень базовую функциональность. Это обычно занимает от 30 секунд до пары минут. Затем я использую комбинацию Cmd + Shift+ T в IDE, чтобы создать тест. Наконец, я пишу базовый тест. Затем я усложняю реализацию большим количеством кода и параллельно расширяю тест или просто пишу больше тестов.
Через час или около того у меня появляется реализованная функциональность с набором тестов.
Моя цель состоит в реализации функциональности, хорошо протестированной набором автоматических тестов.
Сначала создать экземпляр класса, которого не существует, потом вызывать метод, которого не существует, и затем использовать шорткаты для создания этих классов или методов — если это ваш путь, прекрасно.
Но сначала создать класс или метод и потом использовать шорткаты для создания теста или тестов — это то, что подходит мне больше. Стандартный аргумент против такого подхода состоит в том, что если сначала написать продакшен код, то тесты будут зависеть от деталей реализации.
Если я пишу юнит тест, требующий настройки mock, тогда каким бы подходом я не пользовался, мои тесты будут зависеть от деталей реализации. Если я пишу интеграционный тест, тогда, независимо от подхода, я могу написать этот тест никак не зависящим от деталей реализации.
Я не пишу весь продакшен код за несколько часов, чтобы протестировать его вручную и зачем написать тесты только ради покрытия. Продакшен код и тесты эволюционируют параллельно друг другу, шаг за шагом, инкрементально. Этот подход отлично работает в моём случае.
Могу ли я сказать, что следую TDD? Многие TDD-энтузиасты категорически не согласятся с этим.
В соц.сетях можно часто увидеть посты в стиле »Очень жаль, что даже в 20XX году все еще приходится напоминать, что, следуя TDD надо сначала писать тесты! ». Когда так называемые «лидеры мнений» говорят такое, создается впечатление, что «все согласны, что это единственный правильный способ следовать принципам TDD». Подобное допущение не позволяет людям задавать вопросы об использовании немного изменённых версий TDD.
Тем не менее, как я уже сказал ранее, мой стиль следования TDD мне нравится больше. И мне не надо никого ни в чем убеждать. Так что все в порядке.
А как насчет архитектуры Clean/Hexagonal/Onion/Ports&Adapters?
Я много раз пытался полюбить эти архитектурные стили. Но я так и не увидел от них никакой пользы. После преобразования модуля, следующего моему любимому подходу к разработке к этим стилям, код становился неоправданно сложным.
На протяжении всей своей карьеры я в основном работал с приложениями, ориентированными на данные. Я не большой поклонник архитектур Clean/Hexagonal/Onion/Ports&Adapters, особенно для приложений, ориентированных на данные. Основное рекламируемое преимущество этих архитектурных стилей состоит в их способности изолировать инфраструктуру от центральной доменной логики.
В большинстве бизнес-приложений будет низкая или средняя сложность бизнес-логики, которая в бóльшей степени будет зависеть от инфраструктуры баз данных, брокеров сообщений, интеграций с внешними API и т.д. Для таких приложений возможность тестировать центральную логику домена в отрыве от инфраструктуры — невеликое преимущество. Напротив, мне больше нравится убирать ненужные абстракции (интерфейсы) и получать возможность тестировать логику с инфраструктурными компонентами, используя такие инструменты, как Testcontainers.
В большинстве бизнес-приложений (ориентированных на данные), с которыми мне приходилось работать, типичными задачами являются следующие:
Получить данные от пользователя через вызов REST API с формы или из сообщения, полученного от брокера сообщений.
Провалидировать что-либо на наличие значений в обязательных полях, проверить значение на соответствие указанному минимум/максимум значению или соответствию регулярному выражению.
Провалидировать данные относительно имеющихся данных, например, «такой адрес электронной почты уже существует» .
Сохранить или обновить данные.
Извлечь данные из многочисленных источников (таблиц).
Вернуть только необходимые данные под специфичный кейс.
При необходимости отправить события в брокер сообщений, чтобы их могли использовать другие системы.
Большинство бизнес-приложений, ориентированных на работу с данными, включают в себя эти задачи.
Многие люди рекомендуют DDD (Domain Driven Development) как золотой стандарт для создания приложений. Мне нравятся некоторые аспекты DDD, такие как:
Использование универсального языка.
Определение ограниченного контекста.
Моделирование значимых объектов, сущностей, репозиториев, сервисов и т.д.
Но ключевым аспектом DDD является Агрегат.
Я понимаю разницу между сущностью и агрегатом. Обычно единица работы применяется на уровне агрегата.
Некоторые сторонники DDD предлагают следовать строгим правилам в отношении того, как (не)обращаться к агрегату/сущности напрямую. При этом в их понимании всё должно проходить только через агрегат. Это отличный подход, пока ты не столкнёшься со случаем, требующим доступа к списку сущностей. В зависимости от сценария использования правила DDD становятся более гибкими, и сущность может повыситься до уровня Агрегата. Поэтому я предпочитаю моделировать домен напрямую на основе сценариев использования, а не на основе произвольных правил, предложенных пуристами.
После изучения тонкостей всех этих концепций я остановился на следующем подходе для приложений, ориентированных на данные:
Создавать отдельные классы для работы с разными сценариями использования.
Создавать отдельные модели запроса и ответа для каждого случая использования. Это может привести к тому, что некоторые классы будут иметь одинаковые поля, но это нормально.
Следовать CQRS (необязательно разделять базу данных для чтения и базу данных для записи). Использование одной модели для чтения и записи только всё усложняет.
Писать юнит тесты для тестирования любой бизнес логики, включающей вычисления и т.д., используя mocks, если это необходимо.
Писать интеграционные тесты для проверки API с использованием реальных компонентов инфраструктуры.
Да, всё настолько просто.
Это мой опыт и моё мнение после многих лет работы над приложениями, ориентированными на данные.
Если вы хотите сказать мне, что доменная модель Anemic с Transaction Script Pattern плоха, скажите это и предпринимателям, которые управляют многомиллионным бизнесом, используя только Google Sheets.
Если архитектура Clean/Hexagonal/Onion/Ports&Adapters вам подходит — прекрасно.
Но, как я уже сказал, я не вижу большой выгоды от использования этих стилей для приложений, ориентированных на данные. И я рад, что наконец избавился от мысли: «Возможно, я просто не смог оценить великолепие архитектуры Clean/Hexagonal/Onion/Ports&Adapters» — которая крутилась у меня в голове много лет.
Действительно ли мы (разработчики) любим все усложнять?
Если ты дочитал до этого места, возможно ты думаешь, что я обвиняю этих размечтавшихся лидеров мнений в сложности процесса разработки ПО. На самом деле, не совсем. Я думаю, что мы (разработчики ПО) тоже любим всё усложнять.
Позволь мне рассказать тебе простую историю.
Я начал программировать используя Windows OS, и я не использовал Linux как минимум 5 лет после начала моей карьеры в качестве разработчика ПО. Затем мой друг познакомил меня с Linux, и я установил Ubuntu в качестве второй ОС.
Форматировать жесткий диск, устанавливать операционную систему и устанавливать кучу ПО через терминал — это, конечно, весело. Помню, как я пытался заставить работать Wi-Fi или Bluetooth, погрузившись в поиск ответов на Linux-форумах, запускал случайные Linux-команды и, в конце концов, удачно справился с проблемой. Однако на следующий день я обновил версию OS, и Wi-Fi или Bluetooth (или что-то ещё) снова сломалось. И мне снова пришлось повторить ту же процедуру с запуском многочисленных команд, предложенных на форуме. Как по мне, это какое-то гиковское занятие.
Ещё, однажды я купил ноутбук новой модели, для которой драйвера Wi-Fi еще не были выпущены, но существовал GitHub репозиторий с исходным кодом для драйверов. Я клонировал этот репозиторий, собрал драйвера с использованием Make файла, установил их, и все заработало. В тот день я чувствовал себя супер-гением.
Думаешь я стал использовать Linux в качестве операционки на каждый день, пройдя этот путь? Нет. Как только я обновился и исправил проблему на Linux, я переключился на Windows и продолжил работать, как и прежде. Так зачем же я потратил время и усилия на исправление проблемы с драйвером Linux?
Я хотел почувствовать себя компьютерным гением, справляясь с трудными задачами. Я люблю решать сложные проблемы.
Раньше я читал код open-source проектов на GitHub, чтобы узнать, как другие разработчики решают задачи. Если код был простым, я пропускал его и искал следующий. Я хотел видеть код, использующий рефлексию и делающий какие-то безумно сложные вещи. Я хотел читать код, использующий шаблон strategy, а не обычный Enum или if-else, даже если необходимость использовать strategy у меня никогда и не возникнет. Ох, эта тяга к сложному в начале карьеры…
Некоторым разработчикам нравится «ненавидеть PHP». Сможем ли мы найти более расширяемую CMS, чем WordPress, написанный на PHP? Почему не существует большого числа более эффективных фреймворков типа Laravel на твоем любимом языке, который не является PHP? Ни у кого нет времени найти ответа на этот вопрос. Мы все слишком заняты бесконечным копированием свойств объектов из слоя в слой!!!
Я уверен на 100%, что это касается не только меня. Многие люди могут узнать здесь себя. Если сложных задач нет, что нам в таком случае делать? Мы делаем простые вещи сложными и затем решаем сложные проблемы. Не так ли? Мы, разработчики, любим сложность, по крайней мере в начале своей карьеры.
Теперь, когда мне уже за 40, я очень ценю простоту. Я хочу, чтобы код моего приложения был настолько скучным, насколько это возможно, чтобы я мог закончить свою работу и заняться домашними делами. Мне нравятся сложные задачи, но я не хочу сталкиваться с ними каждый день.
Я пересматриваю RailsConf 2014 — Keynote: Writing Software by David Heinemeier Hansson снова и снова, чтобы напомнить себе о красоте простых вещей и о необходимости избегать карго-культа.
Вывод
Нам повезло, что сегодня в нашем распоряжении есть огромное количество материалов, позволяющих научиться создавать сложные системы. Но мы не должны следовать за ними вслепую только потому, что какой-то лидер мнений сказал, что что-то является великой идеей. И самое главное, мы не должны бояться ставить под сомнение установленные нормы, если они покажутся нам неправильными.
Не каждый из нас занимается написанием следующего Facebook, Google или Netflix. Необязательно применять все эти креативные и сложные решения к твоему бизнес-приложению, у которого будет не больше 5000 пользователей в любой отдельно взятый момент времени, только по той причине, что ты когда-то готовился к собеседованию по системному дизайну.
И если кто-то пытается сказать тебе «просто на всякий случай, если ты вдруг захочешь использовать другую базу данных…» — БЕГИ. Настолько быстро, насколько сможешь.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь