Я программист на 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++ сегодня, пусть даже по каким-то причинам это определение кому-то покажется неправильным.
Окей, что я хочу в качестве ответа. Идеальной была бы ссылка на какую-нибудь статью в блоге. Где бы объяснялось, зачем нужно делить программу на объекты. С примерами. Скажем, разбиралась бы какая-нибудь типичная задача. Тетрис, текстовый редактор. И объяснялось бы, как поделить её на классы. Причём, чтобы в качестве классов были не только виджеты. Зачем виджеты сделаны классами, допустим, я уже понял. И главное, чтобы объяснялось почему программу нужно написать именно так, на ООП, а не просто функциями, и почему именно таким способом разбить на классы, а не другим. Просто объяснения в стиле "Давайте напишем на ООП. Хряк, хряк, хряк. Получилось" не надо, хочется понять, зачем было сделано на ООП.
Может быть, можете посоветовать книжек. Но, опять-таки, тех, где на примерах объяснялось, зачем нужен ООП.