Почему ООП — это плохо

Это перевод статьи Джо Армстронга Why OO Sucks, повествование ведётся от имени автора оригинальной публикации.

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

Когда Erlang стал популярным, у нас часто спрашивали, относится ли он к объектно-ориентированным языкам. Правдивый ответ на этот вопрос должен быть таким: «Конечно, нет, Erlang не объектно-ориентированный язык». Но мы не хотели акцентировать на этом внимание, поэтому придумали уловку. Мы говорили вслух, что Erlang в некотором роде является объектно-ориентированным языком. Такой ответ удовлетворял тех, кому везде нужна объектная ориентированность. Но вдумчивый собеседник слышал в этом ответе правильный посыл: «Erlang — не совсем объектно-ориентированный язык».

В такие моменты я вспоминал слова руководителя подразделения IBM во Франции, которые он сказал на открытии конференции IEEE Logic в Париже. В языке Prolog разработчики IBM добавили много объектно-ориентированных расширений. На вопрос «зачем это сделано» руководитель ответил: «Наши потребители хотели, чтобы Prolog был объектно-ориентированным, поэтому мы сделали его объектно-ориентированным».

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

Так почему же объектно-ориентированное программирование — это плохо?

Моя оценка ООП касается основных принципов этой парадигмы. Мои возражения против некоторых из них приводятся ниже.

Возражение 1: структуры данных и функции не должны быть связанными друг с другом

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

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

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

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

Обычно «понимание» ограничивается наблюдением, что функция — это часть вычислительной системы, чья работа — изменять структуры данных типа Т1 в структуры данных типа Т2.

Так как функции и структуры данных имеют разную природу, не надо считать их одинаковыми и работать с ними как с одинаковыми сущностями.

Возражение 2: всё есть объект

Представьте себе time. В объектно-ориентированном языке time должно быть объектом. В Smalltalk даже число 3 — объект. Но в языках, которые не относятся к объектно-ориентированным, time — экземпляр типа данных. Например, в Erlang есть много разных вариантов времени, которые можно чётко и однозначно определить.

-deftype day()     = 1..31.
-deftype month()   = 1..12.
-deftype year()    = int().
-deftype hour()    = 1..24.
-deftype minute()  = 1..60.
-deftype second()  = 1..60.
-deftype abstime() = {abstime,year(),month(),day(),hour(),min(),sec()}.
-deftype hms()     = {hms,hour(),min(),sec()}.
…

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

Возражение 3: в объектно-ориентированных языках определения типов данных распространяются везде

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

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

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

Глобальный тип может быть связанным списком, массивом, хэш-таблицей или более сложным объектом.

В объектно-ориентированном языке мне придётся выбирать объект, в котором я определю глобальный тип. Все остальные объекты, которые захотят использовать эту структуру данных, должны наследовать этот объект. Допустим, я хочу создать объект time, в котором определяется эта структура данных, и в котором… Это очень сложно.

Возражение 4: у объектов есть приватное состояние

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

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

С учётом того, что в реальном мире состояния распространены, какие условия должен обеспечить язык программирования для работы с состояниями?

  • Объектно-ориентированные языки говорят «прячьте состояние от программиста». Состояние скрывается, и доступ к нему можно получить с помощью функций.
  • Обычные языки программирования, например, C или Pascal, говорят, что состояние переменных контролируется правилами области видимости языка.
  • Чистые декларативные языки говорят, что состояния не существует. Глобальное состояние системы передаётся во все функции и выходит из всех функций. Механизмы типа монад в функциональных языках и DC-грамматики в логических языках программирования используются, чтобы спрятать состояние от программистов. Поэтому программисты могут работать так, как будто состояние не имеет значение, но при этом у них может быть доступ к состоянию системы, если это необходимо.

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

Почему же ООП популярно?

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

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