07/04/2020

Размышления (1): о C, C++, связи между ними, переоценка ООП и последствия этого

Если вы являетесь фанатом C++ и/или ООП, лучше пропустите эту статью, потому что здесь я буду критически и аргументировано высказываться (в основном в сторону ООП, C++ и Б. Страуструпа).

Процедурное программирование и объектно-ориентированное


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

Области применения и история ООП и С++


Теперь рассмотрим области применения обоих подходов. Изначально разработка всего ПО была системного уровня — управление устройством приёма перфокарт, жёстким диском, простейшим принтером, примитивные базы данных, несложные файловые системы. Всё это писалось на ассемблере — императивном языке. Поэтому всё началось именно с парадигмы программирования, максимально приближённого к технике. Затем языки программирования развились до таких как C, Pascal, BASIC — процедурные языки. Количество объектов схожих типов, обрабатываемых одной программой, возрастало. Появлялись стандарты интерфейсов — для жёстких дисков, принтеров, дисководов, самих устройств одного типа к компьютеру стали подключать больше. (Обратите внимание на то, что в описании выше я умышленно перечислил все объекты, с которыми работали разработчики в единственном числе, а здесь — во множественном). Появилась многозадачность, и что немаловажно — начали появляться графические интерфейсы — сущности, состоящие из множества объектов с повторяющимися характеристиками (свойствами) со схожим поведением и с некоторыми разнящимися свойствами (например, название и размеры кнопки или окна).
На самом деле интерфейсы были не только графическими, например, был Borland TurboVision — библиотека для создания программ с пользовательским интерфейсом на псевдографике (TUI — Text User Interface). Реализована эта библиотека была на Borland Pascal (Pascal c ООП) и Borland C++.

С конца семидесятых и все восьмидесятые года Бьёрн Страуструп трудился над своим детищем — языком ООП (в том числе и) высокого уровня, в последствии получившим название C++. Долгое время язык назывался «C with Classes» («C с классами») и автор не предполагал выпускать его как публичный продукт. Когда язык C++ начал окончательно формироваться, Страуструп принял решение не отходить от Plain C, и построил C++ на основе последней.
Некоторое время я не понимал, почему человек разработал язык, значительно отличающийся от одного из существующих на тот момент, схожим с ним и даже назвал его похоже («++» означает «шаг вперёд по сравнению с C»). Что, на мой взгляд вносило путаницу (которая никуда не делась). Я даже думал (по аналогии с тем, как, по одной из версий, сделали с JavaScript — использовали Java в названии, чтобы воспользоваться славой молодой и быстро набирающей тогда популярность Java), что Страуструп решил воспользоваться хорошим и сильным имиджем Plain C для того чтобы популяризировать свой язык. Ведь C++ — не первый, и на тот момент, не единственный ООП-язык.

Но теперь, рассмотрев историю, я понял (или выдвинул свою теорию), что C++ стал представляться надстройкой над Plain С, вместо того чтобы выбрать другое название и синтаксис, не для использования славы последней. C++ сохранил совместимость с Plain С для того чтобы хорошо работать с ней в паре. И дело не только в том, что мы можем линковать объектные файлы с обоих языков и легко импортировать функции и даже классы (это можно делать с любыми языками, из кода на которых можно получить бинарно-совместимые объектные файлы). Дело в том, что мы можем использовать исходный код на Plain C в проектах на C++. То есть Plain C и C++ считаются родственниками (во всяком случае в мире UNIX/POSIX) вполне оправдано. Огромная кодовая база, наработанная всем сообществом POSIX-разработчиков, после выхода C++, при необходимости (при пересмотре подхода от процедурного к ООП) могла быть использована. Я понял, что это была не попытка использовать славу Plain C, а обеспечение возможности сохранить огромное количество наработок огромного количества инженеров. Это следствие того, что необходимость управления жёстким диском и принтером, перешла в необходимость управления жёсткими дисками и принтерами, то есть логичное отражение ситуации в мире компьютерной техники на средства разработки.

Рынок профессионалов


Выше я писал о путанице, которая «никуда не делась». Путаница заключается в том, что многие рассматривают C++ не как ООП-надстройку над Plain C, а как «улучшенную» Plain C. Автоматически подразумевая, что Plain C неполноценна и рассматривая её, как «язык предыдущей версии» или поколения. Отсюда происходят много попыток использовать C++ везде, где только получается. А получается далеко не всегда хорошо (об этом ниже). Это имеет последствия и на рынке труда — часто разработчикам на C++ предлагают ощутимо большие зарплаты, чем на Plain-C-вакансиях. А это, в свою очередь, приводит к тому, что новички (студенты и все, кто хотят начать карьеру разработчика) выбирают для изучения C++ (и ООП, соответственно). Становится больше C++-разработчиков, что приводит к ещё большему увеличению соответствующих вакансий. Так этот порочный круг замыкается и превращается в «эпидемию ООП». Отчасти, более высока ставка для ООП-разработчиков оправдана тем, что C++ изначально несколько сложнее изучить — с каждым новым стандартом этот язык усложняется, к требованиям C++-разработчика часто добавляют (и, кстати, так же часто применяемые без надобности) STL, boost и прочие «надбавки». В последние годы к требованиям на C++ ещё добавляется Qt. А Plain C, кажется проще — даже книга от её авторов такая маленькая — за что здесь платить? Разработчику на Plain C нужно платить не за знание «высоких материй» (за которые платят ООП-разработчикам), а за знание и навыки работы с системами — железо (процессоры, контроллеры, шины передачи данных, стандарты, аппаратные протоколы, сети), инструменты (анализаторы, осциллограф, генератор, тестер, иногда даже паяльник), системные API (POSIX, kernel space) и, что самое важное, за понимание того, как всё это связано. Но, как правило, даже когда в компании ищут системного разработчика с пониманием того, что он должен знать, это часто не учитывается предлагаемом окладе. Перед тем как перейти к основной проблеме рынка труда, опишу разницу между информационными технологиями (ИТ) и вычислительной техникой (ВТ). ИТ это Web-технологии, базы данных, обработка информации, desktop- и мобильные приложения, ПО для серверов, графические приложения, профессиональное ПО — текстовые, графические редакторы, пакеты для расчётов физики, IDE для разработчиков и прочее «прикладное» ПО. ВТ — это всё, на чём строятся все ИТ (и вышеперечисленное, но не ограничиваясь им, ПО) от больших машин до встраиваемой техники. Но вся ВТ так же должна быть запрограммирована — процессор сам себя и все свои ядра не включит, Ethernet-пакеты сами себя не отправят и пиксели сами себя не нарисуют на экране. Теперь к проблеме рынка профессионалов. Я не считаю наличие большого количества ООП-разработчиков — специалистов в ИТ, на рынке проблемой. Я считаю проблемой те последствия, которые мы имеем, а именно — то, что перекос в сторону ООП-разработчиков, подогреваемый более высокими вознаграждениями, приводит к дефициту специалистов в вычислительной технике.

Эпидемия ООП или примеры абсурда


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

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

На системном уровне и на встраиваемой технике важно не отвлекаться на ООП-парадигму и не «витать в облаках», а работать как можно ближе к системе или к железу. А ООП-языки (даже C++) сильно «оборачивают» POSIX и аппаратную составляющую машины. Пониманию и изучению POSIX'а и вычислительной техники помогает Plain C, Assembler, так как они максимально близки к вычислительной технике. Переоценка ООП-подхода приводит к тому, что люди учатся ИТ, игнорируя ВТ, которая является основой всей современной техники. И последствием этого становится появление целого пласта профессионалов, вовсе не понимающих ВТ. А людей, понимающих ВТ, становится всё меньше. И в этом виноват не Б.Страуструп, не Э.Шмидт и не Б.Эккель, а весь рынок и те, кто его сформировал и продолжает формировать таким.

Итог

  1. Везде, где можно обойтись без ООП, нужно обходиться без ООП. Таких областей, возможно меньше, чем подходящих под применение ООП, но они есть и их немало. Моё мнение — это весь системный уровень, вся встраиваемая техника. Многие приложения так же могут быть успешно реализованы без ООП.
  2. ООП нужно применять только хорошо подумав, и решив подходит ли ситуация под основной критерий для внедрения ООП — много объектов с похожим поведением и с необходимостью настраивать небольшую часть свойств и достаточно сложные связи между сущностями предметной области. И если у вас Web-сервер, поддерживающий много соединений — даже это ещё не обязательно повод для применения ООП.
Всё это полезно для тех, кто способен (с точки зрения опыта и техники) и может (с точки зрения организационной) принимать такие решения. Тем, кто либо в силу опыта, либо в силу организационных аспектов не может принимать такие решения, пока остаётся учиться и присматриваться.

P.S.


Здесь так же хочу добавить пояснение. Не всем и не всегда понятно что такое ANSI C и Plain C и когда стоит употреблять тот или иной термин. ANSI C — это стандарт языка, Plain C — это термин, обозначающий концепцию, парадигму, процедурного подхода на конкретном языке — C. Когда упоминают ANSI C, имеется в виду контекст стандарта (как правило его жёсткие ограничения), когда говорят Plain C, акцент делается именно на парадигме программирования (процедурном) — как противопоставление C++. Любая ANSI C есть Plain C, но не любая Plain C есть ANSI C.

До 1988 года Plain C-код на разных компиляторах мог не собраться. Выражение ANSI C было популярным после выхода стандарта, в 1988 году, когда компиляторы стали «держаться ближе» к этому стандарту. Тогда было актуально, например, на собеседовании говорить, что вы работаете на ANSI C. Сейчас большинство компиляторов близки к стандарту, поэтому в основном, в речи уместно использовать термин Plain C (или просто «Си»). Так как во-первых, скорее всего, вы имеете в виду именно парадигму программирования, во-вторых, вы вряд ли знаете весь стандарт наизусть чтобы похвастаться знанием ANSI C (даже GCC реализует ANSI C с некоторым дополнениями).

P.P.S.


Да будет всему своё место и всего необходимого достаточно.

No comments:

Post a Comment