Главная

Объектно-ориентированный мусор.

Идея объектно-ориентированного программирования завоевала пьедестал.

1,425 

29+

“Спагетти код” , написанный с помощью ООП, превращается в “Лазанья код”

Роберто Уолтман

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

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

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

Наследование считается одним из главных преимуществ ООП. Все примитивные примеры наглядно демонстрируют, что наследование имеет смысл. Термины “повторное использование” довольно часто используются при обсуждении основ наследования.

Проблема банановых обезьян в джунглях (с англ. Banana Monkey Jungle Problem)

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

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

Джо Армстронг

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

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

Ромбовидное наследование (diamond problem)

Проблема ромбовидного наследования возникает в объектно-ориентированных языках, которые поддерживают множественное наследование. Суть проблемы заключается в том, что есть возможность наследования от одного и того же класса несколькими способами(как показано на рисунке выше). Проблема ромбовидного наследования — это неоднозначность, которая возникает, когда два класса, A и B, наследуются от суперкласса, а класс C наследуется как от A, так и от B.
Если в суперклассе есть метод, который переопределяется в классе A и B, а в C нет, то метод какого класса наследует C: класса A или B?

Действительная суть проблемы
В действительности эта проблема возникает только в ООП языках программирования, которые поддерживают множественное наследование (например C++).

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

Проблема хрупкого базового класса

Хрупкий базовый класс является распространенной проблемой для языков программирования, которые поддерживают наследование. Короче говоря, есть базовый класс, от которого вы наследуете все остальные классы. Его часто называют хрупким потому, что изменения в этом классе могут иметь неожиданные последствия в классах, которые наследуют его.
Я буду использовать пример, использованный Чарльзом Скалфани в своей статье:

Рассмотрим вышеописанный код.
Обратите внимание на строку с комментарием. Впоследствии я изменю эту строку и она все сломает! Этот класс имеет две функции на своем интерфейсе, add() и addAll(. Функция add() добавляет один элемент, а addAll() добавляет несколько элементов, используя при этом функцию add().
А вот и производный класс:

Класс ArrayCount является специализацией общего класса Array. Единственное поведенческое различие заключается в том, что ArrayCount ведет подсчет количества элементов.
Давайте рассмотрим оба класса подробнее.
Array add() добавляет элемент в локальный ArrayList.
Array addAll() вызывает локальный ArrayList, добавленный для каждого элемента.
ArrayCount add() вызывает метод add() своего родителя, а затем увеличивает счетчик.
ArrayCount addAll() вызывает метод addAll() своего родителя, а затем увеличивает счетчик на количество элементов.
Все работает отлично.
Теперь время переломных моментов. Строку с комментарием в базовом классе нужно изменить следующим образом:

Что касается владельца базового класса, он все еще функционирует так, как объявлено. Все автоматизированные тесты все еще проходят, но владелец не замечает производного класса, а ожидает грубого пробуждения.
Теперь ArrayCount addAll() вызывает метод addAll() своего родителя, который внутренне вызывает метод add(), который был переопределен производным классом.

Это приводит к тому, что счетчик увеличивается каждый раз, когда вызывается производный класс add(), а затем снова увеличивается на количество элементов, добавленных в производный класс addAll(). Выходит, что счетчик увеличивается дважды. Автор производного класса должен обязательно знать, как реализован базовый класс. Каждое изменение базового класса может привести к непредсказуемым последствиям в производных классах.

Действительная суть проблемы
Основной проблемой наследования является связь родительского класса с его дочерними классами. Родительский класс не может самостоятельно развиваться без последствий для своих детей. Существуют принципы SOLID разработки, которые предполагают, что конкретные классы должны зависеть от абстракций. Абстракции не подвержены проблеме хрупкого базового класса. Также хорошей практикой является пометка всех окончательных(тех, от которых не будет наследования) классов. Родительские классы стоит проектировать так, как API: скрывать все детали реализации; быть строгим в отношении инпута и аутпута. Также не забывайте подробно описывать ожидаемое поведение класса.

Заключение

Senior full-stack-разработчик Илья Суздальницкий опубликовал эссе на 6000 слов, назвав объектно-ориентированное программирование «катастрофой на триллион долларов». Драгоценное время и силы тратятся на размышления об «абстракциях» и «шаблонах проектирования», а не на решение реальных проблем… Объектно-ориентированное программирование (ООП) было создано с одной целью — управлять сложностью процедурных кодовых баз. Другими словами, оно должно было улучшить организацию кода. Нет объективных доказательств того, что ООП лучше, чем простое процедурное программирование…

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

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

Я не критикую Алана Кея (основополагатель ООП) — он гений. Я хочу, чтобы ООП было реализовано так, как изначально задумывалось. Я критикую современный подход Java/C# к ООП… Я думаю, что совершенно неверно, что ООП де-факто считается стандартом организации кода для многих людей, в том числе людей высоких технических должностей. Также неправильно, что много современных языков не предлагают других альтернатив организации кода, кроме ООП.
Эссе Ильи Суздальницкого в конечном счете обвиняет Java в популяризации ООП, ссылаясь на комментарий Алана Кея: «Java является самой печальной вещью, случившейся с вычислительной техникой со времен MS-DOS». В нем также цитируется замечание Линуса Торвальдса о том, что «ограничение проекта языком программирования C означает, что люди не напутают какую-то идиотскую объектную модель». В конечном итоге он предлагает функциональное программирование в качестве превосходной альтернативы, делая следующие утверждения о ООП:

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

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

29+