О C++ и объектно-ориентированном программировании

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет, Хабр!

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


В последнее время много пишут о C++ и о том, в каком направлении развивается этот язык и о том, что большая часть того, что именуется «современным C++» — просто не вариант для разработчиков игр.

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

Об объектно-ориентированном программировании (ООП) как инструменте



Хотя C++ и описывается как мультипарадигмальный язык программирования, на практике большинство программистов используют C++ сугубо как объектно-ориентированный язык (обобщенное программирование используется для «дополнения» ООП).

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

Об энтропии как тайной силе, подпитывающей разработку ПО



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

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

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

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

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

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

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


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

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

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

О легкости удаления кода как о принципе проектирования



В любой системе, построенной по принципу ООП, именно объектам в составе «созвездия» уделяется основное внимание. Но я считаю, что взаимосвязи между объектами важны не менее, если не более, чем сами объекты.

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

О производительности по определению



Но одно из основных соображений, по которым избегается ООП-дизайн – это производительность. Чем больше кода вам требуется запускать, тем хуже будет производительность.

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

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

В данном случае наиболее бросается в глаза, какую кучу кода генерируют компиляторы, даже после оптимизации. Затем, присмотревшись, можно заметить, как затратно и при этом бесполезно такое сопровождение: когда программе передается ненулевое количество аргументов, код все равно выделяет память (вызов new), загружает адреса vtable обоих объектов, загружает адрес функции Work() для ImplB и перескакивает к ней, чтобы затем сразу же вернуться, так как делать там нечего. Наконец, вызывается delete, чтобы высвободить выделенную память.

Ни одна из этих операций совершенно не была необходимой, но процессор исправно исполнил их все.

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

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

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

О борьбе со стереотипами



В статье Анджело Песке «Переусложнение – корень всего зла» автор попадает в самую точку (см. последний раздел: People) признавая, что большинство софтверных проблем на самом деле обусловлено человеческим фактором.

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

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

Правда, за годы работы я выделил для себя три основных аргумента, из-за которых люди не готовы дать шанс другой стороне:

  • «С хорошим ООП так бы не вышло». «Это плохо спроектированное ООП». «Этот код не следует принципам ООП» и тому подобное. Слышал такие вещи, когда демонстрировал примеры ООП, заведшего во все тяжкие (как я уже говорил выше, с ООП-кодом такое происходит неизбежно). Это типичный пример логического заблуждения «Ни один истинный шотландец…».
  • «Я знаю ООП, и, если начинать с чистого листа, то больше ничем пользоваться не хочу». Это страх потерять свой «сеньорский» статус после того, как на протяжении всей карьеры пользовался принципами ООП и руководил другими людьми, от которых также требовал использовать эти принципы. Я считаю, что здесь мы имеем дело с примером «ошибки невозвратных издержек».
  • «Все знают ООП, очень удобно говорить с людьми на общем языке, обладая общими знаниями». Это логическая ошибка, называемая «аргумент к народу», то есть, если практически все программисты пользуются принципами ООП, то эта идея не может быть неподходящей.


Я совершенно осознаю, что выявить в аргументации логические ошибки – это еще недостаточно, чтобы развенчать их. Однако, я верю, что, видя изъяны в собственных суждениях, можно докопаться до истины и найти глубинную причину, по которой ты отвергаешь непривычную идею.
Источник: https://habr.com/ru/company/piter/blog/528176/


Интересные статьи

Интересные статьи

Начну с мелочи. Удобно ли сейчас организована типичная смена раскладки клавиатуры? В смысле переключения на русский/латинский? На мой взгляд, в смартфонах и то удобнее. Н...
Маркетплейс – это сервис от 1С-Битрикс, который позволяет разработчикам делиться своими решениями с широкой аудиторией, состоящей из клиентов и других разработчиков.
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
В июне этого года в небольшом швейцарском городке Рапперсвиле уже в десятый раз прошло мероприятие под названием ZuriHac. В этот раз на нём собрались более пятисот любителей Хаскелля, от новичков...