Поводом к написанию предыдущей статьи «Шаблоны и принципы деления кода на классы» [1] послужил случай с начинающим программистом, который обратился ко мне за помощью. Однако та история получила неожиданное продолжение, ставшее, в свою очередь, одной из предпосылок уже для этой статьи. И видимо, волей судьбы или просто по забавному стечению обстоятельств, эта история оказалась напрямую связана с комментариями к первой статье [2], где в ходе жаркого диалога я затронул тему мышления и восприятия кода разработчиком.

Так родилась идея поделиться накопившимися за 10-летний стаж наблюдениями и плодами размышлений:

  • Как стиль написания кода отражает образ мышления разработчика.

  • Как разработчики воспринимают код программы.

  • Почему один язык программирования, что называется, «заходит», а другой - нет.

Но прежде чем перейти к сути, я расскажу о тех самых поводах, что подтолкнули меня к написанию статьи. Они служат яркими иллюстрациями выводов, к которым я пришёл.

Дважды в одну реку

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

Разговорившись, я узнал, что мой менти уже второй раз пытается «выучить Джаву и начать на ней кодить», причем с первой попытки прошло уже несколько лет.

— Никак не даётся, - с грустью признался он.

И действительно, работа шла на редкость тяжело. Любопытно, что по отдельности языковые конструкции он знал хорошо, но когда дело доходило до написания цельной программы, начинался какой-то необъяснимый ступор. Я рассудил так: человек не понимает, что такое программирование, и из каких шагов состоит процесс написания программы». Поэтому я стал восполнять этот пробел, и работа сдвинулась с мёртвой точки, хотя не пошла как по маслу. Всё ещё было много моментов, которые вызывали непонимание и трудности, а порой и откровенные «затыки».

Но в таком ритме работу мы всё же сделали и на этом разошлись. И вот недавно мы случайно пересеклись, и он восторженно рассказал мне о том, что начал изучать Python и самостоятельно переписал тот самый интеграционный тест с Java на Python. А дальше много и с упоением говорил, насколько Python ему проще даётся и насколько он лучше его понимает. Я обрадовался за него, и в то же время ещё мне чертовски захотелось глянуть в код, ведь тот Java-тест, написанный с помощью JUnit и Java Message System, просто так, один в один, не перенесёшь в Python в виду отсутствия тех же самых фреймворков.

Мой бывший менти не отказал в любопытстве  и с радостью показал свою работу.

Интеграционный тест на Python и впрямь функционально один в один повторял своего собрата на Java. Более того, алгоритмически они были весьма схожи. Но написан он был совершенно по-другому. И речь не про библиотеки и языковые конструкции, а про саму структуру программы. Весь проект на Python состоял из одного-единственного файла (.venv не учитываю, как не учитываю .m2 в Java), который был поделен на две функции:

  1. В первой был весь код интеграционного теста.

  2. Вторая совсем маленькая функция буквально на пару строк.

Для сравнения, структура аналогичного теста на Java была куда сложнее:

  1. Там тоже был один файл с кодом теста, но он был разбит на 3 метода.

  2. Было несколько файлов с небольшими классами-утилитами, которые использовались в тесте.

  3. К тому же это был Maven-проект, поэтому к общему числу файлов добавился ещё pom.xml и специфичная для Maven структура проекта.

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

Разговор велосипеда с самокатом

Как я уже упоминал, пример кода в статье «Шаблоны и принципы деления кода на классы» вызвал немало вопросов и комментариев. Один из них звучал так:

Может кто-нибудь, пожалуйста, объяснить уместность вынесения одной строки кода в отдельный метод на примере парсинга стринга -> инт (за multiply молчу)?

В своём ответе, я попытался донести своё видение разности подходов к написанию кода: процедурного и объектно-ориентированного. Поскольку тема статьи была про «деление кода на классы», то и код был написан с позиций объектно-ориентированного подхода, тогда как мои оппоненты явно апеллировали к процедурному. (Что это за подходы, я раскрою чуть дальше).

Так вот, код интеграционного теста на Python как раз и был написан в рамках процедурного подхода:

  • Линейная последовательность сверху (начало программы) вниз (конец программы).

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

  • Никаких классов.

  • Никаких файлов-менеджеров проекта и какой-либо структуры директорий.

Просто один файл с кодом, который открыл и читаешь-пишешь последовательно сверху-вниз.

А в Java всё иначе:

  • Несколько классов для одного маленького теста и отношения между ними.

  • Использование менеджера проекта Maven со своим workflow и структурой директорий.

  • JUnit со своим workflow и правилами написания теста.

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

И вот, увидев код теста на Python, то окончательно пришёл к выводу:

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

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

Разное виденье

Предвижу возражения: «Да он же просто начинашка!» И отчасти это так.

Однако и упомянутый выше диалог, и мой опыт работы на проектах говорят об ином:

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

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

Вот вам ещё пример из жизни одного моего проекта.

Заказчик захотел провести code review разрабатываемого нами решения и устроил созвон, чтобы дать обратную связь по коду. В моей практике такое случилось впервые. Как руководителю разработки, мне пришлось пойти на этот не вполне понятный созвон. Со стороны заказчика было несколько менеджеров и ещё один новый человек, которого представили, как главного разработчика с их стороны. И тут же,передали ему слово без всяких предисловий он зарядил: «Мне не понятно…

  • зачем столько классов?!

  • зачем такая запутанная логика?!

  • почему нельзя было попроще написать в 2-3 класса?!

И дальше в основном вопросы крутились вокруг большого количества классов, методов из одной-двух строк и т. п. Я терпеливо объяснял, почему и зачем было так сделано. Но даже после всех объяснений вердикт главного разработчика от заказчика был такой: «Я бы выкинул всё и переписал с нуля». В итоге код оставили без изменений, но я тогда всерьез задумался:

Почему код, написанный сеньорами (а в той команде были только такие), может восприниматься одним разработчиком как «хорошо написанный», а другим — как откровенный «говнокод»?

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

Особенно остро разницу в видении я ощутил, когда, будучи адептом ООП, начал изучать функциональные подходы в программировании. Даже базовые концепции дико ломали мне мозг, и я никак не мог понять: зачем так сложно? Но как сказал один мой коллега про язык программирования Scala:

Поначалу думал, что вся эта функциональщина (функциональные подходы в программировании — прим. автора) какая-то фигня, но, пописав где-то с годик код, понял, что в этом определённо есть смысл.

Впоследствии то же получилось и со мной. Я научился смотреть на код с разных подходов: процедурного, ООП и функционального. И каждый из них требует своего, непохожего друг на друга способа мышления. Так я пришёл к понятию лингвистической относительности в программировании.

Лингвистическая относительность

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

Из всего, что доводилось читать про гипотезу Сепира-Уорфа, чаще всего она звучит в контексте естественных языков. Хотя в определении фигурирует просто слово «язык», которое представляет собой обобщённое определение языковой системы, а искусственные языки, к которым и относят языки программирования, как раз являются её подмножеством.

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

Языковая форма отражает мышление

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

Это привело к тому, что образовались определённые подходы к кодированию, и на текущий момент мне известны четыре таких:

  • Процедурный

  • Объектно-ориентированный

  • Функциональный

  • Промпт-ориентированный

Кратко остановлюсь на каждом.

Процедурный

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

Он характеризуется:

  • Линейной направленностью кода от начала к концу.

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

Свои характеристики процедурный подход унаследовал от алгоритма, который имеет чёткую последовательность от первого шага (начало алгоритма) до последнего шага (конец алгоритма).

Часть современных хороших практик в программировании, например, DRY (Don't Repeat Yourself), как раз являются представителями этого подхода.

Объектно-ориентированный

Объектно-ориентированный подход (ООП) — это про роли объектов и их взаимодействие друг с другом.

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

На первый взгляд, если просто объединить код в блоки (методы), а их обернуть в классы - получается ООП. К сожалению, простое объединение кода по общему признаку (домену) не делает его объектно-ориентированным. Нужны ещё два важных компонента: роль и взаимодействие с другими ролями (читай, объектами).

Часть современных хороших практик в программировании, например, SOLID, как раз являются представителями этого подхода.

Функциональный

Функциональный подход (ФП) — это про выполнение преобразований одной категории объектов в другую.

Стоит обратить внимание на словосочетание «категории объектов», которое не связано с объектами с точки зрения ООП, и на слово «преобразование», выполняемое функцией, которая в свою очередь никак не связана с функцией из процедурного программирования. Такая странная трактовка связана с тем, что ФП в своём фундаменте опирается на математику, поэтому термины в нём имеют математическое толкование.

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

Часть современных хороших практик в программировании, например, Immutable object (неизменяемый объект), как раз являются представителями этого подхода.

Промпт-ориентированный

С ростом популярности больших языковых моделей (БЯМ) в 2022 году, промпт-ориентированный подход (ПОП) также набрал популярность и стал стремительно развиваться. Появившись он задолго до БЯМ, он, например, активно используется в Behavior-driven development (BDD).

Сразу отмечу

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

ПОП — это про составление спецификаций поведения программы на естественном языке.

Спецификация состоит из набора сценариев, где описано какие шаги выполняются (что делается) и ожидаемый результат для каждого шага. Большую роль в ПОП  играют ключевые слова.

Например,

возьмем такой промт: «напиши функцию факториал на Java».

Если заменить ключевое слово Java на Python, то результат будет другой. А если заменить “факториал” на “сумма двух чисел”, то результат также изменится.

Фокус разработчика в этом случае направлен на следующее:

  • Описание «что» (без описания «как») программа должна делать и какой ожидаемый результат. Дополнительно можно указывать определённый порядок действий и название конкретного алгоритма.

  • Владение терминологией исполнителя задачи без необходимости знания и понимания принципов его работы.

Сейчас подобным обычно занимается аналитик, который пишет такие спецификации для разработчика (в классическом понимании).

Мышление определяет действие

Каждый из подходов имеет свой фокус:

  • Процедурный — шаг алгоритма.

  • Объектно-ориентированный — роль объекта и его отношения с другими объектами.

  • Функциональный — преобразование категории объекта.

  • Промпт-ориентированный — описание сценария работы.

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

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

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

Например, у меня преобладает объектно-ориентированный. И именно поэтому меня в своё время так зацепила Java, хотя я пробовал другие языки, такие как Python, Javascript и C. Но это не означает, что все Java-программисты имеют преобладающий объектно-ориентированный образ мышления. Я постоянно встречаю и новичков, и опытных разработчиков, у которых преобладает процедурный подход. И, например, упомянутый диалог показал, что я и мои коллеги по цеху смотрели на код с позиций разных подходов, поэтому и не смогли найти взаимопонимания.

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

Поскольку я тяготею к объектно-ориентированному подходу и в основном пишу на Java, мне до сих пор бывает мучительно тяжело читать код Python-разработчиков. Ведь в своём фундаменте язык использует процедурный подход, а полная поддержка ООП в нём появилась только в версии 2.2. Это повлияло на языковые конструкции и сформировавшиеся практики кодирования в Python, которые отличаются от Java, изначально спроектированного как объектно-ориентированный язык.

А вот с моим бывшим менти, которого я упоминал в начале статьи, получилась обратная ситуация. Он делал два подхода к изучению Java, и язык давалсся ему с большим трудом. Но ситуация изменилась, когда он стал изучать Python. И по его коду, который имел все признаки процедурного подхода, я понял причину, почему Java с её объектно-ориентированным подходом давалась ему так тяжело.

Заключение

Какой же вывод я сделал из этого?

  1. Изучение различных подходов через разные языки программирования формирует умение различать эти подходы и смотреть на код под разными углами.

  2. Сравнивать подходы между собой по принципу «какой лучше?» — бессмысленно. У них разный фокус и каждый наиболее эффективен в своем классе задач.

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

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

  5. Современные хорошие практики в программировании возникли из различных подходов, поэтому этот момент нужно учитывать при их использовании.

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


[1] Статья: Шаблоны и принципы деления кода на классы
[2] Комментарии к статье