Хорошо, давайте немного поговорим о слоне в комнате. ООП было популяризировано в начале 90-х, и тогда это был святой Грааль для разработчиков. Но сейчас, когда проекты становятся все больше и больше, это становится скорее препятствием, чем решением общих проблем. Я знаю, ничто не является серебряной пулей, но с появлением функционального программирования (которое по иронии судьбы продвигает многие передовые методы ООП) нам все еще нужно использовать объектно-ориентированное программирование в качестве предпочтительной парадигмы?
Но сначала давайте поговорим о том, почему ООП по-прежнему так популярен спустя столько лет.
Объектно-ориентированное программирование было популяризировано Java, первым широко распространенным объектно-ориентированным языком. Почему Java стала такой популярной? Я думаю, что это было сочетание нескольких аспектов. Во-первых, его было легко читать, потому что синтаксис был проще по сравнению с другими языками и похож на английский язык (например, peter.love(angela)). Во-вторых, программа запускалась на виртуальных машинах, поэтому разработчику не нужно было думать о таких вещах, как ОС или архитектура процессора. В-третьих, он не использовал файлы заголовков, использование и обслуживание которых доставляло разработчикам огромную головную боль. Java со своими особенностями произвел революцию в ИТ-индустрии, снизив начальный уровень и сделав программирование более приятным для всех. Однако в то время большинство проектов были значительно меньше и менее сложными, чем сейчас, и со временем подход ООП становился все сложнее и сложнее в обслуживании. Вы спросите: «А что в этом плохого?». Я разобью ответ на ключевые аспекты.
Инкапсуляция и зависимости
Одно из правил ООП заключается в том, что объекты должны вести себя как реальные объекты, т.е. автомат для закусок (проект) содержит множество подэлементов (объектов), таких как слот для монет, который позволяет пользователю подбрасывать монету (функция ввода) и передает информацию об этом на клавиатуру (асинхронный выходной сигнал). Однако большинство объектов не похожи на объекты реальной жизни, они более сложны и имеют много зависимостей, которые сводят на нет их аспект инкапсуляции. Много раз в прошлом, когда я работал над новой функциональностью, у меня возникала идея повторно использовать часть моего кода из другого проекта, над которым я работал несколько лет назад. С точки зрения ООП, это отличная идея просто взять один объект из другого проекта и просто использовать его в текущем, потому что в соответствии с объектно-ориентированным программированием объекты должны быть автономными, инкапсулированными частями, которые я могу повторно использовать где угодно. Я хочу. Итак, я открываю предыдущий проект и копирую его, но, о нет… этот объект связан с двумя другими объектами, поэтому я также копирую эти два объекта. Однако один из этих объектов использует пользовательский тип, который был создан где-то еще в этом устаревшем проекте. Эта ситуация может продолжаться очень долго, и из-за этой проблемы с зависимостями вы можете быть вынуждены копировать значительное количество объектов или объем кода, который вам на самом деле не нужен. В таких случаях я просто говорил «похуй» и писал новый аналогичный функционал, который лучше подходил для нового проекта. Однако количество времени, которое я потратил на повторное использование теоретически «повторно используемого» кода, было потеряно навсегда и не придало никакой ценности текущему проекту. Когда проекты разрастаются, все более вероятно, что они превращаются в монолитную кашу, когда даже потенциально простые объекты запутываются в сети зависимостей, что делает их совершенно непригодными для использования вне проекта. Джо Армстронг описал эту проблему следующим образом:
Проблема с объектно-ориентированными языками в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но получили гориллу, держащую банан и все джунгли.
Джо Армстронг
Иерархии отношений
Это аналогичная проблема с предыдущей. Когда проект растет, иерархии отношений становятся больше и сложнее. Это приводит к таким ситуациям:

Рассмотрим эту ситуацию. Объекту J нужны некоторые данные, сгенерированные объектом G, но оба находятся на разных «ветках». Что делать разработчику в этой ситуации? Существует несколько различных решений этой проблемы. Лучшим решением с точки зрения ООП является передача запроса через объект I и C в ближайший общий узел (в данном случае — объект A), а затем этот «хозяин» объект пошлет этот запрос объекту G через объект B. Результат этого запроса также нужно пройти весь этот путь от объекта G до J. Как видите, это кошмар. Чтобы упростить этот беспорядок, вы можете сделать две вещи. Вы можете быть на 100% верны парадигме ООП и попросить своего менеджера дать дополнительное время на рефакторинг этого функционала и посмотреть, насколько красным станет его лицо. Как вариант, вы можете просто сделать простой кросс-срез и соединить эти два объекта напрямую. Это, однако, может привести ко многим проблемам в долгосрочной перспективе. Одним из наиболее очевидных является то, что создание сквозных связей делает код более запутанным, трудным для чтения и поддержки. Однако более опасная проблема связана с одной из основных характеристик объекта. Объекты содержат две группы элементов, внутренние состояния (переменные) и функции. Когда два объекта хотят использовать данные из зависимого объекта (в данном случае непосредственно связанного объекта B и объекта J, который связан сквозным доступом), этот объект может начать давать неожиданные результаты. Чтобы предотвратить это, разработчик должен потратить дополнительное время на создание некоторых функций внутри этого объекта, чтобы защитить его от создания недопустимых/неожиданных/ошибочных результатов.
Менеджеры, службы, фабрики, помощники и т. д.
Еще одна проблема с большими проектами — растущее число объектов, называемых менеджерами, сервисами, помощниками и т. д. Они создаются для инкапсуляции некоторых функций, которые будут использоваться в нескольких разных объектах. Это хорошая идея, если они хранятся как частные сущности внутри этих объектов, потому что это предотвращает проблему общего состояния (описанную в предыдущем абзаце). Однако в большинстве случаев они хранятся в течение всей жизни «главного» объекта, что увеличивает использование памяти, даже если «главный» объект их не использует. Более серьезная проблема заключается в том, что у менеджеров есть общий экземпляр, который является просто причудливым словом для синглтона. Такой подход является большой ошибкой и не должен использоваться нигде. Да, он решает проблемы, связанные с разветвленными иерархиями, но в то же время очень легко достичь сквозности, что приводит к проблемам с общим состоянием. Это также побуждает разработчиков злоупотреблять им, что создает сильно связанные «главные» объекты с менеджером и делает его полностью непереносимым (аналогично проблеме, описанной в параграфе «инкапсуляция и зависимости»).
Уязвимость базовых/абстрактных классов
Другая очень распространенная проблема с ООП — чрезмерное использование базовых и/или абстрактных классов. Наследование и абстрактные типы являются краеугольными камнями объектно-ориентированного программирования, но они могут быть скорее вредными, чем полезными, особенно в больших и сложных проектах. Как «базовые», так и абстрактные классы содержат методы, которые можно использовать и переопределять в дочерних классах. Однако любое изменение в суперклассе может повлиять на любой из его потомков. Рассмотрим этот пример:
Как видите, класс BaseSender может «отправить» одно сообщение или несколько сообщений. MySender — это расширенная версия BaseSender, которая сохраняет сообщения после их отправки. Все работает как задумано. Однако через некоторое время кто-то решает провести рефакторинг класса BaseSender, чтобы сделать его более СУХИМ. Вот результат:
Это небольшое изменение создает большую проблему. Теперь send(messages:) вызывает send(message:), поэтому MySender вызывает save(message:) n раз каждый раз, когда вызывается send(messages:).
Этот простой пример показывает, что даже небольшое изменение в суперклассе может иметь большое влияние на их потомков. Поэтому каждый раз, когда что-то изменяется, разработчик должен проверять каждый дочерний класс на наличие возможных проблем, что увеличивает время разработки одной задачи/тикета.
Передача ссылок (изменчивость)
Передача ссылки на объект — потенциально опасное действие, особенно в высоко асинхронных функциях. Как вы знаете, когда вы хотите передать объект через метод, вы не создаете новый объект, а передаете только ссылку на этот объект. Опять же, существует проблема с совместным использованием объекта двумя объектами или с использованием его из нескольких мест одновременно, что может привести к нестабильному или неожиданному поведению этого объекта. Некоторые разработчики пытаются сделать копию этого объекта, но очень часто это может быть проблемой. Не каждый объект легко скопировать, потому что многие из них хранят внутренние состояния или в данный момент делают что-то асинхронно. В таких случаях разработчик должен выяснить, как создать копию всех тех состояний и задач, которые в настоящее время работают внутри.
Хорошо, теперь вы можете видеть, что ООП в настоящее время имеет много серьезных проблем, которые напрямую связаны с его столпами:
ПИРОГ 🥧 (полиморфизм, наследование, инкапсуляция) — как его есть?
Полиморфизм, наследование и инкапсуляция. Три базовых элемента ООП, которые еще недавно были основой программирования. Однако в настоящее время они лишь тени самих себя. Позвольте мне показать вам, что такие замечательные идеи теперь являются не чем иным, как тремя марионетками объектно-ориентированного программирования.

Полиморфизм
Полиморфизм был отличной идеей. Дочерний класс, который может переопределять методы суперкласса и модифицировать его в соответствии с ними, был тогда большой инновацией. Но сейчас это все равно, что построить пушку, чтобы стрелять в комара. Вы можете сделать то же самое, используя протоколы, расширения или интерфейсы (в зависимости от языка программирования), чтобы вообще не использовать этот элемент ООП.
Наследование
Наследование — самый яркий элемент ООП. Однако, как вы уже знаете, некоторые из описанных выше проблем напрямую связаны с наследованием. Уязвимость абстрактных классов делает их устойчивыми к изменениям, а многоуровневое наследование, затрудняющее чтение и работу с кодом, является признаком чрезмерной инженерии и плохих архитектурных решений. Конечно, разработчики могут ограничивать себя и пытаться создавать простые объекты, которые наследуются только от «базового» или абстрактного класса, что улучшает читаемость кода, но это не решает проблему, описанную в параграфе «Уязвимость базовых/абстрактных классов». . Кроме того, почему разработчики должны ограничивать себя, если ООП позволяет им это делать? Разве это не противоречит всей идее? С моей точки зрения, как и в случае с полиморфизмом, они должны максимально избегать использования этой черты ООП. Такие инструменты, как расширения, пары протокол-расширение или небольшие функциональные объекты, создаваемые и используемые только при необходимости, могут полностью заменить наследование.
Инкапсуляция
Когда я впервые прочитал об инкапсуляции, я был поражен. Маленькие объекты с простыми, едиными закрытыми функциями были блестящей идеей. Но потом я стал программистом и начал работать с другими разработчиками программного обеспечения. Через пару недель я обнаружил, что теория инкапсуляции — всего лишь миф. Для создания сложного функционала приходится создавать чертовски много мелких объектов, что приводит к множеству проблем с сопровождением всего проекта. Эти объекты начинают зависеть друг от друга, создавая запутанную сеть ссылок и настраиваемых типов ввода/вывода. Этот подход также повышает уровень сложности и удобочитаемости, что затрудняет сотрудничество с другими разработчиками, особенно когда они только начинают работать над проектом. Вторая проблема заключается в том, что передача ссылки на объект может нарушить их инкапсуляцию (проблема с общим состоянием, которую я уже рассмотрел парой абзацев выше).
Заключение — что делать?
Суть проблемы с ООП в том, что оно было определено довольно давно. С тех пор ИТ-проекты стали значительно больше и сложнее. Разработчики, чтобы сохранить актуальность этой парадигмы, начали создавать хорошие практики для изменения и улучшения ООП. Результатом их усилий стали такие вещи, как TDD, BDD, UML-диаграммы, OOD, внедрение зависимостей, четкая архитектура и многое-многое другое, что теоретически должно облегчить работу над сложными и сильно объектно-ориентированными проектами. Каждый раз, когда я слышу о новой идее, которая должна облегчить использование ООП, я просто улыбаюсь, потому что это ситуация, похожая на забавные картинки про коммунизм/социализм, которые вы можете увидеть в Интернете.

Итак, как можно решить проблемы, связанные с ООП? Я думаю, что нам нужно сделать революцию, а не эволюцию. Мы не можем бесконечно пытаться исправить эту сломанную парадигму. Вместо этого мы должны взглянуть на эти проблемы с другой точки зрения. Делайте все проще и понятнее. Перестаньте мыслить глобально, не пытайтесь все предугадать и не проектируйте всю архитектуру. Вы можете спросить «почему?». Проекты — это живые существа. Во время разработки может быть так много вещей, которые могут все изменить, поэтому просто расточительно думать на несколько шагов вперед. Кроме того, попытки сделать части вашего кода универсальными с помощью абстракции или сложной структуры могут зайти в тупик, потому что вы можете больше никогда не использовать эти «блестящие» идеи. Итак, просто выбросьте свои UML-диаграммы и замысловатые идеи, которые включают в себя многоуровневые, сложные связи между объектами, раздутые зависимости и т. д., и постарайтесь сделать свой код простым и читаемым, думая в DRY (не повторяйтесь) и KISS (продолжайте это просто, глупо). Время от времени старайтесь реализовать что-то функционально и, самое главное, много общайтесь с другими разработчиками из-за пределов вашей среды и старайтесь понять их точку зрения.