Написать пост

Мнение: объектно-ориентированное программирование — катастрофа на триллион долларов

Аватар Klara Oswald

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

Рассказывает Илья Суздальницкий, senior full-stack-разработчик

Прим. ред. Мнение редакции может не совпадать с мнением автора оригинала.

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

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

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

Дисклеймер

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

Критика ООП — очень деликатная тема. Вероятно, она заденет многих читателей. Тем не менее, я делаю то, что считаю правильным. Цель — не обидеть, а повысить осведомлённость о проблемах ООП. Здесь нет критики ООП Алана Кея — он гений. Было бы замечательно, если бы эта парадигма была реализована так, как он это задумал. Я критикую современный подход Java/C#.

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

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

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

В чём суть?

Объектно-ориентированные программы предлагаются в качестве альтернативы правильным.

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

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

Некоторые могут не согласиться со мной, но современное ООП в Java/C# никогда не было спроектировано должным образом. Оно никогда не было результатом работы хорошего исследовательского института (в отличие от Haskell/FP). Лямбда-исчисление предлагает полную теоретическую основу для функционального программирования. ООП не имеет ничего соответствующего этому. Использование ООП невинно в краткосрочной перспективе, особенно в начальных проектах. Но в долгосрочной перспективе это бомба замедленного действия, которая может взорваться, когда кодовая база станет достаточно большой. Проекты откладываются, сроки горят, разработчики перегорают, добавление новых функций становится практически невозможным. Организация помечает код как «легаси», а команда разработчиков планирует переписать его.

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

ООП-код является недетерминированным. В отличие от функционального программирования, нет гарантий в получении одинакового вывода при одинаковых входных данных. Это делает анализ программы очень сложным. В качестве упрощённого примера: вывод 2 + 2 или calculator.Add(2, 2) в основном равен четырём, но иногда он может становиться равным трём, пяти и даже 1004. Зависимости объекта Calculator могут изменить результат вычисления трудно уловимым, но основательным способом.

Необходимость в стабильном фреймворке

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

Я не считаю себя плохим программистом. Но даже я не могу написать хороший код без сильного фреймворка, на котором можно было бы основать работу. Речь не идёт о софтверных фреймворках. Здесь подразумевается более общее словарное определение фреймворка — «необходимая несущая конструкция». Это фреймворки, которые регулируют более абстрактные вещи, такие как организация кода и уменьшение сложности кода. Хоть объектно-ориентированное и функциональное программирование являются парадигмами программирования, они также являются высокоуровневыми фреймворками.

Ограничение выбора

C++ — ужасный [объектно-ориентированный] язык… Ограничение вашего проекта до C означает, что люди не напортачат ни с какой идиотской «объектной моделью».

Линус Торвальдс широко известен своей открытой критикой C++ и ООП. Одна вещь, в которой он был на 100 % прав — это необходимость ограничения программистов в выборе. На самом деле, чем меньше у программистов выбора, тем более устойчивым становится их код. В приведённой выше цитате Торвальдс настоятельно рекомендует иметь хороший фреймворк, на котором будет основан ваш код.

Многим не нравятся ограничения скорости на дорогах, но они необходимы, чтобы не дать людям разбиться насмерть. Точно так же хорошая среда программирования должна обеспечивать механизмы, которые мешают делать глупости. Хорошая среда помогает писать надёжный код. Прежде всего, это должно помочь уменьшить сложность, предоставляя следующие вещи:

  • модульность и возможность повторного использования;
  • правильная изоляция состояния;
  • высокое отношение сигнал/шум.

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

Функциональное программирование

Что такое функциональное программирование (ФП)? Некоторые люди считают, что это очень сложная парадигма организации кода, которая применима только в академических кругах и не подходит для «реального мира». Функциональное программирование имеет прочную математическую основу и берёт начало в лямбда-исчислениях. Большинство его идей возникли как ответ на слабые стороны более распространённых языков программирования. Функции являются основной абстракцией функционального программирования. При правильном использовании функции обеспечивают такой уровень модульности и возможности повторного использования кода, какой никогда не встречался в ООП. ФП даже имеет шаблоны проектирования, которые решают проблемы null-значений и обеспечивают превосходную обработку ошибок.

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

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

Мы всё не так поняли про ООП

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

Erlang, вероятно, — единственный популярный объектно-ориентированный язык, хоть и обычно не считается таковым. Конечно, Smalltalk — это тоже чистый ООП-язык, однако он не используется широко. И Smalltalk, и Erlang используют ООП так, как это было задумано его изобретателем Аланом Кеем.

Обмен сообщениями

Алан Кей ввёл термин «объектно-ориентированное программирование» в 1960-х годах. Он имел опыт работы в области биологии и пытался заставить компьютерные программы общаться так же, как живые клетки. Основная идея состояла в том, чтобы независимые программы (ячейки) общались, отправляя друг другу сообщения. Состояние независимых программ никогда бы не открылось внешней среде (инкапсуляция). Вот оно. ООП никогда не предназначался для наследования, полиморфизма, ключевого слова new и множества шаблонов проектирования.

ООП в чистом виде

Erlang — это ООП в чистом виде. В отличие от других популярных языков, он сосредоточен на основной идее ООП — обмен сообщениями. В Erlang объекты взаимодействуют, передавая между собой неизменяемые сообщения. Есть ли доказательства того, что неизменяемые сообщения лучше методов? Да, чёрт возьми! Erlang самый надёжный язык в мире. Он поддерживает большую часть мировой телекоммуникационной инфраструктуры, включая интернет. Некоторые из систем, написанных на Erlang, имеют надежность 99,9999999 % (вы правильно прочитали — девять девяток).

Сложность кода

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

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

Что делает кодовую базу сложной? Есть много вещей, на которые следует обратить внимание. На мой взгляд, главными причинами являются: общее изменяемое состояние, ошибочные абстракции и низкое отношение сигнал/шум (бойлерплейт). Все они распространены в ООП.

Проблемы состояния

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

Изменчивое состояние — акт умственного жонглирования

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

Состояние само по себе довольно безобидно. Но изменчивое состояние — большая угроза стабильности. Особенно, если его распространять. Что именно является изменчивым состоянием? Любое состояние, которое может измениться: переменные или поля в ООП.

Рассмотрим пример из реального мира. У вас есть чистый кусок бумаги, вы пишете на нём заметку. В результате вы получаете тот же кусок в другом состоянии (текст). Вы фактически изменили его состояние. Это вполне нормально в реальном мире, поскольку никто не заботится об этом куске бумаги. Если только это не оригинальная картина «Мона Лиза».

Ограничения человеческого мозга

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

Программирование с изменяемым состоянием — это акт умственного жонглирования. Делать это двумя шарами довольно просто, но взяв три или больше, я уроню их все. С написанием кода так же. Я стал намного продуктивнее, а мой код стал намного надёжнее, как только я отбросил изменчивое состояние. Почему мы тогда пытаемся выполнять этот акт умственного жонглирования каждый день на работе? К сожалению, умственное манипулирование изменчивым состоянием лежит в основе ООП. Единственная цель существования методов объекта состоит в том, чтобы видоизменить этот же объект.

Разрозненное состояние

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

Рассмотрим пример из реального мира. Забудем на секунду, что мы все взрослые. Притворимся, что пытаемся собрать крутой грузовик из Lego. Но здесь есть одна загвоздка — все детали грузовика случайно смешаны с деталями других ваших игрушек Lego. И они были разложены в 50 разных коробках, снова случайно. И вам не разрешают группировать детали вашего грузовика. Вы должны держать в голове, где находятся различные его части, и можете вынимать их только одну за другой. Да, вы в конечном счёте соберёте этот грузовик, но сколько времени это займет?

Какое это имеет отношение к программированию? В функциональном программировании состояние обычно является изолированным. Вы всегда знаете, откуда оно исходит. Состояние никогда не разбросано по разным функциям. В ООП каждый объект имеет своё состояние. При построении программы вы должны иметь в виду состояние всех объектов, с которыми вы в данный момент работаете. Чтобы облегчить жизнь, лучше всего иметь очень небольшую часть кода, связанную с состоянием. Пусть основные части вашего приложения не будут содержать состояния и будут чистыми. Это является главной причиной успеха для Flux-паттерна в фронтенде (он же Redux).

Беспорядочно разделённое состояние

Изменчивое состояние в реальном мире почти никогда не является проблемой, поскольку вещи хранятся в частном порядке. Это «правильная инкапсуляция» на работе. Представьте художника, который работает над следующей картиной Моны Лизы. Он работает над картиной один. Заканчивает, а затем продаёт свой шедевр за миллионы. А потом он решает устроить рисовальную вечеринку и приглашает своих друзей — эльфа, Гэндальфа, полицейского и зомби. Все они начинают рисовать на одном и том же холсте одновременно. Конечно, ничего хорошего из этого не выйдет.

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

Проблемы параллелизма

Беспорядочное совместное использование изменяемого состояния в ООП делает распараллеливание такого кода практически невозможным. Для решения этой проблемы были изобретены сложные механизмы: блокировка потоков, мьютекс и многие другие. У таких сложных подходов есть свои недостатки — взаимоблокировки, отсутствие возможности компоновки, плюс отладка многопоточного кода очень сложна и отнимает много времени. Речь даже не идёт об увеличении сложности, вызванном использованием таких механизмов параллелизма.

Не все состояния — зло

Вероятно, изменение состояний по задумке Алана Кея — не зло. Оно, вероятно, полезно, если действительно изолировано (не как в ООП). Также вполне нормально иметь неизменяемые объекты передачи данных. Ключ здесь «неизменяемые». Такие объекты затем используются для передачи данных между функциями.

Однако такие объекты также делают методы и свойства ООП избыточными. Какая польза от наличия методов и свойств объекта, если его нельзя изменить?

Изменчивость неотъемлема в ООП

Некоторые утверждают, что изменяемое состояние — решение разработчиков в ООП, а не данность. Но это не так. Это не выбор разработчиков, а практически единственный вариант. Да, неизменяемые объекты можно передавать в методы Java/C#. Но это делается редко, поскольку большинство разработчиков по умолчанию используют изменение данных. Даже если разработчики пытаются использовать неизменяемость в своих программах ООП, языки не предоставляют встроенных механизмов для неизменяемости и для эффективной работы с неизменяемыми данными (то есть постоянными структурами данных).

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

Невозможно использовать ООП, не вызывая изменения состояния.

Троянский конь инкапсуляции

Часто упоминается, что инкапсуляция является одним из величайших преимуществ ООП. Предполагается защитить внутреннее состояние объекта от внешнего доступа. Но есть небольшая проблема. Это не работает.

Инкапсуляция — это троянский конь ООП. Он продвигает идею общего изменяемого состояния, делая его, казалось бы, безопасным. Инкапсуляция позволяет небезопасному коду проникать в кодовую базу (и даже поощряет это), заставляя её гнить изнутри.

Проблема глобального состояния

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

Позвольте объяснить. Всякий раз, когда создаётся объект, ссылки на его зависимости передаются конструктору. Эти зависимости также имеют своё внутреннее состояние. Вновь созданный объект хранит ссылки на эти зависимости в своём внутреннем состоянии, а затем изменяет их любым удобным для него способом. Он также передаёт эти ссылки на всё остальное, что может в конечном счёте использовать. Это создаёт сложный граф разнородных общих объектов, которые изменяют состояние друг друга. Это, в свою очередь, вызывает огромные проблемы. Становится почти невозможно увидеть, что вызвало изменение состояния программы. Дни могут быть потрачены впустую в попытках отладить такие изменения. И вам повезёт, если не придётся иметь дело с параллелизмом (подробнее об этом позже).

Методы/Свойства

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

Проблема с моделированием реального мира

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

Реальный мир не иерархичен

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

Наследование в реальном мире

ООП-наследование не отражает наследование реального мира. Родительский объект не может изменить поведение дочерних объектов во время выполнения. Даже если вы наследуете свою ДНК от родителей, они не могут вносить изменения в вашу ДНК по своему усмотрению. Вы не наследуете поведение от своих родителей. Вы развиваете своё поведение. И вы не можете переопределить поведение своих родителей.

В реальном мире нет методов

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

Королевство Существительных

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

Объекты (или существительные) находятся в самом центре ООП. Основное ограничение ООП состоит в том, что он превращает всё в объекты. Но не всё должно быть смоделировано как существительные. Операции (функции) не должны моделироваться как объекты. Зачем создавать класс Multiplier, когда всё, что нужно, — функция, умножающая два числа? Просто сделайте функцию Multiply. Пусть данные будут данными, а функции будут функциями.

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

Рассмотрим ситуацию из реального мира. Возьмём того же художника. Пусть он владеет фабрикой рисования (PaintingFactory). Он нанял менеджера по кистям (BrushManager), менеджера по цвету (ColorManager), менеджера по холсту (CanvasManager) и поставщика Моны Лизы (MonaLisaProvider). Его хороший друг зомби использует стратегию потребления мозга (BrainConsumingStrategy). Эти объекты, в свою очередь, определяют следующие методы: создать картину (CreatePainting), найти кисть (FindBrush), выбрать цвет (PickColor), вызвать Мону Лизу (CallMonaLisa) и потреблять мозги (ConsumeBrainz).

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

Модульное тестирование

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

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

  • извлечь его зависимости в отдельный класс;
  • создать интерфейс для вновь созданного класса;
  • объявить поля для хранения экземпляра вновь созданного класса;
  • использовать фреймворк, чтобы «mock-ать» зависимости;
  • использовать специальный фреймворк для внедрения зависимостей.

Сколько ещё препятствий нужно преодолеть, чтобы сделать фрагмент кода тестируемым? Сколько времени было потрачено впустую? Кроме того, нужно создавать экземпляр всего класса, чтобы протестировать один метод. Это подтянет код из всех его родительских классов. С ООП писать тесты для унаследованного кода ещё сложнее, практически невозможно. Целые компании были созданы (TypeMock) из-за проблемы тестирования легаси-кода.

Шаблонный код

Шаблонный код (бойлерплейт) является самой большой проблемой, когда речь идёт о соотношении сигнал/шум. Шаблонный код — это «шум» для компиляции программы. Такой код требует времени для написания и делает кодовую базу менее читаемой.

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

Тестирование приватных методов

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

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

Рефакторинг

Рефакторинг является важной частью работы разработчика. По иронии судьбы ООП-код трудно реорганизовать. Рефакторинг должен сделать код менее сложным и более понятным. Напротив, реорганизованный код в ООП становится значительно более сложным. Чтобы сделать код тестируемым, нужно использовать внедрение зависимостей и создать интерфейс для реорганизованного класса. И даже тогда рефакторинг ООП-кода сложен без специальных инструментов, таких как Resharper.

			// До рефакторинга:
public class CalculatorForm {
    private string aText, bText;
    
    private bool IsValidInput(string text) => true;
    
    private void btnAddClick(object sender, EventArgs e) {
        if ( !IsValidInput(bText) || !IsValidInput(aText) ) {
            return;
        }
    }
}


// После рефакторинга:
public class CalculatorForm {
    private string aText, bText;
    
    private readonly IInputValidator _inputValidator;
    
    public CalculatorForm(IInputValidator inputValidator) {
        _inputValidator = inputValidator;
    }
    
    private void btnAddClick(object sender, EventArgs e) {
        if ( !_inputValidator.IsValidInput(bText)
            || !_inputValidator.IsValidInput(aText) ) {
            return;
        }
    }
}

public interface IInputValidator {
    bool IsValidInput(string text);
}

public class InputValidator : IInputValidator {
    public bool IsValidInput(string text) => true;
}

public class InputValidatorFactory {
    public IInputValidator CreateInputValidator() => new InputValidator();
}
		

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

Сравните это с аналогичным рефакторингом не-ООП-кода в JavaScript:

			// До рефакторинга:
// calculator.js:
const isValidInput = text => true;

const btnAddClick = (aText, bText) => {
  if (!isValidInput(aText) || !isValidInput(bText)) {
    return;
  }
}

// После рефакторинга:
// inputValidator.js:
export const isValidInput = text => true;

// calculator.js:
import { isValidInput } from './inputValidator';

const btnAddClick = (aText, bText, _isValidInput = isValidInput) => {
  if (!_isValidInput(aText) || !_isValidInput(bText)) {
    return;
  }
}
		

Код буквально остался прежним. Функция isValidInput() просто переместилась в другой файл и добавилась одна строка для импорта этой функции. Также добавилась _isValidInput() к сигнатуре функции для удобства тестирования.

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

Костыли в ООП

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

Шаблоны проектирования

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

Фабрика проблем

Фактически невозможно написать хороший и поддерживаемый объектно-ориентированный код.

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

Вскоре добавление новой функциональности и даже понимание всей сложности становится всё труднее и труднее. Кодовая база будет полна таких вещей, как SimpleBeanFactoryAwareAspectInstanceFactory, AbstractInterceptorDrivenBeanDefinitionDecorator, TransactionAwarePersistenceManagerFactoryProxy или RequestProcessorFactoryFactory.

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

Мнение: объектно-ориентированное программирование — катастрофа на триллион долларов 1

Статья на тему: FizzBuzzEnterpriseEdition.

Падение четырёх столпов ООП

Четыре столпа ООП: абстракция, наследование, инкапсуляция и полиморфизм.

Посмотрим, что они из себя представляют на самом деле, один за другим.

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

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

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

Есть несколько проблем с наследованием:

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

Полиморфизм

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

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

Инкапсуляция

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

Абстракция

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

Причудливое слово для простой концепции. В процедурных/функциональных языках вы можете просто «спрятать» детали реализации в соседнем файле. Нет необходимости называть этот основной акт «абстракцией».

Для подробной информации о столпах ООП можете прочитать статью: «Goodbye, Object-Oriented Programming».

Почему ООП доминирует в индустрии?

Ответ прост: рептилоидная инопланетная раса вступила в сговор с АНБ (и русскими), чтобы замучить вас (программистов) до смерти.

А если серьезно, то, вероятно, ответ — Java.

Java — самая неприятная вещь, случившаяся с компьютерами со времен MS-DOS.

Java был прост

Когда он был впервые представлен в 1995 году, Java был очень простым языком программирования по сравнению с альтернативами. В то время входной барьер для написания настольных приложений был высок. Разработка настольных приложений включала написание низкоуровневых API-интерфейсов win32 на С. Разработчики также должны были заниматься ручным управлением памятью. Другой альтернативой был Visual Basic, но многие не хотели замыкаться в экосистеме Microsoft.

Когда появился Java, для многих разработчиков работать с ним было просто. Он был бесплатным и мог использоваться на всех платформах. Такие вещи, как встроенная сборка мусора, понятные API-интерфейсы (по сравнению с загадочными win32-API), правильные пространства имён и знакомый C-подобный синтаксис сделали Java ещё более доступным.

GUI-программирование также становилось всё более популярным. Казалось, что различные компоненты пользовательского интерфейса хорошо отображаются на классы. Автозаполнение метода в IDE также заставило людей утверждать, что ООП API проще в использовании.

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

Затем появился C#

Первоначально Microsoft в значительной степени опиралась на Java. Когда что-то начало идти не так (и после долгой юридической битвы с Sun Microsystems за лицензирование Java), Microsoft решила инвестировать в свою собственную версию Java. Так появился C# 1.0. C# как язык всегда считался «лучшей версией Java». Однако есть одна огромная проблема — это тот же язык ООП с теми же недостатками, скрытыми под слегка улучшенным синтаксисом.

Microsoft вкладывала значительные средства в свою экосистему .NET, которая также включала в себя хорошие инструменты для разработчиков. В течение многих лет Visual Studio была одной из лучших доступных сред IDE. Это привело к широкому распространению платформы .NET, особенно на предприятиях.

В последнее время Microsoft вкладывает значительные средства в экосистему браузера, продвигая свой TypeScript. TypeScript великолепен, потому что он может компилировать чистый JavaScript и добавляет такие вещи, как статическая проверка типов. Но уже не так великолепно отсутствие надлежащей поддержки функциональных конструкций: нет встроенных неизменяемых структур данных, нет композиции функций, нет правильного сопоставления с образцом. TypeScript является в первую очередь ООП-языком. В основном это C# для браузеров. Даже отвечал за разработку и C#, и TypeScript один человек — Андерс Хейлсберг.

Функциональные языки

Функциональные языки никогда не поддерживались такими крупными компаниями, как Microsoft. F# не считается, так как инвестиции были незначительными. Разработка функциональных языков в основном осуществляется сообществом. Это объясняет различия в популярности между объектно-ориентированными и функциональными языками.

Пора двигаться дальше?

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

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

Генри Форд однажды сказал: «Если бы я спросил людей, чего они хотят, они бы ответили — более быстрых лошадей». В мире программного обеспечения большинство людей захотят «лучший ООП-язык». Люди могут легко описать пожелания, которые у них есть (более организованная и менее сложная кодовая база), но не лучшее решение.

Каковы альтернативы?

Внимание, спойлер — функциональное программирование.

Если термины вроде «функтор» или «монада» вызывают у вас некоторое беспокойство, то вы не одиноки. Функциональное программирование не было бы таким страшным, если бы оно дало более интуитивные названия некоторым из его концепций. Функтор — то, что можно преобразовать с помощью функции (вспомните list.map). Монада — вычисления, которые можно объединить в цепочку.

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

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

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

Какие есть ещё варианты? Ваша организация уже использует C#? Попробуйте F# — удивительный функциональный язык, обеспечивающий отличную совместимость с существующим кодом .NET. Используете Java? Тогда Scala или Clojure — хорошие альтернативы. Используете JavaScript? С правильным руководством и линтингом JavaScript может быть хорошим функциональным языком.

Защитники ООП

Реакция от защитников ООП вполне ожидаема. Они скажут, что эта статья полна неточностей. Некоторые могут даже начать называть имена. Они могли бы даже назвать меня junior-разработчиком без реального опыта ООП. Кто-то может сказать, что мои предположения ошибочны, а примеры бесполезны. Это не имеет значения.

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

Закон Деметры не очень полезен — он ничего не делает для решения проблемы недетерминированности. Общее изменяемое состояние всё ещё является общим изменяемым состоянием, независимо от того, каким образом вы получаете доступ или изменяете его. Метод a.total() не сильно лучше a.getB().getC().total(). Это просто заметает проблему под ковёр.

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

Просто инструмент в наборе

Часто слышу мнение людей, что ООП — просто ещё один инструмент в наборе. Да, это такой же инструмент, как и то, что лошади и машины — инструменты для перевозки. В конце концов, все они служат одной цели. Зачем использовать машины, если можно продолжать ездить на старых добрых лошадях?

История повторяется

В начале 20-го века автомобили начали заменять лошадей. В 1900 году в Нью-Йорке было всего несколько автомобилей на дорогах, но в основном люди использовали для перевозки лошадей. Огромная индустрия была сосредоточена вокруг конного транспорта. Целые предприятия были созданы вокруг уборки навоза. Люди сопротивлялись переменам. Они называли автомобили ещё одной «модой», которая в конечном итоге проходит, ведь лошади были здесь веками. Некоторые даже просили правительство вмешаться. В 1917 году на дорогах больше не осталось лошадей.

Насколько это актуально? Индустрия программного обеспечения сосредоточена вокруг ООП. Миллионы людей обучаются ООП и миллионы компаний используют его в своём коде. Конечно, они попытаются дискредитировать всё, что угрожает их хлебу с маслом. Это просто здравый смысл. Ясно видно, как история повторяется. В 20-м веке это были лошади против автомобилей, а в 21-м — объектно-ориентированное и функциональное программирование.

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