Объектно-ориентированное программирование — чрезвычайно плохая идея, которая могла возникнуть только в Калифорнии.
— Эдсгер Вибе Дейкстра
Возможно, это только мои ощущения, но объектно-ориентированное программирование кажется стандартной, самой распространённой парадигмой проектирования ПО. Именно его обычно преподают студентам, объясняют в онлайн-туториалах и, по какой-то причине, спонтанно применяют даже тогда, когда не собирались этого делать.
Я знаю, насколько она привлекательна, и какой замечательной кажется эта идея на поверхности. На разрушение её чар у меня ушли многие годы, и теперь я понимаю, насколько она ужасна, и почему. Благодаря этой точке зрения у меня есть чёткая уверенность в том, что люди должны осознать ошибочность ООП и знать решения, которые можно использовать вместо него.
Многие люди и раньше обсуждали проблемы ООП, и в конце этого поста я приведу список своих любимых статей и видео. Но прежде я хочу поделиться собственным взглядом.
Данные важнее, чем код
По своей сути всё ПО предназначено для манипуляций данными с целью достижения определённого результата. Результат определяет способ структурирования данных, а структура данных определяет необходимый код.
Этот момент очень важен, так что я повторюсь:
цель -> архитектура данных -> код
. Здесь порядок менять ни в коем случае нельзя! При проектировании программы нужно всегда начинать с выяснения цели, которой нужно достичь, а затем хотя бы приблизительно представить архитектуру данных: структуры и инфраструктуру данных, необходимые для её эффективного достижения. И только после этого нужно писать код для работы с такой архитектурой. Если со временем цель меняется, то нужно сначала изменить архитектуру данных, а потом код.По моему опыту, самая серьёзная проблема ООП заключается в том, что оно мотивирует игнорировать архитектуру модели данных и применять бестолковый паттерн сохранения всего в объекты, обещающие некие расплывчатые преимущества. Если это подходит для класса, то это отправляется в класс. У меня есть
Customer
? Он отправляется в class Customer
. У меня есть контекст рендеринга? Он отправляется class RenderingContext
.Вместо построения хорошей архитектуры данных внимание разработчика смещено в сторону изобретения «хороших» классов, взаимосвязей между ними, таксономий, иерархий наследования и так далее. Это не просто бесполезное занятие. В глубине своей оно очень вредно.
Мотивирование к сложности
При проектировании архитектуры данных в явном виде результатом обычно является минимальный необходимый набор структур данных, обслуживающих цель нашего ПО. Если мыслить в категориях абстрактных классов и объектов, то грандиозность и сложность абстракций сверху ничем не ограничивается. Просто взгляните на FizzBuzz Enterprise Edition — такую простую задачу можно реализовать в столь большом количестве строк кода лишь потму, что в ООП всегда есть место для новых абстракций.
Защитники ООП скажут, что проверка абстракций — вопрос уровня навыка разработчика. Возможно. Но на практике ООП-программы всегда разрастаются и никогда не уменьшаются, потому что ООП стимулирует к этому.
Повсюду графы
Так как ООП требует разбрасывания информации по множеству мелких инкапсулированных объектов, количество ссылок на эти объекты тоже растёт взрывными темпами. ООП требует передавать повсюду длинные списки аргументов или непосредственно хранить ссылки на связанные объекты для быстрого доступа к ним.
У вашего
class Customer
есть ссылка на class Order
, и наоборот. class OrderManager
содержит ссылки на все Order
, а потому косвенно и на Customer
. Всё стремится ссылаться на всё остальное, потому что постепенно в коде появляется всё больше мест, ссылающихся на связанный объект.Вам нужен был банан, но вы получили гориллу, держащую банан, и целые джунгли.
ООП-проекты обычно выглядят не как качественно спроектированные хранилища данных, а как огромные спагетти-графы объектов, указывающих друг на друга, и методы, получающие огромные списки аргументов. Когда вы начинаете проектировать объекты
Context
просто для того, чтобы урезать количество передаваемых туда-сюда аргументов, то понимаете, что пишете настоящий ООП-код уровня Enterprise.Задачи поперечных срезов
Подавляющее большинство существенного кода не работает всего с одним объектом, а на самом деле реализует задачи поперечных срезов. Пример: когда
class Player
ударяет при помощи метода hits()
class Monster
, где на самом деле нужно изменять данные? Величина hp
объекта Monster
должна уменьшиться на attackPower
объекта Player
; величина xp
объекта Player
должна увеличиться на уровень Monster
в случае убийства Monster
. Должно ли это происходить в Player.hits(Monster m)
или в Monster.isHitBy(Player p)
? Что если здесь нужно учитывать и class Weapon
? Мы передаём аргумент в isHitBy
или у Player
есть геттер currentWeapon()
?Этот упрощённый пример со всего тремя взаимодействующими классами уже становится типичным кошмаром ООП. Простое преобразование данных превращается в кучу неуклюжих взаимопереплетённых методов, вызывающих друг друга, и причина этого только в догме ООП — инкапсуляции. Если добавить в эту смесь немного наследования, то мы получим хороший пример того, как выглядит стереотипное ПО уровня Enterprise.
Шизофреническая инкапсуляция объектов
Давайте взглянем на определение инкапсуляции:
Инкапсуляция — концепция ООП, связывающая данные и функции для манипулирования этими данными, позволяющая защитить их от внешнего вмешательства и неверного использования. Инкапсуляция данных привела к важной для ООП концепции сокрытия данных.
Намерение благое, но на практике инкапсуляция при дробности объекта или класса часто приводит к тому, что код пытается отделить всё от всего остального (от самого себя). Это создаёт огромный объём бойлерплейта: геттеры, сеттеры, многочисленные конструкторы, странные методы, и все они пытаются защитить нас от ошибок, возникновение которых слишком маловероятно в таких скромных масштабах. Можно использовать такую метафору: я нацепляю на левый карман висячий замок, чтобы правая рука не могла ничего из него взять.
Не поймите меня неверно — наложение ограничений, особенно в случае ADT, обычно является хорошей идеей. Но в ООП со всеми этими перекрёстными ссылками объектов инкапсуляция часто не достигает ничего полезного, а учитывать ограничения, разбросанные по множеству классов, довольно сложно.
По моему мнению, классы и объекты слишком дробные, и с точки зрения изоляции, API и т.д. лучше работать в пределах «модулей»/«компонентов»/«библиотек». И по моему опыту, именно в кодовых базы ООП (Java/Scala) модули/библиотеки не используются. Разработчики сосредоточены на том, чтобы соорудить ограждения вокруг каждого класса, не особо задумываясь над тем, какие группы классов в совокупности формируют отдельную, многократно используемую, целостную логическую единицу.
На одинаковые данные можно смотреть по-разному
ООП требует упорядочивать данные негибким образом: разделять их на множество логических объектов, что определяет архитектуру данных — граф объектов с относящимся к ним поведением (методами). Однако часто полезно бывает иметь разные возможности логического выражения манипуляций с данными.
Если данные программы, например, хранятся в табличном, ориентированном на обработку данных виде, то можно создать два или более модулей, каждый из которых работает с той же структурой данных, но различным образом. Если данные разбиты на объекты с методами, то это больше невозможно.
Это ещё и является основной причиной объектно-реляционного разрыва. Хоть реляционная структура данных и не всегда бывает наилучшей, она обычно достаточно гибка, чтобы с ней можно было работать различными способами, пользуясь разными парадигмами. Однако жёсткость организации данных в ООП вызывает несовместимость с любой другой архитектурой данных.
Низкая производительность
Сочетание разброса данных по множеству мелких объектов, активное использование косвенности и указателей, отсутствие правильной архитектуры данных приводят к низкой скорости выполнения. Такого обоснования более чем достаточно.
Какой же подход использовать вместо ООП?
Я не думаю, что существует «серебряная пуля», поэтому просто опишу то, как это обычно сегодня работает в моём коде.
Первым делом я изучаю данные. Анализирую, что поступает на вход и на выходы, формат данных, их объём. Разбираюсь, как данные должны храниться во время выполнения и как они сохраняются: какие операции должны поддерживаться и с какой скоростью (скорость обработки, задержки) и т.д.
Обычно если данные имеют значительный объём, моя структура близка к базе данных. То есть у меня будет некий объект, например
DataStore
с API, обеспечивающим доступ ко всем необходимым операциям для выполнения запросов и сохранения данных. Сами данные будут содержаться в виде структур ADT/PoD, а любые ссылки между записями данных будут представлены в виде ID (число, uuid или детерминированный хеш). По внутреннему устройству это обычно сильно напоминает или на самом деле имеет поддержку реляционной базы данных: Vec
торы или HashMap
хранят основной объём данных по Index или ID, другие структуры используются как «индексы», необходимые для выполнения быстрого поиска, и так далее. Здесь же располагаются и другие структуры данных, например кеши LRU и тому подобное.Основная часть логики программы получает ссылку на такие
DataStore
и выполняет с ними необходимые операции. Ради параллелизма и многопоточности я обычно соединяю разные логические компоненты через передачу сообщений наподобие акторов. Пример актора: считыватель stdin, обработчик входящих данных, trust manager, состояние игры и т.д. Такие «акторы» можно реализовать как пулы подпроцессов, элементы конвейеров и т.п. При необходимости у них могут может быть собственный или общий с другими «акторами» DataStore
.Такая архитектура даёт мне удобные точки тестирования:
DataStore
могут с помощью полиморфизма иметь различные реализации, а обменивающиеся сообщениями экземпляры акторов могут создаваться по отдельности и управляться через тестовые последовательности сообщений.Основная идея такова: только потому, что моё ПО работает в области, где есть концепции, например, клиентов и заказов, в нём не обязательно будет класс
Customer
и связанные с ним методы. Всё наоборот: концепция Customer
— это всего лишь набор данных в табличной форме в одном или нескольких DataStore
, а код «бизнес-логики» непосредственно манипулирует этими данными.Дополнительное чтение
Как и многое в проектировании программного обеспечения, критика ООП — непростая тема. Возможно, мне не удалось чётко донести свою точку зрения и/или убедить вас. Но если вы заинтересовались, то вот ещё несколько ссылок:
- Два видео Брайана Уилла, в которых он приводит отличные доводы против использования ООП: Object-Oriented Programming is Bad и Object-Oriented Programming is Garbage: 3800 SLOC example
- Доклад Стояна Николова с CppCon 2018: «OOP Is Dead, Long Live Data-oriented Design», в котором автор проводит прекрасный анализ примера кодовой базы ООП и указывает на её проблемы.
- Аргументы против ООП на wiki.c2.com — список стандартных аргументов против ООП.
- Статья Лоуренса Крубнера «Объектно-ориентированное программирование — это дорогостоящая катастрофа, которую нужно прекратить» — длинный пост, глубоко рассматривающий многие идеи.
- Quora: ООП в C++ медленнее, чем C? Если да, то значительна ли разница?