понедельник, 11 июля 2022 г.

Недостатки ООП

Есть много красивых идей, которые у людей ассоциируются с ООП (объектно-ориентированным программированием), но объектно-ориентированное программирование сопряжено также и с рядом недостаков.

По мотивам Object Oriented Programming is an expensive disaster which must end
(http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end)

Кстати, о парадигамах программирования можно ознакомиться в Википедии - их много! Но в данной статье раскрываются два тезиса:
  1. По сравнению с другими языками (лиспами, функциональными языками и т. п.) ООП-языки не обладают уникальными сильными сторонами.
  2. По сравнению с другими языками языки ООП обремены ненужной сложностью, что приводит к высоким издержкам проектирования и создания кода.
ООП может быть плохо определенным, аморфным понятием, но оно абсолютно доминирует в технологической индустрии. Многие разработчики программного обеспечения и многие компании считают, что ООП - единственный разумный способ разработки программного обеспечения сегодня. Любой, кто выступает против ООП, немедленно осознает тот факт, что он выступает против «общепринятого мнения» отрасли.

12 вещей, которые должны быть преимуществами ООП, но на самом деле таковыми не являются
  • Инкапсуляция.
  • Полиморфизм.
  • Наследование.
  • Абстракция.
  • Повторное использование кода.
  • Преимущества дизайна.
  • Сопровождение программного обеспечения.
  • Принцип единой ответственности (Single Responsibility Principle, SRP).
  • Принцип открытия/закрытия (Open/closed principle).
  • Принцип разделения интерфейса (Interface segregation principle, ISP).
  • Принцип инверсии зависимостей (Dependency inversion principle).
  • Статическая проверка типа.
ООП акцентирует внимание на внутренних свойствах объектов и внутреннем поведении, но при создании больших и главное, - развиваемых систем, - основаная сложность лежит в проектировании взаимодействия модулей, а не в области внутреннего устройства объектов. Конечно, важно и то, и другое, но суть проблем лежит в области акцентов - интеграция и взаимодействие или внутренняя структура. И еще одно, ООП - не слишком ли аморфная концепция с точки зрения обеспечения успеха?

Инкапсуляция

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

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

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

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

Полиморфизм

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

Зачем нам полиморфизм? Мы хотим гибкости в способах управления исполнением. Но языки ООП обеспечивают гибкость только на основе сигнатуры метода, и сигнатура почти всегда ограничена типами параметров, передаваемых в метод. Так в чем тогда преимущества полиморфизма, если они весьма относительны.

Есть и другие способы гибкой диспетчеризации исполнения.

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

Наследование - одна из важнейших идей в разработке программного обеспечения: мы все хотим иметь возможность создавать иерархии типов данных.

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

  • Большая иерархия наследования. Чрезмерное использование наследования может привести к иерархии наследования, которая имеет несколько уровней глубины. Такими большими иерархиями наследования становится трудно управлять. Их трудно поддерживать из-за того, что производный класс уязвим для изменений, внесенных в любой из производных классов, что и  приводит к хрупкости. Есть еще соображения производительности: создание экземпляров таких классов включает вызов конструкторов по всей иерархии наследования. Плюс требования к памяти "выше среднего" для таких объектов. Примером такого класса - класс javax.swing.JFrame в библиотеке Java Swing, который имеет шесть уровней наследования.
  • Хрупкие суперклассы. Классы, которые были подклассами, не могут быть изменены в последующих версиях, потому что это может отрицательно повлиять на производные классы.
  • "Разрыв" инкапсуляции. Наследование в ООП - это прежде всего механизм повторного использования исходного кода, а не механизм повторного использования двоичных объектов. Но прозрачность характера наследования ООП зависит от того, является ли автор производного класса автором базового класса или имеет ли он доступ к деталям реализации базового класса. Это нарушает один из других принципов объектно-ориентированного программирования -  инкапсуляцию.

Джошуа Блох в своей книге «Эффективная Java» говорит: «Предпочитайте композицию наследованию». Это симптоматично, что ООП пришлось отказаться от такой мощной идеи как инкапсуляция и признать, что композиция стала предпочтительной стратегией.

Композиция поверх наследования (или принцип составного повторного использования) в ООП - это метод, с помощью которого классы могут достичь полиморфного поведения и повторного использования кода путем включения в класс других классов, реализующих желаемую функциональность. И это вместо наследования.

Абстракция

«Разработчики программного обеспечения используют абстракцию для разложения сложных систем на более мелкие компоненты». Это абсолютно верно и не имеет ничего общего с ООП.

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

Это определение «абстракции» приводит к совету «программа к интерфейсу, а не "программа к реализации класса».

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

Функции производят выход. У функций есть входы и выходы. Входы и выходы - это структуры данных, которые изменяются функциями. В большинстве языков функции построены из последовательности императивов типа: «Сделай то, а потом то ...». Чтобы понять функции, вы должны понимать порядок, в котором они выполняются. Функции понимаются как черные ящики, которые преобразуют входы в выходы. Если я понимаю ввод и вывод, значит, я понял функцию. Это не значит, что я мог написать функцию.

Структуры данных просто есть. Они ничего не делают. Они декларативны по своей сути. «Понять» структуру данных намного проще, чем «понять» функцию. Функции обычно «понимают», наблюдая за тем как они переносят структуру данных типа T1 в структуру данных типа T2.

Поскольку функции и структуры данных представляют собой совершенно разные типы сущностей, считать их одной сущностью (классом) неправильно.

Повторное использование

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

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

Преимущества дизайна (Design Benefits)

Суть преимуществ дизайна в ООП формулируется так: «Кроме того, как только программа достигает определенного размера, объектно-ориентированные программы на самом деле проще программировать, чем не объектно-ориентированные».

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

Джефф Этвуд описал как минимум 2 проблемы с концепцией паттернов дизайна :
1. Шаблоны проектирования - это форма сложности. Как и в случае со всей сложностью, я бы предпочел, чтобы разработчики сосредоточились на более простых решениях, прежде чем сразу переходить к сложному рецепту шаблонов проектирования.

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

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

Сопровождение программного обеспечения

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

Способность поддерживать код в течение длительного времени в решающей степени зависит от способности программистов понимать код, и ни один программист не может правильно рассуждать о профиле изменяющегося состояния, особенно тогда, когда профиль становится достаточно большим - слишком много взаимодействий.

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

Принцип единой ответственности (Single Responsibility Principle, SRP)

Принцип единой ответственности (SRP) гласит, что у класса должна быть одна и только одна причина для изменения. Другими словами, методы класса должны изменяться по одним и тем же причинам, на них не должны влиять разные силы, которые изменяются с разной скоростью.

В пределах класса может быть много причин изменения состояния. Например, для класса "Сотрудник", изменение может вызвано многими причинами: изменением должности, зарплаты, квалификации, отпуском, болезнью и тому подобное. Эти методы обычно реализуют в пределах одного класса, с тем чтобы контролировать все причины изменения в одном месте. Реализуются они так потому что, согласно концепции ООП, хороший класс должен содержать все методы, управляющие им. И мы не можем распределить функции по разным классам, чтобы удовлетворить принципу SPR.

Принцип открытия/закрытия (Open/closed principle)

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

Принцип разделения интерфейса (Interface segregation principle)

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

ISP - это костыль, который поддерживает ООП. То есть ООП нуждается в поддержке. 

«ISP предназначен для того, чтобы система оставалась изолированной, и, в силу этого, ее было легче реорганизовать, изменить и повторно развернуть». 

Более серьезный вопрос: «Облегчает ли ООП рефакторинг, изменение и повторное развертывание?» 

Если ООП подрывает эти вещи, то ISP - не более чем лекарство для очень больного пациента.

Мы все можем согласиться с тем, что «написание… не требующего пояснений программного обеспечения почти так же важно, как написание рабочего программного обеспечения». Но как создать программное обеспечение, не требующее пояснений? 

Ключевым моментом является выявление базовой модели данных, то есть типов данных и их иерархии. 

Фредрик Брукс однажды сказал: "Покажи мне свои блок-схемы и скрой свои таблицы, и я буду продолжать озадачиваться. Покажите мне свои таблицы, и обычно ваши блок-схемы мне не понадобятся; они будут очевидны". 

Эрик Реймонд: "Покажите мне свой код и скройте свои структуры данных, и я буду продолжать озадачиваться. Покажите мне свои структуры данных, и обычно ваш код мне не понадобится; это будет очевидно".

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

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

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

В конце концов, нужно ли вам отказаться от абстракций ООП и перейти к языку более низкого уровня, если вас больше всего беспокоят вопросы эффективности?

Принцип инверсии зависимостей (Dependency inversion principle)

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

Одна из забавных вещей в мире корпоративной Java - это огромная активность по созданию альтернатив основным технологиям J2EE. Во многом это реакция на тяжелую сложность основного мира J2EE. Обычная проблема, с которой приходится иметь дело, - как связать воедино различные элементы: как совместить эту архитектуру веб-контроллера с поддержкой интерфейса базы данных, когда они были созданы разными командами, мало знающими друг друга. Существует три основных стиля внедрения зависимостей:
  • Constructor Injection (внедрение конструктора), 
  • Setter Injection (внедрение сеттера),
  • Interface Injection (внедрение интерфейса).

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

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

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

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

Статическая проверка типа

Самый очевидный аргумент против статической типизации заключается в том, что она требует определенного уровня уверенности, которой может и не быть. Разработчики, использующие статическую типизацию, указывают все типы в начале своего проекта, прежде чем они получат более конкретные сведения о решаемой проблеме. Сторонники статической типизации приведут контраргумент о том, что типы легко менять по мере того, как разработчик узнает больше о решаемой проблеме. Но я бы сказал, что неверно делать вид, будто вы вначале понимаете систему. Я хочу честно сказать, - я невежественен, когда начинаю работать над проблемой, с которой никогда раньше не сталкивался.

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

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

Идея аутсорсинга разработки программного обеспечения основывалась на некоторых предположениях о том, как должна проводится разработка программного обеспечения. В частности, такой идеи - как идея о «гениальном» архитекторе, поддерживаемом армией дебилов, которые действуют как секретари, программируя под диктовку. ООП был программным эквивалентом тенденции, которая стала распространенной в производстве в 1980-е годы: дизайн должен оставаться в головной конторе а фактическое производство кода должно быть отправлено в страны третьего мира. Работая с UML-диаграммами, написание кода могло быть сведено к простой рутинной работе, в то время как разработкой программного обеспечения могли заниматься провидцы, обладающие эпическим воображением, провидцы, которые могли указать иерархию объектно-ориентированных приложений, которая затем могла быть отправлена ​​в Индию или Вьетнам.

Суть в том, что, затрудняя глупым программистам делать плохие вещи, Java действительно мешает умным программистам, пытающимся создавать хорошие программы.

Комментариев нет:

Отправить комментарий