Почему в C++ нужно строить всю программу на ООП (длинный вопрос)? Я программист на C++. Хочу понять, почему считается хорошим тоном строить всю программу на основе ООП. Приведу свой взгляд на ООП, и хочу, чтобы вы мне объяснили, где я не прав.
Пара слов о терминологии. Под "объектом" я понимаю "объект в смысле ООП", а не то, что понимается под объектов в стандарте C++ и на cppreference.com ( en.cppreference.com/w/cpp/language/object ).
Я прекрасно понимаю, зачем нужно программировать без goto. Зачем нужно разбивать код на функции. Но вот необходимости в повсеместном использовании ООП я не вижу. Все обоснования ООП кажутся мутными, сам ООП - хайпом.
Да, вы можете сказать, что ООП нужен, чтобы код выглядел красивее и чище, и чтоб его за мной было проще редактировать другому программисту. Отвечу так: "объяснить я и сам могу, я понять хочу" (c). Чем отличается разбиение на классы от обычного разбиения на функции, я не понимаю. И почему в первом случае другому программисту, работающем после меня, будет якобы проще.
ООП - это три кита: инкапсуляция, наследование и полиморфизм (при этом под полиморфизмом понимается динамический полиморфизм). Из этих трёх, как мне кажется, часто возникает потребность лишь в одном: инкапсуляция. И ещё в одном, не входящем в эту троицу: RAII. Что касается наследования и полиморфизма, то в них возникает потребность очень редко.
Но даже инкапсуляция и RAII, на мой взгляд нужны лишь там, где они нужны. Скажем, владеем мы каким-то ресурсом - файловым дескриптором или ещё чем-нибудь - нужен RAII. Или есть некие данные, к которым мы хотим предоставить некий интерфейс (возможно, гарантирующий сохранение неких инвариантов этих данных) и запретить их менять напрямую - нужна инкапсуляция (то есть в C++ это будет выражено классом с private и public функциями-членами). А зачем нужно строить всю программу на основе ООП - я не понимаю.
Что касается наследования и полиморфизма, то могу привести лишь несколько примеров, где они нужны. (При этом я считаю, что наследование нужно лишь в связке с полиморфизмом.) Первый - это библиотеки виджетов, такие как Qt. Объясню, зачем там нужно наследование и полиморфизм, на мой взгляд. Есть большой виджет, скажем, окно, на котором расположены другие маленькие виджеты, скажем, кнопки, чекбоксы и пр. Большой виджет хранит список маленьких виджетов. Допустим, большому виджету понадобилось нарисовать себя. Для этого нужно нарисовать все маленькие виджеты. А для этого он итерируется по ним по всем и вызывает у каждого функцию-член, скажем, draw. Проблема в том, что каждый из маленьких виджетов рисуется по-разному, то есть функция draw должна быть у каждого своя. Но при этом большой виджет должен неким образом одним циклом проитерироваться по всем и вот эту вот эту функцию draw, которая у каждого своя, каким-то образом вызвать. Как это сделать? Нужно сделать базовый класс и назвать его, скажем, QWidget (так он назван в Qt), объявить у него виртуальную функцию draw, унаследовать от QWidget остальные виджеты и переопределить у них этот draw. Большой виджет будет хранить в себе список QWidget'ов по указателю. Будет итерироваться по ним и virtual call'ом вызывать draw. Готово.
Вот только подобная ситуация в обычных программах (за пределами библиотек виджетов) возникает крайне редко. В данном случае наследование и полиморфизм оказались нужны, потому что мы имеем в данном месте указатель на "виджет вообще", мы не знаем в данном месте кода, какой это именно виджет, но нам нужно вызвать draw, который может быть разным в зависимости от типа виджета. Но подобное возникает крайне редко в программах за пределами библиотек виджетов. Где вообще может такое оказаться, что у нас есть указатель на сущность, и мы не знаем, что это конкретно такое?
Ладно, хорошо, такое может случится, но лишь иногда.
Ещё один пример, который я могу придумать: хранение AST (abstract syntax tree). Там, допустим, узлы, хранящие выражения, представлены типом Expression, от которого наследуются разные конкретные выражения, скажем, FunctionCall, Addition, Multiplication и так далее. И у Expression есть виртуальные функции для различных манипуляций с этим узлом. Но и это, опять-таки, лишь какой-то очень частный случай. И даже в нём можно придраться. Скажем, сделать выражение просто std::variant, хранящим разные варианты этого выражения. Тогда никакое наследование и полиморфизм не нужны. Когда нужно сделать какие-то манипуляции с выражением, делаем switch по типу выражения. Вы можете возразить, что тогда при добавлении нового типа выражения придётся всё менять. Ну да. В случае ООП тоже пришлось бы много чего менять. Вы скажете, что при добавлении нового типа выражения придётся всё перекомпилировать. Ну да. У типичных языков программирования типа C++ новые типы выражений добавляются крайне редко, ну придётся лишний раз каждый год пересобирать компилятор, ничего не случится. Вы скажете, что virtual call быстрее switch. Что ж, может быть оно и так, но тогда получается что в данном конкретном случае преимущество ООП всего лишь в скорости. Никаких философских обоснований тут нет.
Возьмём STL. STL'ные классы не имеют наследования и полиморфизма. Точнее, vector часто наследуется от vector_base, но это лишь деталь реализации, не видная пользователю vector'а. Да и само это наследование нельзя назвать идеоматическим, т. к. идеоматическое наследование, как я понимаю, подразумевает отношение "является", т. е. выходит, что "vector является vector_base", что за бред? От vector'а не рекомендуется наследоваться. Зато у STL'ных классов есть инкапсуляция и RAII, как раз то, что я хорошо понимаю. Я действительно понимаю, зачем vector'у инкапсуляция и RAII. Но вот без наследования и полиморфизма весь этот STL прекрасно работает, что, на мой взгляд, доказывает их ненужность.
На одном собеседовании меня попросили рассказать, как бы я стал писать тетрис. Как я бы разбил это приложение на классы. Подразумевается, что это графическое приложение. Фигурки цветные, красивые. Есть счётчик очков, всё как полагается. И я не смог ответить. Я не вижу тут необходимости в классах. Той редкой ситуации, как было с теми виджетами, когда у нас есть указатель непонятно на что, тут нет. Так что наследование и полиморфизм тут не нужны. Некие данные, у которых должен сохраняться некий инвариант, к которым хочется предоставить некий интерфейс, тут тоже отсутствуют. Значит, инкапсуляция тоже не нужна. Всё, не нужны тут объекты. Нужны функции. Нужны struct'ы. Мне подсказывают, что у приложения есть главное меню, в нём можно выбрать пункт "играть" и перейти к основному игровому полю. Ну да, окей, и что? Это всего лишь значит, что при нажатии на "играть" должна вызываться функция, которая будет отвечать за сам игровой процесс. А когда игра будет окончена, эта функция должна сделать return и вернуть управление в функцию, отвечающую за меню. Где тут объекты? Опять не нужны.
Мне нравится код ядра Linux. Никакого разделения программы на объекты там нет, и всё там окей, прекрасно без такого разделения живут. Правда, полиморфизм там всё же есть в каком-то виде. Например, там есть struct file (которая отвечает за открытый файл), который хранит в себе указатель на struct file_operations (я смотрел исходники Linux 4.11.6). struct file_operations - это своего рода таблица виртуальных функций. И она хранит в себе указатели на операции, которые можно делать с файлами. Своего рода виртуальные функции. И они действительно нужны. Потому что в ядре есть места, где мы работаем с файлом, и не знаем, что это именно за файл (это может быть файл на диске, конец пайпа или ещё что-нибудь), но в этот файл хочется записать. Как в том примере с виджетами. Видите? Полиморфизм есть, но он есть только там, где это нужно. И никакого "А давайте всю программу разобьём на классы, просто потому что так надо" тут нет. На мой взгляд, Linux устроен совершенно верно, именно так и нужно писать.
Идея ООП мне кажется крайне мутной. Даже необходимость монад в хаскелле, мне кажется, проще обосновать. Вот зачем нужны монады, и даже зачем является хорошей идеей писать всю программу на монадах, я понимаю. А зачем нужен ООП - нет. Хорошим ответом на этот вопрос была бы статья в блоге на тему "Что такое монады в хаскелле и зачем они нужны", но только с ООП вместо монад.
Вы можете ответить "Да, ты прав, ООП не нужно, точнее, нужно, но лишь иногда, писать всю программу на классах не надо". И может, даже аргументы какие-нибудь привести. Так вот, это не то, что мне сейчас надо. Я сейчас ищу работу. И для этого я хочу понять ООПшный стиль написания программ. Пусть даже он неправильный, скажем, с академической точки зрения. Но я хочу действительно его понять. Я хочу понять, как оно там сейчас делается в бизнесе. Это то, что мне сейчас на работе пригодится. Так что прошу писать только аргументы в пользу ООП, но не против. Я не хочу тут устраивать холивар на тему "Нужен ли ООП". Холивар на тему "А какое правильное определение ООП?" тоже прошу не устраивать. Ну там, "Что понимал под ООП Alan Kay?" и прочее. Будем понимать ООП в том смысле, в котором его обычно понимаю программисты на C++ сегодня, пусть даже по каким-то причинам это определение кому-то покажется неправильным.
Окей, что я хочу в качестве ответа. Идеальной была бы ссылка на какую-нибудь статью в блоге. Где бы объяснялось, зачем нужно делить программу на объекты. С примерами. Скажем, разбиралась бы какая-нибудь типичная задача. Тетрис, текстовый редактор. И объяснялось бы, как поделить её на классы. Причём, чтобы в качестве классов были не только виджеты. Зачем виджеты сделаны классами, допустим, я уже понял. И главное, чтобы объяснялось почему программу нужно написать именно так, на ООП, а не просто функциями, и почему именно таким способом разбить на классы, а не другим. Просто объяснения в стиле "Давайте напишем на ООП. Хряк, хряк, хряк. Получилось" не надо, хочется понять, зачем было сделано на ООП.
Может быть, можете посоветовать книжек. Но, опять-таки, тех, где на примерах объяснялось, зачем нужен ООП.

21 Авг 2019 в 07:01
254 +1
0
Ответы
1

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

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

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

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

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

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

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

Если вы хотите глубже понять принципы ООП и их применение, рекомендую обратиться к классическим книгам по данной теме, таким как "Объектно-ориентированное программирование. Подход на основе UML" Гради Буча и "Язык программирования C++, 4-е издание" Бьярне Страуструпа. Также вы можете изучить различные онлайн курсы по ООП на платформах типа Coursera и Udemy.

20 Апр 2024 в 13:09
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Название заказа не должно быть пустым
Введите email
Бесплатные доработки
Гарантированные бесплатные доработки
Быстрое выполнение
Быстрое выполнение от 2 часов
Проверка работы
Проверка работы на плагиат
Интересные статьи из справочника
Поможем написать учебную работу
Название заказа не должно быть пустым
Введите email
Доверьте свою работу экспертам
Разместите заказ
Наша система отправит ваш заказ на оценку 96 005 авторам
Первые отклики появятся уже в течение 10 минут
Прямой эфир