ООП используется уже давно, оно применяется в большинстве программ. Но всегда ли ООП является правильным путём? Далеко нет.

Что такое ООП?

ООП — это парадигма, при которой код разделён на множество классов, что приводит к настраиваемому доступу и разъединению компонентов. Основные преимущества использования ООП заключаются в следующем:

1. Сокрытие подробностей реализации

Благодаря использованию слоёв абстракций мы можем обеспечить приватность работы внутреннего устройства ПО. Абстрагирование помогает с безопасностью и удобством использования, так как другие разработчики не знают (и не должны знать) внутреннюю реализацию вашего ПО.

2. Разъединённые компоненты

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

3. Иерархия классов

Использование наследования позволяет нам расширять поведение классов без многократного повторения кода. Это помогает с реализацией принципа DRY (Don»t repeat yourself, «не повторяйся»). Наличие иерархии классов также добавляет полиморфизм, что позволяет работать с подклассами как с базовыми классами, и наоборот.


Как видите, ООП обеспечивает множество преимуществ, но использовать его не всегда правильно. С ним связано много недостатков.

Чем плохо ООП?

Во‑первых, нужно сделать небольшое пояснение: ООП не плохо само по себе, проблема в том, как мы его используем. ООП имеет право на существование. Проблема в том, что его использование чрезмерно. На самом деле, многие программисты даже не задумываются о другой парадигме при написании ПО. ООП стало незыблемым стандартом.

С учётом всего этого давайте перечислим его основные недостатки.

ООП непредсказуемо

Иерархии классов часто приводят к многократному переопределению методов. Само по себе это не проблема, но быстро может ею стать. Посмотрите на следующую диаграмму классов:

Реализация ООП, встречающаяся в некоторых игровых движках
Реализация ООП, встречающаяся в некоторых игровых движках

Можете ли вы найти потенциальную проблему? Возможно, пока нет. Посмотрите на ещё один пример:

Более сложная реализация ООП
Более сложная реализация ООП

Уже есть какие‑то соображения? Может быть, это неочевидно, но не написав ни одной строки кода, мы уже создали непредсказуемый код.

Почему? Каждый класс содержит метод update. Это базовая концепция ООП. Она позволяет переопределять методы из базового класса. Это полезный способ добавления или изменения поведения. Но в то же время он создаёт угрозу. Проблема называется VTable. VTable — это список всех виртуальных методов, которые содержит класс. Он отвечает за поиск нужного метода в среде исполнения.

VTable для показанного выше примера будут выглядеть примерно так:

VTable предыдущего примера
VTable предыдущего примера

VTable также являются причиной того, что мы можем преобразовать тип в его базовый тип, или наоборот, и вызывать базовый метод/метод подтипа. Когда мы ссылаемся на тип как на другой базовый/подтип, мы также ссылаемся на VTable, которая имеет собственную запись для каждого метода.

Рассмотрим следующий код, написанный на Swift:

Очень простой пример полиморфизма
Очень простой пример полиморфизма

Легко понять, что у нас есть список из A, но благодаря принципу полиморфизма мы можем хранить в нём и B. VTable проверяется для динамической диспетчеризации нужного метода в среде исполнения.

Но в этом и есть первая потенциальная опасность. Мы не знаем реального типа каждого элемента в списке, пока явным образом не проверим его. Обычно мы делаем это намеренно, чтобы хранить элементы общего типа в списке. В большинстве случаев это нормально, но открывает возможности для возникновения настоящей проблем. Вскоре мы к этому вернёмся.

А пока давайте рассмотрим ещё один пример:

Более сложный пример полиморфизма
Более сложный пример полиморфизма

Разумеется, это довольно глупый пример, однако он демонстрирует серьёзную угрозу:

То, должен ли вызываться метод базового класса, зависит от реализации подкласса.

Это может привести к большим проблемам. Мы не можем делать здесь допущений; нам придётся проверять каждый подкласс, чтобы узнать, вызывает ли он базовый метод. Разумеется, в реальных проектах существуют документация и стандарты компании, которые разрешают или запрещают это. И хотя такие предосторожности помогают, проблема остаётся. В огромных иерархиях классов становится непредсказуемым, что и когда вызывается.

ООП медленное

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

Но почему ООП медленнее? Выше мы говорили о динамической диспетчеризации. Это одна из трёх основных причин медленности ООП. Чтобы исполнить нужный метод, сначала необходимо проверить VTable. И только после этого метод исполняется. Это означает, что приходится делать как минимум один дополнительный вызов на каждый объект.

Есть и ещё один недостаток: ссылки. Классы — это ссылочные типы, а ссылки необходимо развёртывать/разыменовывать. Это значит, что нам понадобится ещё один вызов. Если мы хотим вызвать, допустим, E.draw() из нашего примера, то в результате выполним шесть вызовов! Почему?

  1. Сначала мы разыменуем ссылку.

  2. Затем обращаемся к VTable для динамической диспетчеризации E.draw().

  3. E.draw() вызывает super.draw().

  4. Это означает, что мы также вызываем C.draw().

  5. C.draw() также вызывает super.draw().

  6. Это означает, что мы также вызываем A.draw().

Довольно приличная лишняя трата ресурсов. Даже если бы мы не вызывали super.draw() для каждого экземпляра, нам всё равно нужно каждый раз выполнять динамическую диспетчеризацию, впустую тратя время исполнения.

Но это ещё не всё. Помните о памяти стека и кучи? ООП здесь тоже плохо себя проявляет. Наши объекты в основном хранятся в куче. Доступ к куче выполняется произвольным образом, и по своей природе она более медленная, чем память стека. По этому пункту я не буду вдаваться в подробности, однако в целом большинство языков чаще всего хранит ссылочные типы в куче.

Подведём итог:

  1. Нам нужно каждый раз разыменовывать ссылочные типы.

  2. Методы каждый раз динамически диспетчеризируются через VTable.

  3. Доступ к ссылочным типам обычно выполняется медленнее.

ООП мотивирует писать спагетти-код

Хотя ООП помогает объединять модули и разделять логику, оно также создаёт собственные проблемы. Часто у нас получается огромная цепочка наследования и ссылок. Когда что‑то одно нужно изменить, десятки других элементов ломаются. Эта проблема возникает из самой природы ООП. ООП спроектировано так, чтобы определять то, что выполняет доступ к нашим данным. Это значит, что чаще всего мы волнуемся о разъединении, сохранении принципа DRY, абстракции и так далее. Из‑за этого в результате возникает множество слоёв и ссылок просто для того, чтобы не нарушить принципы ООП, например, для управления доступом.

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

Это хорошо, потому что мы можем менять внутреннее устройство, не позволяя никому узнавать об этом. Но в то же время это портит ситуацию, потому что внешний мир ожидает, что от таких методов будет поступать очень конкретное множество данных.

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

Куда двигаться дальше?

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

Разработчикам необходимо переосмысливать свой выбор, прежде чем каждый раз слепо выбирать ООП. Не каждая часть вашего ПО обязана быть отделена и идеально абстрагирована. Иногда важны структура и способ обработки данных. В случае, когда критически важна производительность, ООП становится плохим выбором; в таких ситуациях больше подходят подходы наподобие Data‑Oriented‑Design (DOD).

Дополнительные ресурсы: ссылка

Комментарии (20)


  1. panzerfaust
    31.05.2023 13:11
    +15

    многие программисты даже не задумываются о другой парадигме при написании ПО. ООП стало незыблемым стандартом.

    На мой взгляд все ровно наоборот. Многие разрабы на JVM думаю, что пишут на ООП. При этом у них анемичная модель данных, Spring заставляет их работать в процедурном стиле, а в коде сплошь и рядом функторы и монады из ФП.

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


    1. Skyminok
      31.05.2023 13:11

      А изрядная часть сервисов от Spring - это вообще AOP...


    1. nronnie
      31.05.2023 13:11
      +2

      в коде сплошь и рядом функторы и монады из ФП.

      Хорошо, хоть, если так. Потому что я всю дорогу наблюдаю как на интервью у всех сплошной SOLID, а потом смотришь код, а там как будто транспиляция из Алгола-68 :)


  1. HemulGM
    31.05.2023 13:11
    +6

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


  1. aktuba
    31.05.2023 13:11
    +19

    Чем плохо ООП (иногда)

    ООП не плохо само по себе, проблема в том, как мы его используем.

    - ООП плох!
    - чем и почему?
    - разработчики хрень пишут!
    - ок, а чем ООП-то плох?
    - я выше ответил - разработчики хрень пишут!
    - т.е. разработчики плохие?
    - не, они охрененные!


    Серьезно?!

    Почему? Каждый класс содержит метод update. Это базовая концепция ООП. Она позволяет переопределять методы из базового класса.

    "Позволяет"! Автомобиль "позволяет" ездить по встречке. Вопрос - автомобиль "плохо" или водитель-мудак?!

    Но почему ООП медленнее? Выше мы говорили о динамической диспетчеризации. Это одна из трёх основных причин медленности ООП. Чтобы исполнить нужный метод, сначала необходимо проверить VTable. И только после этого метод исполняется. Это означает, что приходится делать как минимум один дополнительный вызов на каждый объект.

    о_О А как автор (не переводчик) притянул реализацию к методологии? Ок, попробуем подыграть: любая методология медленнее нативной реализации (чур не подсказывать!). Всё, остается только машкоды юзать?

    Часто у нас получается огромная цепочка наследования и ссылок. Когда что-то одно нужно изменить, десятки других элементов ломаются.

    Ммм... В самом начале моего коммента об этом, но... Ладно, не буду добивать.

    ООП — это отличный паттерн.

    ООП - это НЕ паттерн. Он не основывается на файловой системе, наличии/отсутствии классов, реализации конкретных элементов. Это методология, т.е. прям как в описании на вики.

    ООП демотивирует нас раскрывать свойства классов внешнему миру, кроме случаев, когда это абсолютно необходимо.

    Не классов, объектов.

    Поэтому мы должны писать публичные методы/обёртки, отвечающие за операции с данными.

    Нет. Перекрутили через мясорубку и фарш, и мух, и г..но.

    Какой-же бред...


    1. aktuba
      31.05.2023 13:11

      минус коммент, минус карма. не автор? конечно нет)


    1. SadOcean
      31.05.2023 13:11

      Справедливости ради, насчет медленности можно переформулировать так:
      - реализации инструментов ООП в основных ЯП могут плохо сказываться на производительности по сравнению с другими реализациями (например императивной)


  1. Siddthartha
    31.05.2023 13:11

    "это плохая игра, но это единственная игра в городе.." )

    только ООП -- не паттерн, а парадигма. для поддержки и развития сложной логики -- лучше парадигмы пока нет. DDD для бизнес-логики, DOD для оптимизации игр и прочее -- это, вроде бы, все же конкретные методологии и, вполне себе, часть парадигмы ООП.


  1. vmorsk
    31.05.2023 13:11
    -2

    Хорошо написано. ООП это просто методология со своими плюсами и минусами. Но, к сожалению, подходы зачастую воспринимают как аксиомы - от недостатка понимания сферы.

    В копилку к чувакам, округляющим глаза на "в GET может быть тело" и "не бывает api кроме rest" ????


    1. onegreyonewhite
      31.05.2023 13:11
      +1

      Насчёт чуваков, которые округляют глаза на "в GET может быть тело". Я думаю они справедливо их округляют, если они читали RFC7231 или RFC2616:

      A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.

      The GET method means retrieve whatever information ([...]) is identified by the Request-URI.

      A server SHOULD read and forward a message-body on any request; if the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request.

      Там как бы в обоих подразумевается, что GET-запрос не может и не должен иметь тело. Это вообще может оказаться неожиданностью для клиента и сервера. А тех, кто так делает надо сжигать на костре прикладывать головой об распечатанный RFC вразумлять и исправлять.

      А насчёт "не бывает api кроме rest", я надеюсь вы хотели сказать наоборот, что бывает, но есть те, кто не понимает расшифровки этой аббревиатуры? Ведь так?


      1. vmorsk
        31.05.2023 13:11

        Физически может? Может. Что делать с теми, кто применяет - это вопрос отдельный (в основном судить по статье УК РФ), а дыры в безопасности надо уметь закрывать, и знать о таких вещах для этого.

        У вас с чувством юмора очень плохо - да, действительно, очень много "программистов" в принципе не понимают что rest это лишь методология, и считают что вообще не бывает никакого api кроме rest (для человека api становится синонимом rest api, даже rest) ????


        1. mayorovp
          31.05.2023 13:11
          +1

          Если тело в запросе GET открывает дыру в безопасности - это одно, и такую дыру надо закрывать, тут обсуждать нечего.

          Но это не означает, что пишущий веб-сервер должен всерьёз поддерживать обработку GET запросов с телом.


  1. igornem
    31.05.2023 13:11
    +1

    Чтобы не создавались вышеописаные проблемы, кроме производительности, уже давно существуют принципы SOLID, нарушая которые все вышеприведенные примеры привели к приведенным в примерах проблемам. В случае необходимости достичь максимальной производительности соглашусь, OOP не самый лучший вариант.


  1. iburanguloff
    31.05.2023 13:11
    +4

    Но в этом и есть первая потенциальная опасность. Мы не знаем реального типа каждого элемента в списке, пока явным образом не проверим его

    Если мы составляем список из A. то вероятно мы хотим работать с ними как с A, а не как с B - в этом и смысл полиморфизма - неважно кто конкретно лежит под А, если список из A, то этот уровень абстракции нам и нужен. С таким же успехом можно сказать и про интерфейсы. Для чего нам проверять каждый элемент? Если мы хотим работать с B, то нам нужен список B. Если для B нужен особый подход, отличный от A - он переопределяет метод, через который внешний мир взаимодействовал с A.

    Разумеется, это довольно глупый пример, однако он демонстрирует серьёзную угрозу:

    То, должен ли вызываться метод базового класса, зависит от реализации подкласса.

    Как себя вести решает сама сущность, разве нет? Будет ли сын вести себя как его родители или немного по другому - его личное дело. Конечно можно написать кривой код, который будет противоречить принципу подстановки - тогда уже дело не в ООП, а в криворукости пишущего.

    Когда что-то одно нужно изменить, десятки других элементов ломаются

    Если что-то нужно изменить, от чего зависят десятки других элементов - это не только про ООП, а про все как в программировании, так и в реальном мире


  1. nronnie
    31.05.2023 13:11
    +1

    Про то, что наследование не следует использовать для повторного использования кода уже кто только не писал - и ГоФ, и Саттер, и Александреску, и Рихтер, и наверняка кто-то еще. Но нет же, продолжаем топтать те же грабли, а потом писать такие вот статьи.


  1. nronnie
    31.05.2023 13:11
    +1

    VTable проверяется для динамической диспетчеризации нужного метода в среде исполнения.

    У вас очень ошибочное представление о том как работает VTable. Смещение указателя на нужный метод в VTable определяется и известно уже на этапе компиляции и никакой "динамической диспетчеризацией" там даже и не пахнет.


    1. mayorovp
      31.05.2023 13:11
      +1

      Вообще-то,, доступ к VTable по фиксированному смещению как раз динамической диспетчеризацией и называется.


      1. nronnie
        31.05.2023 13:11

        Да, тут я, пожалуй, попутал с поздним связыванием. Но, в любом случае, никакого "поиска" по VTable при вызове виртуального метода не происходит - операция не более затратная чем разыменование указателя.


  1. mayorovp
    31.05.2023 13:11

    В середине поста автор обещает рассказать нам чем именно опасен метод update, после чего отвлекается на VTable, дальше рассуждает о методе draw в совсем другой иерархии классов... и так и не возвращается к методу update.

    Так всё-таки, в чём опасность наследования Player - Character - GameObject?


  1. SergejT
    31.05.2023 13:11

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