- Чем быстрее вы забудете ООП, тем лучше для вас и ваших программ
- Данные важнее, чем код
- Мотивирование к сложности
- Повсюду графы
- Задачи поперечных срезов
- Шизофреническая инкапсуляция объектов
- На одинаковые данные можно смотреть по-разному
- Низкая производительность
- Какой же подход использовать вместо ООП?
- Дополнительное чтение
- В защиту ООП. 7 несостоятельных аргументов его противников
- 1. Всё, что есть в ООП, уже давно есть в других парадигмах
- 2. ООП смешивает данные и действия над ними. Это плохо
- 3. Наследование закрепощает программу, делает трудным внесение изменений
- 4. Инкапсуляция не имеет смысла
- 5. В реальном мире нет иерархий отношения, повсюду лишь иерархии включения
- 6. Методология ООП изначально ошибочна
- 7. Но даже миллионы мух не убедят нас, что навоз — это вкусно
- Почему эта статья — в защиту ООП?
Чем быстрее вы забудете ООП, тем лучше для вас и ваших программ
Объектно-ориентированное программирование — чрезвычайно плохая идея, которая могла возникнуть только в Калифорнии.
— Эдсгер Вибе Дейкстра
Возможно, это только мои ощущения, но объектно-ориентированное программирование кажется стандартной, самой распространённой парадигмой проектирования ПО. Именно его обычно преподают студентам, объясняют в онлайн-туториалах и, по какой-то причине, спонтанно применяют даже тогда, когда не собирались этого делать.
Я знаю, насколько она привлекательна, и какой замечательной кажется эта идея на поверхности. На разрушение её чар у меня ушли многие годы, и теперь я понимаю, насколько она ужасна, и почему. Благодаря этой точке зрения у меня есть чёткая уверенность в том, что люди должны осознать ошибочность ООП и знать решения, которые можно использовать вместо него.
Многие люди и раньше обсуждали проблемы ООП, и в конце этого поста я приведу список своих любимых статей и видео. Но прежде я хочу поделиться собственным взглядом.
Данные важнее, чем код
По своей сути всё ПО предназначено для манипуляций данными с целью достижения определённого результата. Результат определяет способ структурирования данных, а структура данных определяет необходимый код.
Этот момент очень важен, так что я повторюсь: цель -> архитектура данных -> код . Здесь порядок менять ни в коем случае нельзя! При проектировании программы нужно всегда начинать с выяснения цели, которой нужно достичь, а затем хотя бы приблизительно представить архитектуру данных: структуры и инфраструктуру данных, необходимые для её эффективного достижения. И только после этого нужно писать код для работы с такой архитектурой. Если со временем цель меняется, то нужно сначала изменить архитектуру данных, а потом код.
По моему опыту, самая серьёзная проблема ООП заключается в том, что оно мотивирует игнорировать архитектуру модели данных и применять бестолковый паттерн сохранения всего в объекты, обещающие некие расплывчатые преимущества. Если это подходит для класса, то это отправляется в класс. У меня есть 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 , а код «бизнес-логики» непосредственно манипулирует этими данными.
Дополнительное чтение
Как и многое в проектировании программного обеспечения, критика ООП — непростая тема. Возможно, мне не удалось чётко донести свою точку зрения и/или убедить вас. Но если вы заинтересовались, то вот ещё несколько ссылок:
Источник
В защиту ООП. 7 несостоятельных аргументов его противников
Когда я, так сказать, прошёлся по Интернету, я заметил одну интересную особенность. Все парадигмы программирования, где-либо обсуждаемые, воспринимаются людьми совершенно спокойно. Если, например, говорят про процедурное программирование, то говорят про него абсолютно спокойно. То же самое — про модульное программирование. Декларативное программирование — никаких бурь, волнений или холиваров. Функциональное программирование — то же самое.
И только вокруг ООП не утихают бури. Одни визжат от него в восторге, другие, наоборот, хаят на чём свет зиждется. И мне, честно сказать, совершенно непонятно, почему на ООП весь мир клином сошёлся.
Возможно, вы только что подумали, что я — скорее противник, чем сторонник ООП. Это абсолютно не так (впрочем, вы это можете понять из заголовка). Нет. Я — скорее противник «серебряных пуль», хайпа, возведения какой-либо методологии или человека на престол и всяческого вождения хороводов. Вы же не водите хороводы вокруг, скажем, гаечного ключа или газонокосилки. И не пишете, я надеюсь, публикаций, почему дрель или молоток — отстой.
Но на сегодняшний день весь Интернет кишит именно напыщенными, гиперэмоциональными, радикалистскими статьями по поводу ООП — если один говорит, что ООП — «зер гут» и вообще всем гутам гут — то другой обязательно вещает, что ООП необходимо срочно выкинуть на помойку (если только он не разделяет взгляды первого). Третьего не дано.
Я же хочу именно привнести третий элемент. Спокойно, без хайпа и ругани рассказать, почему ООП — не эликсир от всех болезней, но также, как и ПП, ФП или ЛП имеет право на существование.
Итак, спокойная статья в защиту ООП. В ней я попытаюсь рассмотреть основные доводы противников ООП и обосновать их несостоятельность.
1. Всё, что есть в ООП, уже давно есть в других парадигмах
Почти все языки программирования являются тьюринг-полными, за исключением языков разметок, как то: HTML, XML, CSS и т.д. Если говорить крестьянским языком, тьюринг-полный язык — язык, на котором можно написать абсолютно любую мыслимую программу. Из этого следует довольно-таки всеобщий тезис: то, что есть в любом наудачу выбранном языке, есть во всех остальных языках. То же можно сказать и про парадигмы. Все отличия языков (и парадигм) — это разные способы реализации тех или иных команд, не считая отдельных лексических особенностей.
Кстати, этот же тезис (всё, что есть в N, есть и в M, и в K, и в R и т.д.) можно сформулировать так: молоток уже состоит из железа да дерева, зачем же нам ещё и пассатижи? Но ведь так никто не станет утверждать.
2. ООП смешивает данные и действия над ними. Это плохо
Аргумент высосан из пальца. Во-первых, где ООП смешивает данные и функции? Во-вторых, утверждение, что это плохо, тоже взято от фонаря, из бочки «настоящий мужик так не делает», а почему — да потому, что гладиолус. ООП в каком-то роде моделирует реальный мир, где данные присущи объекту — никто ведь не станет утверждать, что у стула отсутствует цвет, и у него не четыре ноги. И никаких смешений данных и операций здесь не происходит, объект — это не функция и не оператор. Это абстракция.
3. Наследование закрепощает программу, делает трудным внесение изменений
Тут можно немного притормозить. Наследование вовсе не предполагает выстраивание километровых деревьев с целью замедлять разработку. Наследование придумано для того, чтобы выделять общие свойства и методы в суперкласс, причём делать это нужно с классами, представляющими однотипные объекты. Ошибкой будет создавать, например, два класса, один из которых — наследник другого, ибо здесь нет выделения общего кода в суперкласс просто потому, что здесь нет ничего общего.
Если не предполагается расширять родительский класс третьим классом — такое наследование попросту бессмысленно. Если вы создаёте магазин спиртных напитков, то классы Beer, Vodka и Vine можно унаследовать от класса Alcohol, но совершенно не нужно создавать ещё и класс Drinks, если только вы не хотите продавать ещё и, скажем, парагвайский чай.
Также ошибкой будет создание иерархий, в которых классы никак не относятся друг к другу. Ну зачем, расскажите мне, городить башню, где классы Муха и Котлета наследуются от суперкласса Сыр, который, в свою очередь, наследуется от суперкласса Пятница?! Но это уже не недостаток ООП, а кривые руки того, кто такое сочиняет.
4. Инкапсуляция не имеет смысла
Вот тут я частично согласен. С точки зрения работы программы инкапсуляция действительно ни на что не влияет. Если я закрою переменную с помощью private — ну и что, я всё равно смогу её открыть, просто убрав private, а потом менять там всё, что душе заблагорассудится.
Но это верно лишь чисто технически. Философия ООП гласит: правильно организованный и инкапсулированный класс можно рассматривать как чёрный ящик. Представьте себе коробку, на одной стороне которой разнообразные кнопки, слоты для подачи данных, а на другой — выходной слот, который возвращает информацию. Возьмём, к примеру, стек. Представьте коробку, на одной стороне которой есть один слот для вставки данных и кнопка push рядышком. На обратной стороне — кнопка pop. Вы подаёте туда записку с числом 8 и давите кнопку push. Затем подаёте ещё бумажку и второй раз давите push. И так N раз, а затем жмёте pop. Из ящика вылетает бумажка с числом 76 (или другое, в общем, то, которое вы подали). Нужно ещё число? Второй раз давите pop. И так до морковкина заговенья тех пор, пока ящик не опустеет. А если вы продолжите давить pop, механизм из ящика завоет: стек пуст! Именно так и выглядит объект.
Но после того, как вы создали и настроили класс, вам уже фиолетово, как он там работает — он просто правильно работает, а большего и желать не нужно. А инкапсулируя все эти структуры, вы не держите всё подряд в памяти. Они (множество ящиков) просто общаются между собой так, как вы настроите.
Инкапсуляция — своеобразный костыль, поддерживающий сотню столпов вашей программы в то время, пока вы конструируете сто первый. В крупных проектах (а именно для их создания и придумали ООП) без этого, увы, никак.
Хотя, вряд ли это «увы» здесь вообще уместно.
5. В реальном мире нет иерархий отношения, повсюду лишь иерархии включения
Да разве? Но ведь никто не мешает создать, например, иерархию, где все реки мира (Конго, Сена, Темза, Амазонка, Колыма и т.д.) являются объектами одной всеобъемлющей «Реки», которой присущи свойства (например, состоит из воды) и действия (например, течёт), а уже она будет наследоваться от «Водоёма», который тоже состоит из воды, а от «Водоёма» можно унаследовать ещё и «Озеро», объектами которого будут отдельные озёра (Байкал, Каспийское море, Титикака и т.д.). Схема довольно грубая. Но иерархии отношения — это тоже абстракция. Что-то а-ля платоновской идеи, если хотите. В реальном мире их нет, они существуют только в уме, это обобщение, и не более того. Но ведь именно так человек очень часто мыслит. Мы ведь можем сказать «носок», без уточнения, каков у него цвет, из какого материала соткан и т.д., но существует ли этот «носок» в действительности?
И всё же нас не должно смущать, что нет ни «объекта», ни «носка».
6. Методология ООП изначально ошибочна
Абсолютно необоснованный аргумент. ООП создавалось для того, чтобы моделировать своеобразный виртуальный мир, состоящий из объектов, как и наш мир. Например: человек — объект из реального мира. Он может ходить, бегать, кушать, срать спать, играть в футбол, смотреть футбол, но, к сожалению, я тут не могу всё перечислить, да и, честно сказать, всё перечислять было бы противно. Этот же самый человек обладает свойствами: наличие/отсутствие волос, цвет волос, если они есть, цвет глаз, если они есть цвет кожи, количество пальцев на руках и т.д. Если правильно сконструировать все поля и методы, как я уже писал выше, то программный объект сможет моделировать те или иные свойства реального объекта. Человек очень даже хорошо мыслит в таких категориях — именно поэтому ООП и стало распространённым. Оно очень помогает при написании больших проектов, так как привносит модульность и позволяет разбивать программный пакет на отдельные компоненты, взаимодействующие друг с другом.
7. Но даже миллионы мух не убедят нас, что навоз — это вкусно
Самый популярный аргумент против ООП. Мол, массы в большинстве своём глупы (всё же я не думаю, что это относится и к программистам), бегают по «модным шмоткам» и восхищаются ими.
Но задумайтесь, а если бы на пьедестал взошло не ООП, а, скажем, ЛП? Думаете, было бы всё по-другому? Ничего подобного! Нашлись бы и фанаты, и злостные противники, а на ООП смотрели бы как на инструмент (к этому я, вообще-то, и призываю), а не как на таблетку, сотворённую самим Богом и потому незаменимую.
Почему эта статья — в защиту ООП?
Все современные разговоры про парадигмы программирования, как мне видится, сводятся к двум диаметральным посылкам: оставим ООП и выкинем всё остальное, или же выкинем ООП и… ну, вы поняли меня.
Я не хочу, чтобы вполне годную парадигму посчитали достойной свалки, но я и не хочу, чтобы вокруг неё водили хороводы, а всё остальное забыли. Я думаю, что второе сделать проще, а против первого и направлена эта статья.
Источник