[Перевод] ФП vs ООП

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


udcfhd13y7i2zdqhnhtotltstyg.jpeg

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

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

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

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


Что такое ООП?

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

Посмотрите на два выражения:

1: f (o); 2: o.f ();

В чём разница?

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

Когда мы видим выражение 1. мы видим функцию f, которая вызывается в которую передаётся объект o. При этом подразумевается, что есть только одна функция с именем f, и не факт, что она является членом стандартной когорты функций, окружающих o.

С другой стороны, когда мы видим выражение 2. мы видим объект с именем o которому посылают сообщение с именем f. Мы ожидаем, что могут быть другие виды объектов, котоые принимают сообщение f и поэтому мы не знаем, какого конкретно поведения ожидать от f после вызова. Поведение зависит от типа o. то есть f — полиморфен.

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

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

Поэтому то, что действительно отличает ООП программы от не ООП программ это полиморфизм.

Возможно вы захотите возразить, что полифорфизм можно сделать просто используя внутри f switch или длинные цепочки if/else. Это правда, поэтому мне нужно задать для ООП ещё одно ограничение.

Использование полиморфизма не должно создавать зависимости вызывающего от вызываемого.

Чтобы это объяснить, давайте ещё раз посмотрим на выражения. Выражение 1: f (o), похоже зависит от функции f на уровне исходного кода. Мы делаем такой вывод потому что мы также предполагаем, что f только одна и что поэтому вызывающий должен знать о вызываемом.

Однако, когда мы смотрим на Выражение 2. o.f () мы предполагаем что-то другое. Мы знаем, что может быть много реализаций f и мы не знаем какая из этих функций f будет вызвана на самом деле. Следовательно исходный код, содержащий выражение 2 не зависит от вызываемой функции на уровне исходного кода.

Если конкретнее, то это означает, что модули (файлы с исходным кодом), которые содержат полиморфные вызовы функций не должны ссылаться на модули (файлы с исходным кодом), которые содержат реализацию этих функций. Не может быть никаких include или use или require или каких-то других ключевых слов, которые создают зависимость одних файлов с исходным кодом от других.

Итак, наше редукционистское определение ООП это:


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


Что такое ФП?

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

f (a) == f (b) если a == b.

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

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

У этого есть и более глубокое следствие — ни одно именованное значение нельзя менять. То есть оператора присваивания нет.

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

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

Это только один из простых трюков, которые фукциональная программа может использовать чтобы хранить изменения состояния без необходимости изменять состояние [1].

Итак, редукционистское определение функционального программирования:


Референциальная Прозрачность — переприсваивать значения нельзя.


ФП против ООП

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

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

Но ортогональность не подразумевает взаимного исключения (спросите Джеймса Клерка Максвелла). Вполне можно создать систему, которая использует и динамический полиморфизм и референциальную прозрачность. Это не только возможно, это правильно и хорошо!

Почему эта комбинация хороша? По точно тем же причинам, что оба её компонента! Системы построенные на динамическом полиморфизме хороши, потому что они обладают низкой связностью. Зависимости можно инвертировать и расположить по разные стороны архитектурных границ. Эти системы можно тестировать используя Моки и Фейки и другие виды Тестовых Дублей. Модули можно модифицировать не внося изменения в другие модули. Поэтому такие системы проще изменять и улучшать.

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

Главная мысль тут такая:


Нет никакого холивара ФП против ООП

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

[1] Так как мы используем машины с архитектурой Фон Неймана мы предполагаем, что в них есть ячейки памяти, состояние которых на самом деле изменяется. В механизме рекурсии, который я описал, оптимизация хвостовой рекурсии не даст создавать новые стекфреймы и будет использоваться первоначальный стекфрейм. Но это нарушение референциальной прозрачности (обычно) скрыто от программиста и ни на что не влияет.

© Habrahabr.ru