Как стать автором
Обновить

Комментарии 55

Мне довелось лицезреть множество абстрактных оторванных от жизни примеров с кошками, собачками, машинками, трансформерами, треугольниками, кружочками, … которые призваны объяснить принципы ООП. Ну не серьезно как-то это все. 

Ну да, ну да... Зато вот это серьезно?

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

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

До того, как работать в банке, занимался разработкой системы мониторинга инженерного оборудования зданий (софт верхнего уровня обеспечивающий работу с сетью промконтроллеров и подключенных к ним конечных устройств). И там активно использовал принципы ООП - структура классов отражала физическую сущность системы - был класс "объект" (на котором установлены устройства), был базовый класс "устройство" с производными от него классами для "УСО" (контроллер нижнего уровня), "IP-шлюз" (контроллер верхнего уровня) и т.д. и т.п.

И да, все это бы красиво, удобно, работало. Но. Только потому что сущностей было относительно немного, и относительно немного было свойств у каждой сущности.

В банке же первое желание было "ну можно же что-то ООПное сделать...". Но очень быстро пришло понимание того, что все это на практике нереализуемо.

  • Количество сущностей (клиент, счет и т.п.) зашкаливает. Это даже не десятки, сотни.

  • Количество свойств у каждой сущности зашкаливает. При этом свойство может быть самостоятельной сущностью со своими свойствами и "вложенными" сущностями.

  • Связи между сущностями крайне сложны и запутаны.

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

Вообще, под каждую конкретную задачу тут рисуется соответствующий запрос. Который, как правило, достаточно сложен - может содержать несколько подзапросов (with ... as ...), join по нескольким таблицам (и/или подзапросам) и т.д. и т.п. Если пытаться все это запихнуть в ООП (в виде набора сущностей и их свойств), получим существенный оверкод и снижение производительности. Или под каждую задачу придется писать свой набор объектов со своими свойствами и методами, но тогда теряется смысл - получим фактически тот же процедурный подход, но с вистом и профурсетками классами

Даже в приведенном примере:

            MathOperation[] operArr = new MathOperation[] 
            {
                new MulOperation(),
                new DivOperation(),
                new Sub2PosOperation()
            };

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

Если вы прогоните оба примера в цикле, скажем, 1 000 000 раз с засечкой времени - вы увидите разницу.

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

Ну да, ну да... Зато вот это серьезно?

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

Далее, судя по тому что:

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

а в банке

Все это приводит к тому, что количество типов будет огромным. Структура классов крайне сложной. И все это отрицательно скажется на производительности по одной простой причине - создавая объект типа "клиент" мы не может сразу в конструкторе подтянуть все, что с ним связано - это огромный объем информации - собирать ее долго, хранить в памяти нерационально

я возьмусь предположить (исключительно предположить! я ничего не утверждаю!),

что для системы мониторинга иерархия объектов была построена правильно, или как вы это формулируете:

структура классов отражала физическую сущность системы

а вот в банке иерархия была построена НЕ правильно и НЕ отражала физическую сущность системы. Проблемма обычно именно в этом! Я как раз пытался подойти к формулировке такой проблемы в своей статье, в разделе: "Выбор объекта базового класса, философское отступление"

Я это много раз на практике видел, когда выбранная иерархия объектов буквально стоит поперек тех реальных задач, которые надо решать, как раз такая поперечность ведет к последствиям которые вы описали:

Все это приводит к тому, что количество типов будет огромным. Структура классов крайне сложной. И все это отрицательно скажется на производительности

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

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

Но хоть анекдот-то понравился? или тоже нет?

а вот в банке иерархия была построена НЕ правильно и НЕ отражала физическую сущность системы. Проблемма обычно именно в этом!

Я попытался объяснить что построить "правильно" иерархию объектов в столь сложной системе задача крайне нетривиальная, более того, мне она кажется невозможной в практическом плане.

Ну вот очень упрощенно. Есть сущность "клиент". У него куча клиентских данных (там одних адресов может быть штук шесть разных, а адрес это отдельная сущность со своими атрибутами).

У клиента еще много всяких свойств. Например, счета. У счета свои признаки (которых тоже несколько десятков). Дальше к счету могут быть привязаны карты. Со своими признаками. Далее, у клиента могут быть "субъекты" - всякие доверенные лица (т.е. есть еще доверенности), которые не являются клиентами банка, но у которых тоже много всяких данных (те же адреса, доверенности и т.п.).

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

А теперь простенькая задачка. Нужно выбрать клиентов определенного типа (скажем, ФЛ и ИП), имеющих счета открытые типа 40817, последняя дата актуализации клиентских данных у которых не ранее чем год от текущей а дата окончания срока действия основного ДУЛ (документ удостоверяющий личность) не более чем +месяц от текущей. Есть этому клиенту еще не посылалось уведомление об окончании срока действия ДУЛ, послать уведомление. После чего проверить наличие у данного клиента держателей карт (когда клиент делает к своему счету карту третьему лицу) и если им еще не посылалось уведомление - разослать.

Это (на нашем уровне) достаточно тривиальная задача. Как ее решать в рамках ООП?

В процедурном подходе все достаточно просто - один запрос (да, он будет "разлапистым", но работает достаточно быстро) выдает полный набор всех необходимых данных, которые просто передаются в функцию отправки уведомления. Далее второй запрос дает необходимые данные для рассылки уведомления держателям карт. Тоже быстро.

Что буде в ООП? Получим выборку, для каждого элемента создадим объект "клиент" и для него вызовем метод "послать уведомление"? А представляете сколько таких методов будет у класса "клиент"? Сотни, если не тысячи. И каждый новый бизнес-процесс потребует добавления новых методов.

Или у нас будет функция послать уведомление, которая будет принимать на вход объект типа "клиент"? Но зачем, когда у нас уже есть такая функция, которая принимает на вход набор данных из выборки и у нас нет необходимости тратить время и ресурсы процессора на создание объекта. К слову сказать, достаточно много выборок, которые могут вернуть несколько миллионов (а то и десятка миллионов) записей. И на каждую запись объект создавать а потом удалять?

У нас огромные объемы данных и огромное количество разных бизнес-процессов. И вопрос эффективности использования ресурсов стоит крайне остро. Посему стараемся минимизировать (кроме ситуаций когда без этого ну совсем никак) работу с динамической памятью, отдавая предпочтение статике.

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

А теперь попробуйте обосновать необходимость затрат времени и ресурсов на этот процесс. Главные критерии:

  • оно будет быстрее работать?

  • оно будет потреблять меньше ресурсов процессора?

  • и, главное - сколько на этом заработает банк?

Есть огромная система. Это не монолит, это скорее модель акторов. В ней десятки тысяч таблиц и индексов. Десятки тысяч программных объектов. Сотни миллионов бизнес-операций в сутки. Тысячи параллельных бизнес-процессов.

Построить подо все это "правильную" и непротиворечивую ООП модель ну такое себе занятие.

К слову сказать, у нас есть команда, которая этим занимается. Лет уже наверное десять (ну может чуть меньше). Результат - практически нулевой. Нет, что-то там у них работает, но работает локально, до охвата всего-всего там как до луны пока. И никто так и не смог доказать, что их подход быстрее или экономичнее по ресурсам (а у нас есть объективные инструменты для измерения этих параметров) чем традиционный процедурный подход (который в разработке ничуть не сложнее на самом деле).

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

принимает на вход набор данных из выборки и у нас нет необходимости тратить время и ресурсы процессора на создание объекта

Здесь под выборкой имеется в виду курсор базы данных, из которой фетчатся данные? И дальше работа с сырыми данными-строками в функции-рассылке? Эффективно, но когнитивно кажется неудобно, легко например порядок полей попутать, или дату криво распарсить, и потом долго искать проблему. А иначе мы в любом случае эти данные как-то сохраняем в памяти, и сравнение с созданием объектов тогда уже выглядит как экономия на пакетах.

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

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

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

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

Просто потому что тут под ООП понимается "базовые классы, наследование, полиморфизм и бла-бла-бла".

Я уже писал - мы работаем на платформе (железо + ОС), которая сама по себе объектная. Тут все есть объект (даже переменные в программе системой воспринимаются как некий "объект" для которого кроме значений в системе еще и свойства хранятся. В те жа процедуры можно кроме параметров передавать т.н. "операционные дескрипторы" (специальным модификатором в нашем языке или прагмой в С/С++) по которым можно узнать, например, что реально скрывается за парамеnром char* - на что реально он указывает.

И там снизу до верху - здесь нет файлов - есть объект типа *FILE с дополнительными атрибутами - pf-src (файл исходных текстов), pf-dta (файл данных, сиречь таблица), lf (логический файл - что-то типа индекса, но более широкое - там и джойны могут быть допусловия...). Нет программ - есть объект типа *PGM. И т.д. и т.п.

Но ни базовых типов, ни наследования, ни полиморфизма тут нет на уровне ОС. Тем не менее мыслить "объектами" тут привычно и естественно. Даже при процедурном подходе.

А внутри него никто не мешает нам собрать необходимые подготовленные данные целиком (даже странно так не делать), то есть не по одному забирать клиентов, а сразу нужных с фильтрами и джойнами.

Именно так и делается. См. мой комментарий с примером Только как это запихать в ООП "с наследованием и полиморфизмом"? А, главное, зачем?

Здесь под выборкой имеется в виду курсор базы данных, из которой фетчатся данные? И дальше работа с сырыми данными-строками в функции-рассылке? Эффективно, но когнитивно кажется неудобно, легко например порядок полей попутать, или дату криво распарсить, и потом долго искать проблему.

И опять см. выше. Никаких там "сырых данных" нет. fetch заполняем массив структур (мы работаем блоками, но можно и по одной записи, но это не так эффективно). Т.е. на выходе имеем сразу заполненную структуру со всеми полями.

Если формат структуры не соответсвует тому, что написано в select ... в объявлении курсора, оно просто не скомпилится - SQL препроцессор нашего языка выкатит ошибку.

И да, наш язык поддерживает все типы данных, которые есть в SQL. И умеет с ними работать. И "парсить дату" нам нет необходимости. Мы можем с ней работать как с датой (вычесть/прибавить нужное количество лет/месяцев/дней, преобразовать ее в строку в нужном формате - *ISO/*EUR или еще каком...)

Т.е. после fetch мы сразу имеем заполненный массив структур с нужными нам данными, а дальше каждый элемент структуры отдается на обработку.

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

Дело в том, что считать сколько там тратится на смс вообще не наша задача. Мы даже не знаем как будет доставляться уведомление клиенту - смс, пушом, курьером на дискете, почтовым голубем... Это не наша забота. Мы - мастер-система. Ядро АБС банка. Наша задача собрать данные и выложить их в очередь с тем, чтобы некая внешняя система их оттуда взяла и уже делала с ними то, что считает нужным.

Мы только смотрим кому нужно посылать уведомление и проверяем посылали его или нет. Если нет - формируем пакет, выкладываем в очередь и делаем отметку что мы со своей стороны уведомление отослали. Все.

И, повторюсь, это всего лишь одна мелкая задачка из многих десятков тысяч. Просто как пример того, что 80% (условно) работы банка (на нашем уровне) происходит именно по такой схеме:

  • есть набор таблиц, в которых содержатся данные

  • есть список условий, по которым нужно выбрать данные.

  • есть набор данных, которые необходимы для реализации логики процесса

  • есть логика что и как делать с этим данными

Вот общая канва. И все 4 пункта в каждом случае разные - нет каких-то общих точек. Для каждого бизнес-процесса все будет свое. По каждому пункту.

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

Все это не более чем общие слова.

А на практике так:

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

Или когда у вас есть какие-то общие операции, которые не зависят от конкретики, куда можно передавать любой объект, унаследованный от данного базового класса.

Здесь ничего этого нет. Каждый бизнес-процесс работает со своим набором данных по своей логике. И, соответственно, нет возможности построить базовый класс для всех процессов, а потом от него наследовать слегка модифицируя логику. Тут вам придется под каждый процесс писать свой класс с нуля.

Вернемся в примеру выше. У нас есть массив структур, память под который выделяется в процессе компиляции. Операцией exec sql fetch этот массив заполнятся нужными нам данными. Далее, мы берем очередной элемент массива и отправляем его в процедуру обработки, где уже происходит вся магия.

Ок. Допустим мы ударились в концептуальность и написали свой класс в котором содержатся все данные и методы их обработки. Что с того? А ничего. exec sql fetch нам не заполнит массив объектов. Он так не умеет. Т.е. нам все равно придется объявлять массив структур, потом каждый элемент массива запихивать в объект класса и дергать нужный метод (причем время жизни объекта ограничено временем работы этого самого метода - ни до ни после нам этот объект не нужен). Извините, а зачем, когда мы все это и так уже имеем, но без всего вот этого вот? Нам тут не нужен объект, нужны только данные. Которые у нас уже есть. Та же самая логика написана в виде процедуры, получающий на вход заполненную в fetch структуру (элемент массива). Т.е. все то же самое, но проще. Ну не концептуально, да...

Еще добавлю.

На мой взгляд использование объектов (в классическим понимании - "наследование-инкапсуляция-полиморфизм") оправдано тогда, когда время жизни объекта существенно. Т.е. создали его, потом он живет своей жизнью по заложенной в нем логике.

В нашем случае это не так. Есть считать "объектом" (без "н-и-п") элемент выборки из БД, то время его жизни крайне мало - от момент попадания в выборку до момента когда он трансформируется в некое сообщение и кладется в очередь.

И никакой особой логики в нем не заложено кроме форматирования сообщения. Вся логика в самой выборке.

Так что тут нет никакого смысла в том, чтобы тратить время и ресурсы на создание (или заполнение) объекта из элемента выборки.

элемент выборки из базы данных - это обычно МАП который позволяет обращаться к своим полям через имя этого поля. Проблема в том что поля могут быть разных типов, поэтому желательно иметь какой-то базовый класс (см. переменную ConvertedObjectClass forConvert в примере ниже) который позволяет применять конверсию данных из объекта унаследованного от этого базового класа к типу в который надо привести данные конкретного поля при обращении, как-то так чтобы можно было писать:

UnifiedSqlAnswer mapObj = <any SQL request>;
ConvertedObjectClass forConvert = mapObj.GetField("Fld Name");
typeA var = (typeA) forConvert;

Вроде как и на С++ такое вполне можно сделать.

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

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

элемент выборки из базы данных - это обычно МАП который позволяет обращаться к своим полям через имя этого поля.

Зачем так сложно-то?

Я же привел пример. С реальным кодом.

  1. Наш я зык поддерживает все типы данных БД - date, time, decimal, numeric, chr, varchar, integer и т.п. Никакой маппинг и конвертация тут не нужны.

  2. Все, что вы написали - от бедности. Попытки прикрутить к языку то, чего там нет изначально. Все это приводит к лишним телодвижениям.

Если мы описываем структуру

      dcl-ds t_dsSQLData qualified template;
        CUS  char(6)     inz;
        CLC  char(6)     inz;
        BRNM char(4)     inz;
        CTP  char(2)     inz;
        SER  char(10)    inz;
        NUM  char(35)    inz;
        OPN  zoned(7: 0) inz;
        EDT  zoned(7: 0) inz;
      end-ds;

для понимания - zoned(7:0) это тип данных с фиксированной точкой, соответствующий SQL'ному NUMERIC(7,0)

Затем объявляем курсор

        exec sql declare curRDKCHK1Clients cursor for
                   select HDA1CUS,
                          HDA1CLC,
                          GFBRNM,
                          GFCTP,
                          RDKSER,
                          RDKNUM,
                          RDKOPN,
                          RDKEDT
                     from HDA1PF HDA1
                     join GFPF      on (GFCUS, GFCLC) =
                                       (HDA1.HDA1CUS, HDA1.HDA1CLC)
                     join RDKPF RDK on (RDKCUS, RDKCLC, RDKUCD, RDKSDL, RDKOSN) =
                                       (HDA1.HDA1CUS, HDA1.HDA1CLC, '001', 'Y', 'Y')
                    where HDA1DAT < :minDA
                      and exists (
                                   select CAFCUS
                                     from CAFPF
                                    where (CAFCUS, CAFCLC, CAFATR1) = (HDA1.HDA1CUS, HDA1.HDA1CLC, 'Y')
                                 )
                      and not exists
                              (
                                select RDKMCUS
                                  from RDKMPF
                                 where (RDKMCUS, RDKMCLC, RDKMUCD, RDKMSER, RDKMNUM, RDKMOPN, RDKMTP) =
                                       (HDA1.HDA1CUS, HDA1.HDA1CLC, '001', RDK.RDKSER, RDK.RDKNUM, RDK.RDKOPN, '3')
                              );

и массив структур куда будем читать блоками по 1000 записей

dcl-ds dsSQLData  likeds(t_dsSQLData) dim(sqlRows);

где sqlRows - константа равная 1000.

А потом цикл чтения

          dou lastBlock;
            exec sql fetch curRDKCHK1Clients for :sqlRows rows into :dsSQLData;

            lastBlock = sqlGetRows(rowsRead);

            for row = 1 to rowsRead;
              procData(dsSQLData(row));
            endfor;
          enddo;

sqlGetRows - функция возвращающая количество реально прочитанных записей и признак есть ли еще записи в выборке (точнее признак последнего блока)

      //=======================================================================
      // Процедура получения количества прочитанных строк в SQL запросе
      //=======================================================================
      dcl-proc sqlGetRows;
        dcl-pi *n ind;
          sqlRowsRead  int(10);
        end-pi;

        dcl-s  sqlRowCount  packed(31 : 0) inz(*zero);
        dcl-s  sqlDB2LstRow int(10)        inz(*zero);

        exec sql GET DIAGNOSTICS
                :sqlRowCount  = ROW_COUNT,
                :sqlDB2LstRow = DB2_LAST_ROW;

        sqlRowsRead = sqlRowCount;

        return (sqlDB2LstRow = 100);
      end-proc;

Итого:

 МАП который позволяет обращаться к своим полям через имя этого поля.

Не мап, а заполненная структура с полями поддерживаемых языком типов данных.

Проблема в том что поля могут быть разных типов, поэтому желательно иметь какой-то базовый класс

Зачем???

Вот вам пример. Там поля двух типов - char и zoned (numeric в sql). Нормальная структура. Зачем тут "базовый класс"?

у вас будет место где определены все нужные вам преобразования типов, это класс ConvertedObjectClass, и вы сможете ими управлять и анализировать их

Откуда и куда предлагаете пребразовывать? И, главное, зачем?

Вы сможете очень много чего контролировать-проверять уже на этапе компиляции.

Что именно? Соответствие полей структуры t_dsSQLData набору полей в select и так контролируется на этапе компиляции.

Все типы полей структуры нативные для данного языка. Нет никаких "мапов", нет никакой "конвертации". Все это просто не нужно. Fetch заполняет структуру, дальше с ней работаем.

Это позволит в какой то степени структурировать ваш код.

Чем вам не нравится код в данном примере? Это из реального взято. Он достаточно прозрачен и легко читаем (для тех, кто с этим языком работает, конечно).

И здесь нет ничего лишнего. Никаких конструкторов, никаких операций выделения память, копирования туда-сюда, маппинга, конвертаций - все это будет занимать лишнее время когда речь идет об обработке 10-30млн записей (да, для нас это вполне обычные объемы данных) которую нужно уложить в приемлемое время. Все это работает предельно быстро и при этом потребляет минимум ресурсов (в т.ч. реального процессорного времени).

Никаких конструкторов, никаких операций выделения память, копирования туда-сюда, маппинга, конвертаций

А с чего вы взяли что при использовании ООП подхода там должно быть что-то лишнее? Я вас уверяю что все делается как раз без всего этого лишнего что вы перечислили. У нас есть массив из N-миллионов записей одной структуры (одного типа) и есть один объект с описанием этой структуры (можно один на каждый поток-thread программы, который с этим массивом работает, читайте мою предыдущую статью про параллельность и потоки). Это объект с описанием этой структуры для того, чтобы обращаться к текущей структуре и к ее полям. По сути, этот объект с описанием структуры, фактически, будет сложным указателем (не умным, а именно сложным, хотя одно другому не мешает!) сложным указателем, через который можно обращаться к целой-текущей структуре-записи и к ее полям, в том числе по имени поля, я вот что имел в виду. Можно сделать такой сложный указатель достаточно умным чтобы он и размер массива со структурами умел сообщать, и даже два размера, один уже загруженный, а другой ожидаемый для полной загрузки, например, и текущий индекс, и фиг знает что еще, что вам может понадобиться.

Вы можете привести какой-то пример, в котором идет обращение к структуре, полученной из базы данных? Вы же написали, как вы получаете структуры-записи из базы данных, я правильно понял? Нужен пример, где ваш код работает с этими полученными структурами и с их полями! Мне почему-то кажется, что именно там у вас все не очень структурировано.

Если вы сможете найти такой пример, в котором происходит работа со структурами, полученными описанным вами способом, я смогу вам показать зачем там нужен базовый класс, про который вы спросили:

Вот вам пример. Там поля двух типов - char и zoned (numeric в sql). Нормальная структура. Зачем тут "базовый класс"?

Кто-то же работает с этими полями, кто-то их читает как поля структуры или нет? Или вы структуры просто дальше пересылаете? Вот это очень интересно выяснить.

Если вы захотите продолжить дискуссию, я не смогу до завтра написать новый комментарий, но смогу добавить ответ для вас в конце статьи, если вы не будете против. Мне интересна тема и она вроде как даже развивает тему статьи.

А с чего вы взяли что при использовании ООП подхода там должно быть что-то лишнее?

Хотя бы лишнее копирование.

Если у вас (у нас) есть структура, то мы просто подсовываем ее адрес в функцию чтения записи из БД, которая просто копирует туда содержимое записи. Дальше мы напрямую работаем с этой структурой.

Если у нас будет объект, то нам придется сначала прочитать запись в буфер, потом этот буфер передать в объект чтобы он заполнил свои внутренние поля. Вот вам лишняя операция.

Вы можете привести какой-то пример, в котором идет обращение к структуре, полученной из базы данных? Вы же написали, как вы получаете структуры-записи из базы данных, я правильно понял? Нужен пример, где ваш код работает с этими полученными структурами и с их полями! Мне почему-то кажется, что именно там у вас все не очень структурировано.

Я вам привел вполне конкретный пример.

Хотите еще? Ну вот вам реальная функция работающая с полями структуры.

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

//=======================================================================
// Процедура проверки займов
//=======================================================================
dcl-proc chkCre;
  dcl-pi *n char(1);
    dsCPDA       likeds(t_qdsCPDAR);
    dsSettEntry  likeds(t_dsSettingsEntry);
  end-pi;

  dcl-f SC20LF    disk(*ext) 
                  usage(*input)
                  block(*yes)
                  keyed 
                  usropn 
                  qualified
                  static;

  dcl-ds dsSC       likerec(SC20LF.SCPFR: *all);
  dcl-s  limit      packed(63: 0)  inz(*zero);
  dcl-s  SummR      packed(63: 0);
  dcl-s  res        char(1)        inz('Y');
  dcl-s  currRte    like(t_dsC8PF.C8SPT);

  if not %open(SC20LF);
    open SC20LF;
  endif;
  
  // Получить записи из таблицы счетов (SCPF) по УИК
  setll dsCPDA.CPDACPNC SC20LF;
  
  read SC20LF.SCPFR dsSC;
  
  dow not %eof(SC20LF) and
      dsSC.SCAN = dsCPDA.CPDACPNC;
    // Рассчитать сумму займа, сконвертированную в рублёвый эквивалент 
    // на счетах, тип которых (SCACT) входит в список
    if %lookup(dsSC.SCACT: dsSettEntry.arrAct: 1: dsSettEntry.cntAct) > 0;
      if dsSC.SCCCY <> currRoubles;
        currRte = cpoGetCurrRte(dsSC.SCCCY);
        
        SummR = dsSC.SCBAL * currRte;
        limit += SummR;
      else;
        limit += dsSC.SCBAL;
      endif;

      // Если превысили лимит - устанавливаем код и флаг и выходим
      if limit > dsSettEntry.limitSum;
        res = 'N';
        leave;
      endif;
    endif;

    read SC20LF.SCPFR dsSC;
  enddo;

  return res;

end-proc;

Суть - проверка суммы займов клиента по определенным типам счетов на превышение заданного лимита. На вход приходит две структуры - dsCPDA ("дневной агрегат клиента") и dsSettEntry (настройки проверки). Это кусок блока проверок в системе контроля платежей.

dcl-f SC20LF    disk(*ext)
                ...

Объявление файла SC20LF - индекс по полю CPNC (уникальный идентификатор клиента) в таблице счетов

dcl-ds dsSC       likerec(SC20LF.SCPFR: *all);

Объявление структуры "такой же как структура записи в таблице SCPF". Т.е. это будет структура у которой имена и типы полей совпадают с именами и типами полей в таблице.

Нас тут интересуют поля

  • dsSC.SCAN - УИК клиента (char(6))

  • dsSC.SCACT - тип счета (char(2))

  • dsSC.SCCCY - валюта счета (char(3))

  • dsSC.SCBAL - баланс по счету (packed(23:0) - аналог decimal(23,0) в SQL)

setll dsCPDA.CPDACPNC SC20LF;

Установка курсора перед первой записью с заданным значением индекса (в данном случае значение поля dsCPDA.CPDACPNC входной структуры)

read SC20LF.SCPFR dsSC;

Чтение очередной записи, перевод курсора на следующую запись

dow not %eof(SC20LF) and
      dsSC.SCAN = dsCPDA.CPDACPNC;

цикл "пока значение поля dsSC.SCAN в прочитанной записи равно заданному значению dsCPDA.CPDACPNC (т.е. пока счет нужного нам клиента) или пока не достигнем конца файла".

dsSettEntry.arrAct - массив интересующих нас типов счетов

%lookup(dsSC.SCACT: dsSettEntry.arrAct: 1: dsSettEntry.cntAct)

Проверка что счет в прочитанной записи входит в список интересующих нас (по типу счета)

Если валюта счета не рубли - получаем текущий курс и конвертируем в рубли.

Считаем суммарный баланс. Если он превысил заданный лимит (dsSettEntry.limitSum) - выходим с флагом "превышение заданного лимита" (нас интересует только это, конкретное значение суммы по счетам нам не нужно в данном случае, только "больше-меньше заданного порога").

Как видите, все очень просто. Никакие "объекты" тут не нужны.

Это объект с описанием этой структуры для того, чтобы обращаться к текущей структуре и к ее полям. .

Объясните дураку - зачем "объект с описанием этой структуры для того, чтобы обращаться к текущей структуре и к ее полям", если я и так могу обращаться к полям структуры напрямую, безо всяких объектов? Вот просто "чтобы был объект"? Концептуальность ради концептуальности? Лишний уровень абстракции? Сколько строк кода займет у вас описание объекта когда я просто могу написать

dcl-ds dsSC       likerec(SC20LF.SCPFR: *all);

и получить готовую структуру к полям которой я смогу обращаться напрямую после того как прочитаю туда очередную запись

read SC20LF.SCPFR dsSC;

???

Приведите пример вашего кода. Почему-то, мне кажется, что вам не удастся уложить его в две строки :-)

Кто-то же работает с этими полями, кто-то их читает как поля структуры или нет? Или вы структуры просто дальше пересылаете? Вот это очень интересно выяснить.

Ну вот я привел пример как происходит работа с полями структуры в которую прочитана запись. Просто из реальной задачи выбрал то, что попроще. В предыдущем примере просто кода побольше, но суть та же - отдаем структуру (в которую прочитали очередной элемент выборки) в некую функцию, которая уже что-то там делает с полями этой структуры. На самом деле там получается некоторая дополнительная информация по клиенту, формируется XML сообщение которое выкладывается в очередь (MQ) и в соотв. таблицу добавляется запись о том, что этому клиенту послано соотв. уведомление. Расписывать все это тут долго, поэтому нашел пример попроще. И да, его точно также можно написано на SQL вместо прямого чтения (цикл setll/read), но в данном случае прямое чтение работает быстрее и потребляет меньше ресурсов.

И еще раз. Я не против ООП как таковой. Я знаю ООП и умею ООП (я начинал писать на С, потом на С++ практически с первых его версий в начале 90-х). Я лишь против того, чтобы пихать какую-то одну концепцию везде без оглядки на то, будет она там эффективно работать или нет.

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

Если у нас будет объект, то нам придется сначала прочитать запись в буфер, потом этот буфер передать в объект чтобы он заполнил свои внутренние поля. Вот вам лишняя операция.

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

Дальше. У вас получается гораздо круче, чем просто использование ООП, у вас целый язык (самодельный?) для работы с базой данных. Я конечно этого не ожидал! Я не ожидал что в ваших примерах можно найти работу с полями прочитанных записей, и, соответственно, не пытался это найти, а тем более анализировать.

Теперь попробую, но это требует времени.

работаем с этими записями-структурами через объект-указатель на текущую запись

Еще раз - зачем? У меня есть массив структур и я могу к любому элементу массива (и любому полю этого элемента) обратится напрямую. Зачем мне объект для этого?

Дальше. У вас получается гораздо круче, чем просто использование ООП, у вас целый язык (самодельный?) для работы с базой данных

Это не самодельный язык. Это специализированный язык для реализации коммерческой бизнес-логики. Мы работаем на платформе IBM i (бывш. AS/400). И там есть язык RPG (на самом деле очень старый язык, чуть не ровесник COBOL, но он до сих пор развивается на этой платформе и его современный диалект суть нормальный процедурный язык, но с кучей специфики именно для работы с БД и разных коммерческих вычислений).

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

Прочитал этот тред и рядом, понял что мы решаем разные задачи)

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

Это не наша забота. Мы - мастер-система. Ядро АБС банка. Наша задача собрать данные и выложить их в очередь

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

Все это не более чем общие слова.

Для Вас они звучат так, потому что вы с такими задачами просто и не сталкиваетесь. Вам нужно взять данные из талицы, возможно слегка трансформировать и передать дальше. Тут даже на уровне терминов нет ничего общего)

Еще раз тезисно.

Тем не менее все обсуждения из-за того, что вы выступаете с ярой критикой ООП, примеряя всё только на свой контекст и свои задачи.

Во-первых, я критикую только то, что ООП пытаются притянуть туда, где он не нужен.

Для Вас они звучат так, потому что вы с такими задачами просто и не сталкиваетесь.

Во-вторых, я уже писал, что знаю и умею ООП - с 1991-го, когда ушел в разработку профессионально, и по 2017-й основным языком был С (сначала) и С++ (с тех пор как он появился у нас).

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

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

В-третьих, я отметил манипулятивность статьи. Взяли спагетти-говнокод, переписали его на ООП и восхитились как стало красиво и структурировано. Но чтобы структурировать код ООП не нужен, я тому привел пример - как это можно (не говорю что нужно, но можно) сделать и без ООП.

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

Если при этом вы можете выделить для нескольких типов объектов набор общих свойств и логики поведения - это тоже про ООП - базовые классы и наследование от них.

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

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

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

В-третьих, я отметил манипулятивность статьи. Взяли спагетти-говнокод, переписали его на ООП и восхитились как стало красиво и структурировано

Только не понятно, в чем же манипуляция, и где вы увидели восхищение? Я же там написал, что кто-то сможет лучше, вроде?

Я просто написал пример того, что можно рассматривать как ООП подход, только это реально работающий пример, который можно потрогать руками. И я надеюсь достаточно интересный пример. А про ООП и про чистый код – это даже не я написал, я же дал ссылку на другую статью в которой написано примерно то же самое только для другого примера. По-моему, это вы здесь передергиваете по поводу манипуляции!

И я вижу, что наши взгляды на то, что считать структурированным кодом сильно расходятся, но может быть в наш век разнообразия и инклюзивности это не так уж и плохо? Вы ошибаетесь если считаете, что я пытаюсь как-то ограничить вас в правах выбирать ваш путь структурирования кода! Как говорится ваш код – вам и структурировать! Мне казалось, что статья эта про то, что ООП не создает проблем с производительностью, при правильном применении. А при неправильном … , я уже писал, повторю: с дуру можно что хочешь сломать.

Не понятно, как у вас получается прочитать о том, чего там нет. Зато теперь я могу утешить себя мыслью, что те, кто ставят минусы, ставят их за то, что они додумали в ходе чтения этой статьи, а не за то, что они на самом деле прочитали.

Да - забыл поблагодарить за новую реализацию задачи!

не совсем понятно как вот тут:

  // Хотим чтобы деление всегда было "нацело"
  *a = (*a / *b) * *b;

получится то что записано в коментарии к коду, но, в любом случае

, это очень интересно для меня, это то ради чего я писал статью, в том числе - услышать чужое мнение-интерпретацию! спасибо!

не совсем понятно как вот тут:

получится то что записано в коментарии к коду

А очень просто. Вот пример

25 / 7 = 3.57....

Но, поскольку у нас аргументы целые и результат целый, то дробная часть будет проигнорирована

int a = 25, b = 7, c;

c = a / b; // c = 3

Таким образом,

a = a / b; // a = 25 / 7 = 3 в целочисленной арифметике
a = a * b; // a = 3 * 7 = 21

Т.е. корректируем a так, чтобы оно делилось на b нацело. Фактически - ближайшее меньшее изначально сгенерированного, нацело делящееся на b.

Т.е. корректируем a так, чтобы оно делилось на b нацело. Фактически - ближайшее меньшее изначально сгенерированного, нацело делящееся на b.

Можно быстрее:

*a -= *a % *b;

По поводу анекдота. И производительности.

Коллеги, сервис *** за последние 5 недель увеличил потребление процессорных ресурсов в 3 раза!!!
Он уже является 2-м по величине сервисом после *****.
В качестве альтернативы мы рассматриваем перенос запуска сервиса на резервный сервер, но там есть лаг по отставанию до 10 мин.
Заказчикам сервиса это может не понравиться :(

Это реальное письмо от сопровождения. Увеличение потребления ресурсов связано с увеличением объема данных с которыми работает данный сервис. Т.е. обычное масштабирование - количество клиентов растет, соответственно растут объемы данных. И в какой-то момент то, что удовлетворительно (по времени и процессору) работало многие годы, перестает удовлетворять. Вплоть до того, что оно может начать тормозить какие-то другие процессы, а дальше уже может начаться лавинообразный процесс торможения всего и вся.

На такие вещи у нас заводится "дефект производительности на промсреде". А дефект - это значит что откладываем все остальное и правим его.

И это не анекдот, увы. Это реальная жизнь у нас. Так что вся концептуальность и уровни абстракции идут лесом если они привносят лишние уровни стека, динамическую работу с памятью и все вот это вот.

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

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

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

Абсолютно согласен. Особенно с последним абзацем. Искусство программирования это, во многом, искусство компромиса.

Верно. Компромисса и умения отделять главное от второстепенного.

А где та грань, которая определяет резонность использования ООП? Не всегда можно систему написать двумя способами и проверить как оно будет работать. Может есть косвенные признаки или даже сферы применения в которые не стоит использовать ООП?

Вопрос сложный на самом деле. Сродни тому, что описывал Л.Андреев в рассказе "Правила добра"

Если говорить в целом, то надо понимать что вам в данном случае даст использование ООП и чем за это придется заплатить (бесплатного ничего не бывает). И не будет ли плата чрезмерной в данном конкретном случае.

rukhi7 справедливо указывал на то, что когда вы читаете запись из БД в сыром виде и для работы с ней требуется какой-то маппинг, то, наверное, будет обоснованно создавать "объект с описанием этой структуры для того, чтобы обращаться к текущей структуре и к ее полям".

Если бы в тех примерах, которые я привожу, каждый раз требовался бы какой-то маппинг, наверное, имело бы смысл сделать базовый класс "таблица" (с операциями позиционирования, чтения, записи), наследовать от него конкретные классы под конкретные таблицы с конкретной структурой и потом с ними работать.

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

Допустим, у вас есть строка (просто массив char) и вам нужно удалить из нее лишние пробелы, то, казалось бы, имеет смысл запихнуть ее в объект типа string и вызвать соотв. метод. А если язык уже предоставляет вам богатый набор средств работы со строками и вы можете сделать тоже самое просто написав

str = %concatarr(' ': %split(str: ' '));

то напрашивается вопрос - зачем вам лишний объект когда можно обойтись без него ("бритва Оккама")?

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

А если при наследовании вам придется каждый раз в наследуемом классе прописывать 80-90% логики (или вообще каждый раз писать новый класс) - чем это будет лучше обычного процедурного подхода? Где вы сэкономите?

А если еще потом потребуется доработка логики где-то в базовом классе, вы рискуете еще и в интеграции проблем огрести.

Писать "хороший" или "плохой" код можно в любой парадигме. Это зависит от вашей квалификации но не от используемого подхода.

Хороший код - это прежде всего простой и легко понимаемый код. Который потом легко модифицировать локально. А если у вас чтобы понять что делает (во всех подробностях) одна строчка кода (вызов какого-то метода какого-то класса) нужно прошерстить 3-5-10 исходников, продираясь сквозь сложную структуру наследования - это не очень хороший код. Особенно, если все это можно уложить в одну процедуру на экран-полтора и там все разу будет понятно.

Так что все от задачи и по ситуации. Однозначного ответа тут нет и быть не может. Нет "серебряной пули", нужно учится мыслить гибко и избавляться от догм.

А крокодилы более длинные, чем зелёные

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

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

ООП сложен. Сложен в понимании и проектировании. Вот эти интерфейсы и абстрактные классы, с кошками и утками, которые двигаются но не все плавают... Размазывание логики по классам и файлам, где есть this.value=value; и метод calculate(); Просто когда Вы пишете на Java или C# - у Вас нет выбора.

Кому-то ультимативный подход "всё есть объект" не нравится. И так появился Kotlin. Другие отказались от классического наследования и появился Rust и Go.

Так можно же писать на шарпе и не использовать ООП (если не считать, что все неявно наследуется от object). Это скорее опциональная фича, чем нечто прибитое гвоздями.

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

from random import randint

Operation = {
    "-": lambda a, b: (a - b, f"{a}-{b}"),
    "+": lambda a, b: (a + b, f"{a}+{b}"),
    "*": lambda a, b: (a * b, f"{a}*{b}"),
}

Spliter = "-" * 50

def randomOp():
    randomIndex = randint(0, len(Operation) - 1)
    opView = list(Operation.keys())[randomIndex]
    return opView, Operation[opView]

def output(*msgs):
    print("\n".join(map(str, msgs)))

def sayHello():
    output(
        "Hello, colleague!",
        'To stop the program type "q" in answer.',
        Spliter,
    )

def testUser(calls):
    opView, op = randomOp()
    calls[opView] += 1

    a = randint(2, 10)
    b = randint(2, 10)
    opResult, opRepr = op(a, b)

    inputString = input(f'"Please Solve: {opRepr} = <..>"')
    if inputString == "q":
        return None, None

    userAnswer = int(inputString)
    return userAnswer, opResult

def giveFeedback(userAnswer, opResult):
    output(
        "Congratulations it is correct answer!"
        if userAnswer == opResult
        else f'"{userAnswer}" is wrong answer. Correct is {opResult}',
        Spliter,
    )

def sayGoodby(calls):
    resume = [f"Count of {opView} operations={n}" for opView, n in calls.items()]
    output(Spliter, *resume, Spliter)

def main():
    sayHello()

    calls = {o: 0 for o in Operation}

    continueTesting = True
    while continueTesting:
        userAnswer, opResult = testUser(calls)
        continueTesting = userAnswer != None
        if continueTesting:
            giveFeedback(userAnswer, opResult)

    sayGoodby(calls)

main()

Примерно в таком стиле я пишу свой последний проект, работа над которым идет уже более 3-х месяцев, но финал виден на горизонте. Сейчас в проекте около 15 000 строк кода, а в ходе разработки и экспериментов было выброшено в разы больше (мне так кажется, не судите строго). Взявшись за этот проект, я решил отбросить условности, которые создавали мне дискомфорт во время разработки и тормозили работу, в итоге мой стиль, может быть, несколько и вышел за рамки дозволенного высоколобыми энтерпрайз-разработчиками, на работа спорится. Чего и вам желаю.

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

Разве "писать проще" - это не то, к чему нужно стремиться?..

Да, но при соблюдении прочих условий.

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

"Писать проще", "писать быстрее" - это все мы делаем для себя. Чтобы потом проще было сопровождать, расширять или дорабатывать функционал и все такое прочее.

Но платит нам в конечном итоге заказчик. А ему все равно - ООП там, не ООП...

Разве я не об этом же?

Ну не совсем :-)

Если выбирать между "просто, но не медленно работает" и "сложно, но быстро работает" в наших условиях приходится выбирать второе...

Вы считаете, что мой код будет работать медленнее, чем код, написанный в стиле кода из статьи?.. И мне почему-то кажется, что вы для себя решили, что моя позиция заключается в том, что "пусть медленно, лишь бы просто". Не думаю, что давал повод для такого умозаключения. Мой код обычно работает быстро, поскольку, как правило, он опирается на хорошие алгоритмы. И при этом, иногда, мне удается сделать его хорошо читаемым и легко сопровождаемым во всех смыслах.

Я ничего не могу сказать по поводу конкретного кода без прогона его через профайлер.

Я просто говорю что часто видел как ООП код с несколькими уровнями абстракции работал медленнее простого процедурного кода. За счет динамического выделения памяти, за счет вызовов конструкторов (развертка-свертка стека тоже занимает некоторое время).

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

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

Такой подход не дает ровным счетом ничего.

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

И опять мне хочется вопросить: "разве я не об этом же"?

Ну вообще в статье был посыл "как здорово ООП".

Я говорю что иногда здорово, иногда нет. И чистый код можно и без ООП писать.

А если смотреть еще шире, то ООП жто не обязательно классы, наследование полиморфизм и инкапсуляция.

Мы вот рвботаем на платформе где все есть объект. На уровне ОС. Но ни наследования ни полимрфизма тут нет. Просто любая сущность рассматривается как объкт того или иного типа, обладающая неким набором атрибутов. И для каждого типа объектов определен свой набор допустимых для данного типа действий.

Все относительно. Можно написать просто, а можно сложно. Причем, оба варианта будут иметь право на жизнь.
Типа, усложнить проект, но получить удобную площадку для расширения функционала.
Или не усложнять там, где "сделал и забыл" и даже примерно не нужно будет ничего расширять в будущем.

"Можно написать просто, а можно сложно". Мой опыт говорит мне, что писать сложно - это просто. А писать просто - это трудно. Не настаиваю, у каждого свой опыт.

А вот 100500 базовых классов там, где можно написать пару-тройку процедур - это "просто" или "сложно"?

Причем, процедуры будут логически атомарными - каждая выполняет свою логически законченную функцию.

А базовый класс не выполняет ничего. От него потом еще надо наследоваться для получения нужного функционала.

Не совсем понял вопрос. Вообще, мы пишем сервисами, где ты ему А, он это как-то обрабатывает и отдает (или не отдает) тебе Б. Объекты используются чисто для хранения и передачи состояния и никакой логикой не владеют. Есть пара мест, где внутри сервисов скрывается ООП, чтобы несколько упростить кодовую базу и не писать одно и тоже десять раз.

Сервисы, соответственно, содержат в себе минимальный функционал, только то, что относится непосредственно к ним и прочий solid.

Имхо, логику в ООП вообще опасно пихать. Она любит подкидывать проблем при дальнейшем развитии.

Имхо, логику в ООП вообще опасно пихать. Она любит подкидывать проблем при дальнейшем развитии.

Вроде бы считается, что ООП - это инкапсуляция данных и логики, которыми она обрабатывается.

Объекты используются чисто для хранения и передачи состояния и никакой логикой не владеют

Тогда это не объект класса, а просто структура. Которая есть просто область памяти, условно поделенная на поля.

У нас вот есть embedded sql где можно написать запрос и сказать - вот тебе массив структур из 1000 элементов, заполни его очередными 1000 строками выборки. И он заполнит. Массив структур выделяется статически, в момент компиляации. Никаких конструкторов там нет, никакого динамического выделения памяти там нет.

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

как-то так:

      dcl-c sqlRows     const(1000);

      dcl-ds t_dsSQLData qualified template;
        CUS    char(6)     inz;
        CLC    char(6)     inz;
        ClTp   char(1)     inz;
        ActDte zoned(7: 0) inz;
        MsgTp  char(1)     inz;
      end-ds;

....
      // массив структур куда будем читать
      dcl-ds dsSQLData  likeds(t_dsSQLData) dim(sqlRows);

      // определение выборки (SQL)
      exec sql declare curRDMS09Clients cursor for ... (тут тело запроса на пару экранов) ...

....

      dou lastBlock;
        // Читаем очередной блок записей
        exec sql fetch curRDMS09Clients for :sqlRows rows into :dsSQLData;

        // определение сколько записей реально прочитано
        // и есть ли еще записи в выборке
        lastBlock = sqlGetRows(rowsRead);

        for row = 1 to rowsRead;
          // Обработка очередного элемента
          procClient(dsSQLData(row));
        endfor;
      enddo;

Вот как-то так... Просто и никаких лишних телодвижений. Куда тут ООП? Создавать объект класса из каждого элемента массива dsSQLData? А зачем?

Должен поправить код. Отходил от компьютера, поэтому не смог сделать это сразу, как сообразил.

from random import randint

Operation = {
    "-": lambda a, b: (a - b, f"{a}-{b}"),
    "+": lambda a, b: (a + b, f"{a}+{b}"),
    "*": lambda a, b: (a * b, f"{a}*{b}"),
}

Spliter = "-" * 50

def randomOp():
    randomIndex = randint(0, len(Operation) - 1)
    opView = list(Operation.keys())[randomIndex]
    return opView, Operation[opView]

def output(*msgs):
    print("\n".join(map(str, msgs)))

def sayHello():
    output(
        "Hello, colleague!",
        'To stop the program type "q" in answer.',
        Spliter,
    )

def testUser():
    opView, op = randomOp()

    a = randint(2, 10)
    b = randint(2, 10)
    opResult, opRepr = op(a, b)

    inputString = input(f"Please Solve: {opRepr} = <..>: ")
    if inputString == "q":
        return None, None, None

    userAnswer = int(inputString)
    return opView, userAnswer, opResult

def giveFeedback(userAnswer, opResult):
    output(
        "Congratulations it is correct answer!"
        if userAnswer == opResult
        else f'"{userAnswer}" is wrong answer. Correct is {opResult}',
        Spliter,
    )

def sayGoodby(stat):
    output(Spliter, *stat, Spliter)

def main():
    sayHello()

    calls = {o: 0 for o in Operation}

    continueTesting = True
    while continueTesting:
        opView, userAnswer, opResult = testUser()
        continueTesting = opView != None
        if continueTesting:
            calls[opView] += 1
            giveFeedback(userAnswer, opResult)

    stat = [f"Count of {opView} operations={n}" for opView, n in calls.items()]
    sayGoodby(stat)

main()

Надо бы еще заняться неймингом, но в данном контексте это уже будет перебор, пожалуй.

Да нормально все у вас с неймингом, я вообще никогда не писал на Питоне, но даже мне все понятно.

Вот только… У меня там разрядность разная была у чисел для умножения и для сложения-вычитания, для сложения используются числа меньше ста, для умножения меньше десяти. И вы это, видимо, недосмотрели.

Но я вам даже могу посоветовать, как это поправить: надо кроме lambda a, b

еще константу (DigitCount) как-то засунуть в объект, который вы из мапа достаете, и вместо

a = randint(2, 10)

 написать что-то вроде:

a = randint(2, <op.DigitCount>)

Еще у вас там отрицательные числа могут получаться при вычитании, а это не всегда подходит.

А еще ужасно интересно почему же вы постеснялись написать операцию «деление»? Не получилась? Или в одну строчку не убралась?

Ну и вопрос на засыпку: получится у вас с вашим подходом расширить программу чтобы поддерживать составные операции, например:

2 + 3 * 7 = <..> или (2 + 3) * 7 = <..>

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

@SpiderEkb

Судя по тому, что вы написали, у вас корнем иерархии является «клиент», а насколько я знаю-понимаю банковскую специфику, правильным выбором для корня иерархии там обычно является «Счет». Я понимаю, что это практически нельзя уже исправить в работающей системе, но вы хотя бы гипотетически рассматривали, что бы было, если бы у вас иерархия была построена от другого корня?

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

а насколько я знаю-понимаю банковскую специфику, правильным выбором для корня иерархии там обычно является «Счет»

Нет.

Счет - это самостоятельная сущность. Оно не может быть корнем иерархии. У клиента, как правило, не один счет. Клиент является постоянным и неизменным. Счета могут открываться и закрываться.

У клиента, в свою очередь, огромное количество данных, которые к счетам вообще никакого отношения не имеют.

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

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

Да нет у нас никакой ООП иерархии. Это траты времени впустую.

Есть набор таблиц (несколько десятков тысяч) где разложены данные по темам. Клиенты, карточки клиентов, адреса клиентов, документы, доверенности, субъекты клиентов и т.п. Есть таблицы счетов, есть таблицы платежных документов, таблицы рисков клиентов. Есть таблицы пластиковых карт, связи между клиентами-владельцами счетов и держателями карт. Есть таблицы системы комплаенс - там всякие списки росфина, стоплисты и т.д. и т.п. Куча справочных и настроечных таблиц. Достаточно количество "витрин" где хранится наиболее плотно востребованная актуальная информация по разным темам (то, что можно быстро взять из одной таблицы а не собирать каждый раз по нескольким).

И, строго говоря, процентов 70-80 работы банка - это не работа с объектами (клиент, счет и т.п.), но работа с некоторыми выборками данных. Т.е. то, что описывается не функциональной моделью, но моделью потока данных.

И конкретный состав выборки определяется конкретным бизнес-процессом (которых у нас десятки тысяч, которые могут добавляться, меняться...).

Я уже приводил пример выше - делаем выборку конкретных данных по конкретным условиям и отдаем ее на обработку. Вот так это работает.

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

Я уже говорил что раньше работал с простоя ситемой. Там да - есть корневой тип "обьъект" (улица-дом-подъезд...) и есть корневой тип "устройство" (конечное устройство - контроллер нижнего уровня - контроллер верхнего уровня).

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

Все! Конечно тут можно построить ООП модель которая будет проста и непротиворечива.

В банке этого нет. Все слишком сложно и запутано. Даже если вы создадите объект типа клиент, то в реальной работе оно вам никак не поможет. Ну разве что при работе с карточкой клиента когда вам нужно работать с клиентскими данными по конкретному ПИНу.

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

В общем, это не тот случай, где ООП что-то реальное может дать. Затраты на разработку иерархии не окупятся.

Сказать по правде, первое же, что пришло мне в голову в ходе прочтения статьи — хорошо известная цитата создателя термина "object-oriented" о языке C++ (а C# в этом смысле ничем от C++ не отличается). Вспомнили? Или не слышали/читали ранее? Добро пожаловать: Alan Kay «Actually I made up the term "object-oriented", and I can tell you I did not have C++ in mind»

Это ООП ради ООП. Во имя “священной” “чистоты” кода. Тут же сразу вспомнился и ещё один позабытый уже многими мем об эволюции "Hello world” от одной строчки неофита на BASIC и до монстра на полторы сотни строк энтерпрайзного плюсатого ООП.

Это ООП ради ООП. Во имя “священной” “чистоты” кода. Тут же сразу вспомнился и ещё один позабытый уже многими мем об эволюции "Hello world” от одной строчки неофита на BASIC и до монстра на полторы сотни строк энтерпрайзного плюсатого ООП.

Вот именно против этого я и пытаюсь возразить. Против концептуальности ради самой концептуальности ценой снижения прозрачности и эффективности кода.

Когда вам приходит на доработку некий код, вы видите в нем вызов какого метода какого-то объекта и вам нужно понять что же там происходит внутри. Лезете внутрь метода и там видите еще пару методов еще каких-то объектов, которые создаются внутри этого метода. И вам приходится лезть все глубже и глубже. А в итоге оказывается что все это можно было сделать в рамках одной процедуры на полэкрана и в рамках одного уровня стека. И без создания каких-то дополнительных объектов с коротким временем жизни. И где вся логика выхватывается одним беглым взглядом.

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

Нет. Причин несколько:

  • Клиентов у нас порядка 50млн. Не факт что именно это клиент сегодня будет задействован. Как определить время жизни такого объекта?

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

  • Остатки на счетах могут постоянно меняться - кеширование тут не сработает. Кроме того, для одного бизнес-процесса могут потребоваться сумма остатков по одним типам счетов, для другого - по другим. Для третьего - сумма остатков по счетам где баланс не превышает 1000р (например). Каждый раз переписывать класс клиента? Нерационально.

  • Вообще если привязывать к объекту какие-то конкретные функции, вам придется написать их тысячи. Разных. Под разные бизнес-процессы и разные условия.

Я уже писал - мы практически не работаем с объектами типа клиент, счет и т.п. как с таковыми. Мы работаем с наборами данных, сформированными по конкретным условиям. Где-то это какие-то остатки по счетам для клиентов, попадающих под какие-то условия. Где-то это список счетов, удовлетворяющих каким-то условиям (например, счета под автоматическое закрытие). Где-то это платежный документ, который прогоняется через серию различных проверок. Где-то еще что-то.

Для каждого процесса своя выборка. И время жизни выборки крайне мало - выбрали, обработали, забыли. Операции с выборкой везде свои. И в каждом процессе в любой момент могут поменяться бизнес-условия.

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

Чтобы понимать масштабы:

  • 27 тыс. программных объектов

  • 15 тыс. таблиц и индексов базы данных

  • Более 150 программных комплексов ("комплекс" - свой набор таблиц и программ под какую-то конкретную задачу)

  • Занимает более 30 Терабайт дискового пространства

  • В день изменяется более 1,5 Терабайт информации в БД

  • За день выполняется более 100 млн. бизнес операций

  • Одновременно обслуживается более 10 тыс. процессов (и везде своя логика, везде свои наборы данных)

  • За год происходит более 1100 изменений в алгоритмах программ (и тут важно не порушить интеграцию - изменения в одном процессе не должны повлиять на другие, все процессы изолированы)

И это только то, что работает на центральных серверах банка.

Все примеры, что я тут приводил - это капля в общем море того, что постоянно происходит в системе.

Вы работает с языком, который навязывает вам ООП парадигму. И решаете те задачи, в которых она эффективно применима. Мы работаем в другой парадигме, с другим языком, который предназначен для эффективного решения наших задач. И да, у нас есть С++, но для наших задач он не столь эффективен.

Нет. Причин несколько:

«Нет» – это ваш ответ в рамках вашей работы с вашей системой. И я вполне допускаю что этот ответ вполне обоснован. Я не собираюсь вам что-то возражать по поводу вашей работы.

Но этот ответ никак не влияет на мой ответ в рамках моей работы с моей системой, мой ответ остается «Да». У меня есть примерно все те же проблемы, которые вы перечислили и у меня получается их разделить на уровни и таким образом сократить их количество и где-то решать глобально, а где-то оперируя деталями разного уровня.

Моя система ненамного меньше в плане масштаба. У меня в системе проходит 2-2.5 мегабайта в секунду через один интерфейс. Да и в банке я когда-то работал.

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

PS

Напишите хороший, структурированный пример с использованием процедурного подхода

я не могу это написать, потому что это то, что пропагандируете вы, а не я.

Потом я уже написал замечания и даже исправления для кода предложенного

@iamawriter после чего, видимо, он сильно расстроился и пропал, хотя у меня есть ощущение что он нас подслушивает :) .

Я, честно говоря, не совсем понимаю, что вы под этим «процедурным подходом» имеете в виду, потому что

Во первых, вы тоже критиковали код, который написал @iamawriter (как мне кажется),

Во вторых, вряд ли мне в этом поможет предложенное вами «Программирование для математиков».

Но было бы очень интересно посмотреть, как бы вы этот «процедурный подход» сформулировали-оформили для статьи, и как бы вас поддержала аудитория Хабра с этим, вами оформленным «процедурным подходом». Или хоть ссылку дайте, где термин определен корректно, по вашему.

«Нет» – это ваш ответ в рамках вашей работы с вашей системой. И я вполне допускаю что этот ответ вполне обоснован.

Именно так. Я лишь говорю что ООП не всегда и не везде поможет. Не более того.

Моя система ненамного меньше в плане масштаба. У меня в системе проходит 2-2.5 мегабайта в секунду через один интерфейс. Да и в банке я когда-то работал.

Если вы работали в банке на уровне ядра АБС, то должны представлять специфику этой работы, количество одновременно работающих бизнес-процессов и тот факт, что там невозможно выделить какое-то конечное количество базовых классов. Просто потому, что для каждого бизнес-процесса будет свой набор. И они никак не будут пересекаться.

Здесь работа не с объектами, а с потоком данных. Который формируется "на лету" и трансформируется в процессе обработки.

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

На основании чего столь смелое утверждение? Я вот такого не вижу. Я говорил, что у нас есть команда, которая уже лет 7-8 пытается что-то такое сотворить. И ни один PEX (Performance EXplorer) тест не показал сколь видимого улучшения производительности.

А вот наоборот - запросто. Накладные расходы на создание объектов в больших выборках становятся заметными.

я не могу это написать, потому что это то, что пропагандируете вы, а не я.

Вы взяли плохой спагетти-код, перевели его на ООП и сказали что это доказывает преимущество ООП.

Елси вы хотите сравнивать ООП и процедурный подход, вы должны как минимум иметь одинаковый опыт работы и с тем и с тем. И уметь в процедурной парадигме писать хороший структурированный код. Только тогда вы сможете что-то доказать. Пока что вы пытаетесь доказать что теплое теплее мягкого. Что просто бессмысленно.

typedef struct dsOperation {
  char opSign;
  int (*operation)(int, int);
};

int opAdd(int a, int b) {return a + b;}
int opSub(int a, int b) {return a - b;}
int opMult(int a, int b) {return a * b;}
int opDiv(int a, int b) {return a / b;}

int main()
{
  dsOperation ops[] = {{'+', opAdd}, 
                       {'-', opSub}, 
                       {'*', opMult}, 
                       {'/', opDiv}};
  int opCode;
  int arg1, arg2, res;
  
  opCode = rand() % 4;    // тип операции
  arg1 = rand() % 99 + 1; // аргумент 1 1..99
  arg2 = rand() % 99 + 1; // аргумент 2 1..99

  res = ops[opCode].operation(arg1, arg2);

  printf("%d %c %d = %d\n", arg1, ops[opCode].opSign, arg2, res);
}

Вот вам чисто процедурный подход. Нужно еще операций? Пишите функцию, добавляете в таблицу.

Бантиками, уж извините, не стал оборачивать, только суть. Так, на скорую руку набросал.

Если обратите внимание - никаких конструкторов, все инициализируется на этапе компиляции. И, обратите внимание, ни одного if/switch тут нет. А вот что оно короче - это есть.

А теперь скажите - чем это концептуально хуже того, что вы предложили?

Или хоть ссылку дайте, где термин определен корректно, по вашему.

Да хоть в википедии посмотрите.

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

А вот чтобы адекватно сравнивать нужно хорошо владеть и тем и этим.

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

Про ООП писал уже, повторяться не стану. Опыт есть. Где-то с начала 90-х (когда там Turbo C++ 2.0 появился, уже не помню). И до 2017 активно писал только на ++ и активно использовал ООП (там, где это было оправданно, естественно).

Вот полный вариант:

#include <stdio.h>
#include <stdlib.h>

struct dsOperation {
  char opSign;
  int (*operation)(int, int);
};

struct dsStatistics {
  char opSign;
  int  corrAnsw;
  int  failAnsw;
};

int opAdd(int a, int b) {return a + b;}
int opSub(int a, int b) {return a - b;}
int opMult(int a, int b) {return a * b;}
int opDiv(int a, int b) {return a / b;}

int chkAnswer(int result, char* answer)
{
  int res = 1, answ;
  
  if (answer[0] == 'q') res = 0;
  else {
    answ = atoi(answer);
    if (result != answ) res = -1;
  }
    
  return res;
}

void addStat(dsStatistics* stat, int res)
{
  switch (res) {
    case -1:
      stat->failAnsw++;
      printf("Fail :-(\n");
      break;
    
    case 1:
      stat->corrAnsw++;
      printf("Success :-)\n");
      break;
  }
}

void showStat(int cntTests, dsStatistics* stat, int cntStat)
{
  int i;
  
  printf("Total tests: %d\n", cntTests);
  printf("Results:\n");
  
  for (i = 0; i < cntStat; i++)
    printf("Operation: %c, Correct: %d, Fail: %d\n", stat[i].opSign, stat[i].corrAnsw, stat[i].failAnsw);
}

int main(int argc, char **argv)
{
  dsOperation ops[] = {{'+', opAdd}, 
                       {'-', opSub}, 
                       {'*', opMult}, 
                       {'/', opDiv}};
  dsStatistics stat[] = {{'+', 0, 0}, 
                         {'-', 0, 0}, 
                         {'*', 0, 0}, 
                         {'/', 0, 0}};
  int opCode;
  int arg1, arg2, res;
  int done;
  int cntTests = 0;
  char answ[8];
  
  do {
    opCode = rand() % 4;    // тип операции
    arg1   = rand() % 99 + 1; // аргумент 1 1..99
    arg2   = rand() % 99 + 1; // аргумент 2 1..99

    res = ops[opCode].operation(arg1, arg2);

    printf("%d %c %d = ?\n", arg1, ops[opCode].opSign, arg2);
    printf("Your answer ('q' for exit): ");
    gets(answ);
    
    done = chkAnswer(res, answ);
    
    if (done != 0) {
      addStat(&(stat[opCode]), done);
      cntTests++;
    }
    else showStat(cntTests, stat, 4);
  } while (done != 0);
  
  return 0;
}

Чтобы расширить список возможных операций просто пишем функцию op... для нужной операции и добавляем ее в структуры dsOperation ops[] и dsStatistics stat[]

При желании можно еще структурировать. Например, вынести блок

    opCode = rand() % 4;    // тип операции
    arg1   = rand() % 99 + 1; // аргумент 1 1..99
    arg2   = rand() % 99 + 1; // аргумент 2 1..99

    res = ops[opCode].operation(arg1, arg2);

    printf("%d %c %d = ?\n", arg1, ops[opCode].opSign, arg2);
    printf("Your answer ('q' for exit): ");
    gets(answ);

в отдельную функцию askQuestion... и/или

    opCode = rand() % 4;    // тип операции
    arg1   = rand() % 99 + 1; // аргумент 1 1..99
    arg2   = rand() % 99 + 1; // аргумент 2 1..99

в функцию genArgs (куда можно втащить нужную логику).

Тут уже все фантазией ограничивается.

Обратите внимание на то, что здесь нет лишних вызовов

            MathOperation[] operArr = new MathOperation[] 
            {
                new MulOperation(),
                new DivOperation(),
                new Sub2PosOperation()
            };

Которые, на самом деле приведут к вызову 6-ти (!!!) конструкторов (каждый конструктор наследуемого класса вызовет конструктор базового класса и чем больше цепочка наследования, тем длиннее цепочка вызовов конструкторов) в рантайме.

Это к вопросу о производительности.

И все равно все это будет короче по коду.

:) Вот это нечестный прием :) использовать детскую привязанность оппонента к коду написанному в старом стиле на чистом С :) ! Мне такой код до их пор приходится поддерживать местами, и это меня нисколько не напрягает.

Только вы хотели продемонстрировать некий «процедурный подход». Но то, что вы написали является версией Объектно-Ориентированного Подхода, который, собственно, именно в таком виде и был изобретен, когда С++ еще не существовало, насколько я помню. У нас вместо класса-объекта – структура, вместо виртуальных функций – поля структуры с типом указателя на функцию. Вместо конструктора, код инициализации структуры. Основной принцип я бы сформулировал как: объединение данных и методов объекта внутри одной сущности, структуры в этом случае.

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

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

3 / 7,

 25 / 15, вы не проверяли программу?

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

Вы бы посмотрели, например, реализацию, которую я написал для примера для «вычитания». Зачем там пришлось писать дополнительную логику в виде:

if (op2 > op1)

{ int tmp = op2; op2 = op1; op1 = tmp; }

Почему вы ее выкинули в своей версии?

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

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

Только вы хотели продемонстрировать некий «процедурный подход». Но то, что вы написали является версией Объектно-Ориентированного Подхода, который, собственно, именно в таком виде и был изобретен, когда С++ еще не существовало, насколько я помню. У нас вместо класса-объекта – структура, вместо виртуальных функций – поля структуры с типом указателя на функцию.

Признаком ООП является инкапсуляция (методы + данные с которыми они работают), наследование, полиморфизм. Ни того, ни другого, ни третьего тут нет.

Структура не есть объект. Это просто объединение данных в один блок. Очень странно, если Вы не видите разницы. Структуры были и в С и в Паскале, и даже в КОБОЛе (который вообще в 50-х года прошлого столетия появился).

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

Я точно с такой же легкостью могу разбить все это на отдельные массивы и ничего принципиально не изменится.

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

if (op2 > op1)

{ int tmp = op2; op2 = op1; op1 = tmp; }

Почему вы ее выкинули в своей версии?

Да просто потому, что у меня сейчас в работе три сложных задачи. А это просто писалось на коленке. Кстати, оно компилируется и работает. И, кстати, будучи скомпилированным g++ оно занимает на диске 330кБ и не требует никаких динамических библиотек. Ее даже можно перенести на дискете 5.25" 360кБ :-)

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

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

И опять это ничего не изменит принципиально.

Потом там много чего можно обсуждать, например, как реализовать «операцию», которая требует 3-х операндов

Например?

Мне вот не приходит на ум атомарных операций с тремя параметрами. Есть унарные (например, инверсия знака), есть бинарные (арифметические операции). Все остальное уже решается парсингом в стек и потом его раскруткой (надеюсь, что такое "обратная польская нотация" в курсе?)

на самом деле, достаточно написать пару классов чтобы покрыть большинство возможных вариантов

Если не лень - напишите класс, который будет покрывать все возможные сочетания четырех арифметических операций с учетом возможных скобок. Это как раз то, на что я ссылался у Кушниренко. Когда-то очень давно я это реализовывал в полном объеме - арифметика, функции (тригонометрия и т.п.).

как локализовать код, который относится к одному типу операции

он локализован в функциях

int opAdd(int a, int b) {return a + b;}
int opSub(int a, int b) {return a - b;}
int opMult(int a, int b) {return a * b;}
int opDiv(int a, int b) {return a / b;}

которые могут быть сколь угодно сложными.

можем ли мы расширить код для поддержки операций с числами с десятичной дробной частью заданной точности после запятой

Не проблема, если язык позволяет. На RPG (который на 100% процедурный) я вам могу реализовать все это даже с перезагрузкой, фиксированной точкой и, коль будет нужда, со всякими операциями с округлением.

Например (перезагрузка функций):

// Отправка сообщения в queLIFO/queFIFO очередь
// Возвращает количество отправленных байт
// в случае ошибки -1
dcl-pr USRQ_SendMsg int(10) overload(USRQ_Send: USRQ_SendKey);

dcl-pr USRQ_Send int(10) extproc(*CWIDEN : 'USRQ_Send') ;
  hQueue    int(10)                    value;                                  // handle объекта (возвращается USRQ_Connect)
  pBuffer   char(64000)                options(*varsize);                      // Буфер для отправки
  nBuffLen  int(10)                    value;                                  // Количество байт для отправки
  Error     char(37)                   options(*omit);                         // Ошибка
end-pr;

// Отправка сообщения в queKeyd очередь
// Возвращает количество отправленных байт
// в случае ошибки -1

dcl-pr USRQ_SendKey int(10) extproc(*CWIDEN : 'USRQ_SendKey') ;
  hQueue    int(10)                    value;                                  // handle объекта (возвращается USRQ_Connect)
  pBuffer   char(64000)                options(*varsize);                      // Буфер для отправки
  nBuffLen  int(10)                    value;                                  // Количество байт для отправки
  pKey      char(256)                  const;                                  // Значение ключа сообщения
  nKeyLen   int(10)                    value;                                  // Фактический размер ключа
  Error     char(37)                   options(*omit);                         // Ошибка
end-pr;

Вызываем USRQ_SendMsg, а дальше компилятор по количеству и типам параметров сам подставит вызов USRQ_Send или USRQ_SendKey

Так что это тоже не вопрос ООП или не ООП.

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

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

Вы бы посмотрели, например, реализацию, которую я написал для примера для «вычитания»

Хотите логики в генерации аргументов? Нате вам:

#include <stdio.h>
#include <stdlib.h>

const int argLimit = 99;

struct dsOperation {
  char opSign;
  int  (*operation)(int, int);
  void (*generate)(int*, int*);
  int  corrAnsw;
  int  failAnsw;
};

int opAdd(int a, int b)  {return a + b;}
int opSub(int a, int b)  {return a - b;}
int opMult(int a, int b) {return a * b;}
int opDiv(int a, int b)  {return a / b;}

void genAdd(int* a, int* b) 
{
  *a = rand() % argLimit + 1;
  *b = rand() % argLimit + 1;
}

void genSub(int* a, int* b) 
{
  genAdd(a, b);

  // Чтобы не было отрицательных результатов
  if (*a < *b) {
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
  }
}

void genDiv(int* a, int* b) 
{
  genSub(a, b);

  // Хотим чтобы деление всегда было "нацело"
  *a = (*a / *b) * *b;
}

int chkAnswer(int result, char* answer)
{
  return ((answer[0] == 'q') ? 0 : (result == atoi(answer)) ? 1 : -1);
}

void addStat(dsOperation* op, int res)
{
  switch (res) {
    case -1:
      op->failAnsw++;
      printf("Fail :-(\n");
      break;
    
    case 1:
      op->corrAnsw++;
      printf("Success :-)\n");
      break;
  }
}

void showStat(int cntTests, dsOperation* op, int opCount)
{
  int i;
  
  printf("Total tests: %d\n", cntTests);
  printf("Results:\n");
  
  for (i = 0; i < opCount; i++)
    printf("Operation: %c, Correct: %d, Fail: %d\n", op[i].opSign, op[i].corrAnsw, op[i].failAnsw);
}

int main(int argc, char **argv)
{
  dsOperation ops[] = {{'+', opAdd,  genAdd, 0, 0}, 
                       {'-', opSub,  genSub, 0, 0}, 
                       {'*', opMult, genAdd, 0, 0}, 
                       {'/', opDiv,  genDiv, 0, 0}};
  int  opCode, arg1, arg2, res, done, cntTests = 0, opCount = sizeof(ops) / sizeof(dsOperation);
  char answ[8];
  
  do {
    opCode = rand() % opCount;
    ops[opCode].generate(&arg1, &arg2);
    res = ops[opCode].operation(arg1, arg2);

    printf("%d %c %d = ?\n", arg1, ops[opCode].opSign, arg2);
    printf("Your answer ('q' for exit): ");
    gets(answ);
    
    done = chkAnswer(res, answ);
    
    if (done != 0) {
      addStat(&(ops[opCode]), done);
      cntTests++;
    }
    else showStat(cntTests, ops, opCount);
  } while (done != 0);
  
  return 0;
}

Тут гарантировано не будет отрицательных чисел при вычитании и деление всегда нацело.

98 - 54 = ?
Your answer ('q' for exit): 44
Success :-)
63 + 83 = ?
Your answer ('q' for exit): 146
Success :-)
55 * 35 = ?
Your answer ('q' for exit): 12324
Fail :-(
63 + 30 = ?
Your answer ('q' for exit): 93
Success :-)
97 - 62 = ?
Your answer ('q' for exit): 35
Success :-)
52 / 26 = ?
Your answer ('q' for exit): 2
Success :-)
76 / 19 = ?
Your answer ('q' for exit): 4
Success :-)
42 + 55 = ?
Your answer ('q' for exit): q
Total tests: 7
Results:
Operation: +, Correct: 2, Fail: 0
Operation: -, Correct: 2, Fail: 0
Operation: *, Correct: 0, Fail: 1
Operation: /, Correct: 2, Fail: 0

Кстати, пока конструкторы вызываются один раз при запуске программы, столько же раз, как и инициализация объектов-структур в вашем коде, кстати, это никак не влияет на производительность во время работы программы.

Ой... Вы серьезно не видите разницу между статической инициализацией структуры в момент ее объявления (происходит в момент компиляции) и вызовом конструктора (происходит в процессе выполнения)? Правда?

3 / 7

= 0 т.к. аргументы целочисленные, дробная часть откидывается.

25 / 15

= 1 Аналогично предыдущему.

Но выше и это учтено. Первый аргумент корректируется под второй так, чтобы деление было нацело.

Ну и для вычитания и деления первый аргумент всегда больше второго.

Для сложения и умножения нет допусловий, поэтому используется один и тот же базовый генератор.

Дальше можно накручивать что угодно. И при этом обходится без классов и конструкторов. И код по-прежнему будет структурируемым и легко читаемым.

Перечитал статью еще раз. Вообще говоря, тут налицо манипуляция.

Автор сначала приводи пример плохого, не структурированного кода, а затем пример того же, но уже с некоторой структурой и намеком на то, что кроме как при помощи ООП структурировать код невозможно.

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

А дальше - таблица операций (адресов функций) где (например) 0 - сложение, 1 - вычитание, 2 - умножение, 3 - деление (придумаем что-то еще - просто напишем еще одну реализацию и добавим в таблицу)

Вуаля! Получили структуру. Генерируем тип операции, аргументы, по таблице вызываем функцию - нате вам результат.

Все чисто на процедурах.

Высказывание

Вообще говоря, можно написать классы, которые поддерживают составные операции. Например, такие:

2 + 3 * 7 = <..> или (2 + 3) * 7 = <..>

вообще вызывает улыбку. Сколько классов вы собираетесь писать? Тут два. Но ведь бывает еще (5 - 2) / 7 Или (ужас!) (3 + 4) * (7 - 2) / (3 - 1)... На каждый тип будем писать свой класс? Серьезно?

Такие вещи уже решаются совсем иным способом. Например, написание стекового калькулятора (неплохо описан в старой книге А.Г.Кушниренко, Г.В.Лебедев Программирование для математиков). Можно попробовать реализовать. Хоть на ООП, хоть процедурно, хоть функционально.

Короче говоря, если сравнивать, то сравнимое. Напишите хороший, структурированный пример с использованием процедурного подхода. А потом его аналог с использованием ООП. И разберите по косточкам, постройте дерево всех вызовов - максимальная глубина стека вызовов для процедурного подхода и ООП. Количество операций выделения/освобождения памяти. Размер потребляемой памяти и т.д. и т.п.

Дальше продолжать дискуссию не вижу смысла. Она зашла в тупик на мой взгляд.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории