Showing posts with label ООП. Show all posts
Showing posts with label ООП. Show all posts

19/03/2021

Размышления (2): о C++, Java и областях применения их

В прошлой статье (Размышления (1): о C, C++, связи между ними, почему ООП переоценен и последствия этого) я рассмотрел историю развития двух основных парадигм программирования, уместность и неуместность применения их в современных условиях. В этой статье я поднимусь чуть выше, и опишу своё видение применения самих языков, основываясь на технических особенностях их реализаций и обоснованности применения их в областях.

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

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

Из этого вытекают определённые последствия:
C++ хорошо применять для решения двух задач: разработки высоконагруженных приложений и приложений, работающих в условиях ограниченных ресурсов (что, на самом деле есть одно и то же, только с разных сторон). Как правило, оба вида этих приложений пишутся для узкого круга платформ. Высоконагруженные иногда вовсе для одного сервера или супер-компьютера компании. А работающие в ограниченных условиях — для какого нибудь одного процессора/контроллера. То есть кросс-платформенность в таких ситуациях бывает не нужна. Иногда даже возможность переноса на уровне кода (сборка под другие платформы, портирование) бывает не востребована. А возможности оптимизации, как через сам код C++, так и через применение ассемблерных вставок, безграничны. То есть, исходя из особенностей технической реализации языка C++, использовать его целесообразно для разработки высокоточных с точки зрения вычислительных ресурсов приложений (после C и ассемблера, разумеется) и высоконагруженные сервисы. Без сомнения вам было бы приятно, если бы мой блог (как и другие сайты) загрузился бы и работал в несколько раз быстрее.
Java же разрабатывалась для быстрой разработки кросс-платформенных приложений с относительно низкими порогами вхождения в разработку. На этом языке есть очень мало возможностей что-то оптимизировать (всё это делается путём косвенного воздействия на JVM через свой код). Работают приложения на Java относительно небыстро. А если это «относительно» небыстро умножить на тысячу, миллион запросов/операций/вызовов, получится ощутимо медленно. Зато можно быстро и легко разработать пользовательское (end-user) приложение любой сложности и оно будет работать на всех популярных платформах без переноса (пересборки, перекомпиляции и даже без переконфигурации!).

И что мы видим на практике? В подавляющем большинстве Java используется для разработки enterprise-платформ и web-backend, то есть достаточно критических с точки зрения вычислительных ресурсов систем. А для разработки пользовательских приложений используется C++. Ответом на эту потребность стала кросс-платформенная Qt для разработки пользовательских приложений, в том числе и с GUI, на C++. Да, работает быстро, так как это C++, но писать на C++/Qt несколько сложнее — пороги входа значительно выше, чем на Java и переносимость всё равно обеспечивается только через пересборку. А возможность создавать кроссплатформенные приложения, которой обладает Java из коробки остаётся не востребована вовсе — крупные enterprise-приложения, которые разрабатываются на ней, редко куда-то портируются и бывает так что такой продукт всю свою жизнь работает на одном сервере.

Разработку Enterprise и Web на Java можно понять с финансовой точки зрения — воспользоваться Java для ускорения разработки ПО и повышения безопасности (а на Enterprise и Web она очень важна), заплатив больше за серверные мощности, дешевле чем вкладывать в оптимизацию разрабатываемого ПО и, следовательно, тратить больше времени, разрабатывая их на языке более низкого уровня — C++. А вот разработку end-user-приложений на C++ я понять не могу.

Мне кажется, в сложившейся ситуации, логичнее было бы использовать для разработки пользовательских приложений Java, оставить её на Enterprise и Web, а C++ «вытолкнуть» (скорее оставить, ибо она и так там) на встраиваемую технику и на низкий уровень. В «идеальном» варианте лучше C++ использовать и на Enterprise с Web'ом, но это уже наверное маловероятный сценарий.

Но всё есть так как есть — молодые специалисты тяжело работают, решая несложные задачи на C++ (помним о высоких порогах входа), где часто меньше нужна скорость и больше пригодилась бы кроссплатформенность, а имидж Java опошлен, её область применения искусственно заужена, а уровень оплаты труда так же искусственно завышен.

Что это? Сложившиеся традиции? Как и когда они сложились? Когда и почему всё повернуло в эту сторону?

P.S.
Смоделировать ситуацию, в которой разработка высокоответственных продуктов ведётся на C++, а простых — на Java (что так же снижает барьеры использования пользователями разных платформ), я оставляю вам.

P.P.S
Здесь я сравнивал только применение Java и C++ потому что я имею опыт и (конечно личное) видение проблемы некорректного использования только этих языков. Это не означает, что я не понимаю что конфликты PHP vs Python, Python vs Java, Python vs C++ и пр. так же существуют (а, может, какие-то из этих конфликтов и не существуют). Я не писал о них только потому что не имею достаточного опыта участия в таких проектах или хотя бы наблюдения их.

07/04/2020

Размышления (1): о 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++ считаются родственниками (во всяком случае в POSIX) вполне оправдано. Огромная кодовая база, наработанная всем сообществом POSIX-разработчиков, после выхода C++, при необходимости (при пересмотре подхода от процедурного к ООП) могла быть использована. Я понял, что это была не попытка использовать славу Plain C, а обеспечение возможности сохранить огромное количество наработок. Это следствие того, что управление жёстким диском и принтером, перешло в управление жёсткими дисками и принтерами, то есть логичное отражение ситуации в мире компьютерной техники на средства разработки.

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

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

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

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

Да будет всему своё место, но к ВТ, в любом случае, следует относиться с надлежащим уважением.

P.S.
Здесь так же хочу добавить пояснение. Не всем и не всегда понятно что такое ANSI C и Plain C и когда стоит употреблять тот или иной термин. ANSI C — это стандарт языка, Plain 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 с некоторым дополнениями).