Объектно-ориентированное программирование – самая большая ошибка компьютерных наук
Рассказываем, как ООП наносит огромный ущерб экономике и почему пора менять парадигму программирования. Присоединяйтесь к надвигающейся революции! Обсудить C++ и Java, вероятно, являются одними из худших ошибок, которые когда-либо делала компьютерная наука. Многие выдающиеся программисты, включая создателя ООП Алана Кея, критиковали и продолжают критиковать эти языки за то, что они извратили первоначальную идею и породили самую печально известную парадигму современной разработки. К несчастью, современное ООП приобрело огромную популярность. Это позволило ему нанести огромный ущерб экономике – триллионы долларов – и даже убить (в буквальном смысле) тысячи человек! Почему же ООП так опасен? *** Представьте, что вы садитесь в свою хорошо знакомую машину и едете по хорошо знакомому маршруту. И вдруг что-то идет не так, как всегда. Вы отпускаете педаль газа, но машина продолжает ускоряться! Давите на тормоз – не помогает! Страшно? Подобный инцидент произошел в 2007 году с Джин Букаут – и еще с сотнями водителей Toyota Camry. Десятки человек погибли. Производитель ссылался на залипание педалей, человеческий фактор и даже половые коврики, однако истинным виновником оказалось программное обеспечение автомобилей. Команда экспертов 18 месяцев разбирала кодовую базу Toyota – и нашла сотни потенциальных причин возникновения непреднамеренного ускорения. Код в целом был запутанным и беспорядочным – то, что на сленге называется «спагетти». Из-за этого Toyota пришлось отозвать более 9 млн автомобилей и выплатить более 3 млрд долларов. Подобные инциденты не уникальны – и это пугает. Например, два самолета Boeing 737 Max потерпели крушение из-за ошибки, вызванной спагетти-кодом (346 жертв, 60 млрд долларов ущерба). То же самое может случиться, например, на атомной электростанции или в реанимационной палате. Как ни странно, программный код пишется прежде всего для людей. Мартин Фаулер говорил, что любой дурак может написать код, понятный компьютеру, а хорошие программисты пишут код, понятный людям. Если код непонятен, то очень скоро он перестанет работать. Спагетти-код непонятен, связи между его частями не очевидны. Любое изменение может что-нибудь сломать. На такой код невозможно писать тесты, его сложно расширять и больно поддерживать. Со временем любой код может превратиться в спагетти. По мере усложнения он становится все более и более запутанным, энтропия растет – и в один прекрасный момент вы уже имеете дело со клубком зависимостей. Чтобы бороться с этим нужны строгие ограничения, вроде ограничения скорости на дорогах. Причем такие ограничения должны быть максимально автоматизированы и не зависеть от человеческого фактора. В идеале их должна накладывать сама парадигма программирования. ООП не накладывает на код никаких ограничений, которые могли бы предотвратить его запутывание. Безусловно, есть лучшие практики разработки – внедрение зависимостей, TDD, Domain Driven Design, которые реально помогают. Однако они не внедрены в парадигму, не обеспечиваются ей, и нет инструментария, который мог бы следить за их соблюдением. Встроенные фичи ООП только добавляют путаницы. Например, инкапсуляция скрывает и рассеивает состояние по всей системе. Преимущества полиморфизма еще сомнительнее – мы не знаем точно, какой путь выполнения выберет наша программа. Если вдобавок ко всему этому приходится иметь дело с несколькими уровнями наследования, спагетти-код обеспечен. В большинстве объектно-ориентированных языков данные передаются по ссылке, то есть разные участки программы могут иметь дело с одной и той же структурой данных – и менять ее. Это превращает программу в один большой сгусток глобального состояния и противоречит первоначальной идее ООП. Алан Кей, создавая свой язык Simula, предполагал, что части программы будут общаться между собой подобно биологическим клеткам, то есть независимо. Они должны были посылать друг другу сообщения, оставаясь закрытыми от внешнего мира (инкапсуляция). Но в современном ООП одни «клетки» проникают внутрь других и меняют их состояние. Это приводит к большой связанности кода. Изменения в одном месте программы могут привести к неожиданным последствиям в другом. Склонность к спагеттификации – не единственная проблема ООП-парадигмы. Работая с ПО мы обычно хотим, чтобы оно было надежным и предсказуемым. Если В информатике, «недетерминированный алгоритм» – это алгоритм, указывающий несколько путей обработки одних и тех же входных данных, – без какого-либо уточнения, какой именно вариант будет выбран. Википедия Если эта цитата вам не нравится, то это потому, что недетерминированность в целом никуда не годится. Вот пример кода, который просто вызывает функцию: nondet-det-func.js Мы не знаем, что делает эта функция, но кажется, что она всегда возвращает один и тот же результат для одних и тех же входных данных. А вот другой код: nondet-nondet-func.js Эта функция вернула разные значения для одних и тех же входных параметров. Функция Что делает функцию детерминированной или недетерминированной? nondet-func-impl.js Результат работы функции Функция Детерминированный код предсказуем, недетерминированный – нет. Давайте рассмотрим простую функцию сложения: nondet-simple-add.js В большинстве языков программирования операция сложения реализуется на аппаратном обеспечении, то есть за нее отвечает очень надежный процессор. Поэтому мы всегда можем быть уверены, что функция Теперь немного усложним задачу – введем в бой объекты: nondet-add-boxed.js Пока все идет хорошо. Давайте внесем небольшое изменение в тело функции nondet-add-mutation.js Что случилось? Внезапно результат перестает быть предсказуемым! В первый раз код сработал нормально, но с каждым последующим запуском его вывод становился все более и более неожиданным. Другими словами, функция больше не детерминирована. Почему это вдруг произошло? Потому что функция начала вызывать побочные эффекты, изменив значение за пределами своей области действия. *** Детерминированная программа гарантирует, что Недетерминированные программы – это полная противоположность. В большинстве случаев вызов Каковы последствия недетерминированного кода? В нем легко появляются дефекты – баги, которые заставляют разработчиков тратить драгоценное время на отладку и значительно ухудшают качество работы ПО. Чтобы сделать наши программы более надежными, мы должны в первую очередь заняться их детерминизмом. Что такое побочный эффект? Если вы принимаете лекарство от головной боли, но оно вызывает у вас тошноту, то тошнота является побочным эффектом. Проще говоря, этот что-то нежелательное и не связанное с основной задачей препарата. Вернемся к нашей функции сложения: nondet-add-func-only.js Функция Так как объекты передаются по ссылке, то и объект Разобравшись с детерминизмом и побочными эффектами, мы готовы говорить о чистоте. Чистая функция – это функция, которая одновременно детерминирована и не имеет побочных эффектов. То есть у нее всегда предсказуемый результат работы, и она не делает ничего лишнего. Чистые функции имеют множество преимуществ: И так далее. Проще говоря, чистые функции возвращают радость в программирование. Для примера давайте поговорим о двух фичах ООП — геттерах и сеттерах. Результат геттера зависит от внешнего состояния — состояния объекта. Многократный вызов геттера может привести к различным выходным данным, в зависимости от состояния системы. Это делает геттеры изначально недетерминированными. Сеттеры предназначены для изменения состояния объекта, что делает их по своей сути побочными эффектами. Таким образом, все методы в ООП (кроме, возможно, статических) либо не детерминированы, либо вызывают побочные эффекты. Следовательно, ООП – это что угодно, только не чистое программирование. Есть ли что-то, что может спасти программирование – луч надежды в мрачном мире программных сбоев? Что-то достаточно надежное и детерминированное, чтобы на нем можно было строить качественное ПО? Это математика. В computer science математика воплотилась в парадигме функционального программирования, основанного на системе лямбда-исчисления. А на чем основано современного ООП? Уже не на биологических законах клеток, как планировал Алан Кей. Оно базируется на множестве нелепых идей вроде классов и наследования, склеенных скотчем, чтобы лучше держались. Основной строительный блок функционального программирования – функция в ее математическом понимании. В большинстве случаев – чистая функция, детерминированная и предсказуемая. Программа, составленная из таких функций, тоже становится предсказуемой. Значит ли это, что в такой программе нет багов? Конечно нет. Однако и баги в ней детерминированы. Для одних и тех же данных всегда будет возникать одна и та же ошибка, что существенно облегчает ее исправление. До появления процедур и функций в программировании широко использовался оператор Очень похожий процесс происходит сейчас с ООП. Только вместо вопроса «как я попал в эту точку исполнения», мы спрашиваем «как я попал в это состояние». ООП (и императивное программирование в целом) не может нам ответить. Когда все передается по ссылке, любой объект может быть изменен другим объектом – и парадигма не накладывает никаких ограничений, чтобы это предотвратить. Инкапсуляция совсем не помогает – вызов метода для мутации некоторого поля объекта ничем не лучше, чем его непосредственная мутация. Программы быстро превращаются в месиво зависимостей и глобального состояния. Функциональное программирование избавит нас от сложных вопросов. Когда состояние остается неизменным, оно не вызывает проблем. Раньше многие разработчики сопротивлялись рекомендации прекратить использовать В ООП есть много лучших практик, которые теоретически должны помочь справиться со спагеттификацией, например, предпочтение композиции наследованию. Однако сама парадигма ООП не накладывает никаких ограничений и не следит за применением этих практик. Можете ли вы поручиться, что джуниоры в вашей команде будут их соблюдать? В функциональном программировании есть только композиция. Функции вызывают другие функции, большие функции состоят из малых. Это единственный способ построения программ. Иначе говоря, парадигма программирования сама навязывает лучшую практику, она является нативной, естественной. Когда у нас нет мешанины связей и зависимостей, разработка, тестирование и рефакторинг становятся удовольствием. Жаль вас разочаровывать, но это не так. Объектно-ориентированное программирование – полная противоположность функциональному. Оно нарушает многие фундаментальные принципы: Программисты ООП тратят большую часть своего времени на исправление ошибок. Программисты ФП – на написание работающего кода. ООП было очень большой и ужасно дорогой ошибкой. Давайте все, наконец, признаем это. Если ваш автомобиль работает на объектно-ориентированном ПО, вы не можете быть спокойны. Пришло время принять меры. Мы все должны осознать опасность ООП и начать делать маленькие шаги в сторону функционального программирования. Это не быстрый процесс, реального сдвига можно ожидать не раньше, чем через 10 лет, однако он должен произойти. В ближайшем будущем ООП-программисты станут «динозаврами», как сейчас разработчики на COBOL. C++, Java, C# умрут. TypeScript тоже умрет. Начните изучать функциональное программирование прямо сейчас! F#, ReasonML и Elixir – все это отличные варианты для начала. Присоединяйтесь к надвигающейся революции!Проблема спагетти
Откуда берется спагетти-код?
ООП – корень зла
Ссылочные типы
Предсказуемость
2 + 2
всегда должно быть равно 4
, а нажатие на педаль тормоза всегда должно приводить к замедлению автомобиля. Это называется детерминированностью.2+2
будет равно 5
хотя бы один раз из миллиона, это может привести к ужасным последствиям.
console.log( 'result', computea(2) ); console.log( 'result', computea(2) ); console.log( 'result', computea(2) ); // result 4 // result 4 // result 4
console.log( 'result', computeb(2) ); console.log( 'result', computeb(2) ); console.log( 'result', computeb(2) ); // result 4 // result 4 // result 4 // result 2 <= плохо!
computeb
недетерминирована. Она может выдавать ожидаемое значение, но это не гарантируется.
function computea(x) { return x * x; } function computeb(x) { return Math.random() < 0.9 ? x * x : x; }
computea
зависит только от аргумента x
и всегда будет одинаков для одинакового x
. Эта функция детерминирована.computeb
недетерминирована, потому что вызывает недетерминированную функцию Math.random
. С чем мы взяли, что Math.random
недетерминирована? Она не принимает аргументов, а вычисление случайной величины основывается на системном времени, то есть зависит от внешнего состояния.Непредсказуемость
function add(a, b) { return a + b; };
add
детерминирована.
const box = value => ({ value }); const two = box(2); const twoPrime = box(2); function add(a, b) { return a.value + b.value; } console.log("2 + 2' == " + add(two, twoPrime)); console.log("2 + 2' == " + add(two, twoPrime)); console.log("2 + 2' == " + add(two, twoPrime)); // output: // 2 + 2' == 4 // 2 + 2' == 4 // 2 + 2' == 4
add
:
function add(a, b) { a.value += b.value; return a.value; } console.log("2 + 2' == " + add(two, twoPrime)); console.log("2 + 2' == " + add(two, twoPrime)); console.log("2 + 2' == " + add(two, twoPrime)); // output: // 2 + 2' == 4 // 2 + 2' == 6 // 2 + 2' == 8
2+2 == 4
. Другими словами для входных параметров (2, 2)
функция add
всегда должна возвращать 4
. Независимо от того, сколько раз вы ее вызвали, работает ли она параллельно и что происходит за ее пределами.add(2, 2)
вернет 4
. Но время от времени функция может возвращать 3
, 5
или даже 1004
. Недетерминизм крайне нежелателен, и теперь вы понимаете, почему.Побочные эффекты
function add(a, b) { a.value += b.value; return a.value; } add(two, twoPrime)
add
выполняет ожидаемую операцию, она добавляет a
к b
. Однако это также приводит к побочному эффекту – изменению объекта a
. two
за пределами функции изменился. two.value
теперь равно 4
. После второго вызова станет 6
и так далее.Чистота
Насколько чисто ООП?
Чистое решение
Как я тут оказался?
goto
. Он позволял перейти к любой части кода во время работы. Из-за этого разработчики часто попадали в сложные ситуации, когда было непонятно, как они оказались в текущей части программы.goto
. К сожалению, сейчас ситуация повторяется с идеями функционального программирования.А как же спагетти-код?
Но ООП и ФП дополняют друг друга!
Действуйте, пока не поздно
- 4 views
- 0 Comment
Свежие комментарии