Попал в подкаст «За чашкой чая», эпизод 16. Можно слушать на любых доступных платформах: Anchor, Apple, Yandex.
Под катом текстовая расшифровка моей части. Хотите — слушайте, хотите — читайте.
против Энтропии
Эффект старой плёнки.
Есть в SAP HCM стандартная транзакция PAR1 (Гибкие данные сотрудников). Но при внедрении у пользователей были хотелки, которые в стандарте невозможны. Скопировали транзакцию в Z-область. Допилили её так, как хотелось пользователям. Добавили инфотипы, ещё кое-что по мелочи. Дело нехитрое.
Затемнение.
Прошло десять лет.
В обычной жизни мы не паримся с памятью в ABAP, сборщика мусора как бы нет, глобальной памяти нет.
В самом простом случае у нас есть то, что называется транзакцией (не путать с транзакцией базы данных), что можно определить как «приложение» на нашем собственно говоря сервере приложений. Транзакция всегда ассоциирована с определённой именованной программой.
И вот пока приложение работает, оно потребляет память в виде переменных, экземпляров классов, массивов (также известных как «внутренние таблицы»). И если посмотреть на стек вызовов, то он упирается во входную точку начала этой программы. Ничего ABAPовского за пределами этой точки не существует. Одно приложение не может получить доступ к данным другого приложения, только через какие-то обходные манёвры, например: через постоянное хранилище (СУБД), через взаимные блокировки и прочие костыли. Как только приложение завершает работу, вся его память высвобождается. А приложения долго не работают, потому что они уже в некоторой степени атомарны (то есть как бы уже не монолит, но ещё не микросервисы).
Есть уже случае чуть посложнее, например: есть веб-сервис, слушающий входящие соединения, или одно «приложение» вызывает другое «приложение» через SUBMIT REPORT . Но подход вы уже уловили, это и называется SAP LUW, то есть Logical Unit of Work. Получается, что стек вызовов и контекст существуют неразрывно только внутри него.
И как обычно бывает, изредка появляется необходимость перепрыгнуть заборы.
Один из известных костылей -оператор IMPORT/EXPORT MEMORY ID, который позволяет перебрасывать переменные (структуры, массивы) между приложениями, но только если они существуют в одной пользовательской сессии. Например Приложение-А вызывает Приложение-Б, Приложение-Б генерирует данные, закидывает их в ячейку памяти X, после чего завершает работу, управление возвращается в Приложение-А, которое считывает данные из ячейки памяти X, профит!
Разговор был бы не так интересен, если бы мы остановились на этом. Но перепрыгнув забор, мы обнаруживаем ещё один забор!
А если мы хотим перебросить данные между разными пользовательскими сессиями?
Для примера кейс будет таким: есть чистый слушающий HTTP-сервис, в котором мы хотим организовать что-то вроде сессии.
HTTP-сессии в ABAP могут быть как stateless, так и stateful. По сути их отличие в том, что этот пресловутый SAP LUW растягивается на всю пользовательскую сессию (stateful) или только на один http-вызов (stateless). В stateful-режиме сервис работает со своими причудами, например: обновлённый код сервиса не подхватывается на лету, чтобы изменения в сервисе вступили в силу нужно завершить сессию. Так же и в обычном ABAPe: пока запущена программа со всеми диалоговыми шагами обновлённый код не подхватывается — необходимо перезайти в транзакцию. Получается в рамках HTTP необходимо сделать log-off, что очень редко случается целенаправленно в жизненных реалиях, гораздо чаще сессия умирает по таймауту.
И вот кривая дорожка сомнительных архитектурных решений привела нас к небольшому челенджу — организовать хранение данных сессии при stateless-сессии HTTP.
Окей, вызов принят. Представим типичный сценарий:
Обыкновенным ABAPом красные места можно тупо реализовать через БД или другой как-мы говорим-у нас-в-деревне «персистент сторэдж», мы же это умеем. Но мы же дошли досюда не за простыми решениями.
Существует возможность создать экземпляр объекта и положить его в какую-то суперглобальную область памяти, существующую вечно внутри сервера приложений. Соответственно, в любой момент можно попробовать взять этот объект во временное пользование (на чтение или на изменение). И это будет тот-же-самый-объект со всеми его атрибутами. Он будет существовать до перезагрузки сервера или до его ручного уничтожения через кнопку в админке (SHMM). Вот вкратце история. А под катом немного подробностей.
При разработке часто бывает необходимость вести некоторые настройки или конфигурации.
На первом уровне это хардкод внутри программы:
Хардкод вполне оправдан, если:
Вторым уровнем может возникнуть классическая настроечная таблица + сгенерированное ведение в транзакции SM30. Впрочем тут тоже можно сделать шаг вправо и влево:
Такие настройки уже вполне можно передать в руки настройщиков и пользователей.
Доходит иногда до смешного, даже в стандарте можно наблюдать целые настроечные таблицы в виде одной колонки («Активировать»), соответственно в таблице только одна строка со значеним — флажок стоит или нет.
Третьим уровнем могут часто возникать разные велосипеды для организации небольших настроек.
Часто можно заметить, что в системе есть много разных стандартных узкоспециализированных хранилищ, которые можно использовать в собственных корыстных целях, например:
Но некоторые приходят к мысли, что следует разработать некоторое универсальное хранилище настроек (Z-разработка), вроде большой таблицы в стиле ключ-значение или даже нечто похожего на реестр Windows, навскидку:
Добро пожаловать под кат, представляю вам свой велосипед, там есть пара занятных моментов. Почему свой? Да потому что у остальных есть фатальный недостаток, очевидно. (далее…)
Устав от простыней в стиле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
LOOP AT SCREEN. IF screen-name = 'ZSS-PERID'. screen-input = 0. ENDIF. IF zss-pernr IS NOT INITIAL. IF screen-name = 'ZSS-PERNR' OR screen-name = 'ZSS-LNAME' OR screen-name = 'ZSS-FNAME'. screen-input = 0. ENDIF. ENDIF. IF screen-name = 'ZSS-STELL'. screen-input = 0. ENDIF. ... IF screen-name = 'ZSS-TAXNM'. IF ... . screen-required = 0. ELSE. screen-required = 1. ENDIF. ENDIF. MODIFY SCREEN. ENDLOOP. ... LOOP AT SCREEN. IF screen-group1 = 'M1'. IF ... screen-input = 0. ELSE. screen-input = 1. ENDIF. MODIFY SCREEN. ENDIF. ENDLOOP. ... |
я решил обкатать новый подход.
Первым делом декларативное определение, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
mt_profile_readonly = VALUE #( ( mask = '*' action = zcl_screen=>readonly ) ). mt_profile_employee = VALUE #( ( mask = '*' action = zcl_screen=>readonly ) ( mask = '*QKTGR*' action = zcl_screen=>obligatory ) ( mask = '*POSID*' action = zcl_screen=>input ) ). mt_profile_contractor = VALUE #( ( mask = '*' action = zcl_screen=>readonly ) ( mask = '*QKTGR*' action = zcl_screen=>input ) ( mask = '*TAXNM*' action = zcl_screen=>obligatory ) ( mask = '*TDATE*' action = zcl_screen=>input ) ). mt_profile_visitor = VALUE #( ( mask = '*' action = zcl_screen=>input ) ( mask = '*PERID*' action = zcl_screen=>invisible ) ( mask = '*PERNR*' action = zcl_screen=>invisible ) ( mask = '*STELL*' action = zcl_screen=>invisible ) ). |
Применение на практике сводится к следующему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
MODULE screen_3000 OUTPUT. go_person_controller->process_screen( ). ENDMODULE. ... METHOD process_screen. IF m_mode = mc_display. zcl_screen=>apply_profile( mt_profile_readonly ). ELSE. IF m_data-pernr IS NOT INITIAL. IF is_employee( EXPORTING iv_pernr = m_data-perid ). zcl_screen=>apply_profile( mt_profile_employee ). ELSE. zcl_screen=>apply_profile( mt_profile_contractor ). ENDIF. ELSE. zcl_screen=>apply_profile( mt_profile_visitor ). ENDIF. ENDIF. ENDMETHOD. |
Пробую пока покатать-проверить, насколько такой подход применим в условиях, приближенным к боевым. Нужно больше мест применения, чтобы определить какой подход даст лучшую читаемость, компактность и проверяемость. И потихотньку можно добавить мяса.
Раннее утро. Птички чирикают.
Сижу, никого не трогаю, читаю утреннюю газету.
SAP вещает:
сморите как всё было плохо, какое всё разное, инконсистенси, то да сё, фу-фу-фу
вот мы вам сделаем Fiori 3, там будет всё такое консистентное, прям ух-ты-красота
современные технологии, двадцать первый век, а о цене давайте не будем говорить
И неожиданно нахлынуло чувство какой-то органичной целостности этого мира.
(далее…)
Простая иллюстрация подхода при передаче параметра в виде ссылки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
DATA lt_messages TYPE bapiret2_tab. DATA(lo_messages) = NEW zcl_message_collection( ir_messages = REF #( lt_messages ) ). if ... MESSAGE e011(zca_test) INTO lo_messages->new_message WITH '#1V1' '#1V2'. lo_messages->add( ). RETURN. endif. if ... MESSAGE w012(zca_test) INTO lo_messages->new_message. lo_messages->add( ). endif. if ... MESSAGE i013(zca_test) INTO lo_messages->new_message. lo_messages->add( ). endif. |
Чего можно таким добиться?
Например: это маленькое ухищрение позволяет однократно указывать обрабатываемую таблицу. Это делает код чуть менее многословным. Альтернатива:
1 2 3 |
APPEND lo_messages->convert_sy2bapi( ) INTO lt_messages. ...или... lo_messages->add( CHANGING messages = lt_messages). |
Насколько это красиво? Чисто и просто… Впрочем, есть тут некоторая неочевидность. Мы запускаем метод в классе без указния локальной переменной в качестве параметра, а эта локальная переменная меняется.
Если собирать ошибки внутри вспомогательного класса, а потом их получать вместе, то становится сложнее прекратить обработку немедленно. Выглядит как-то корявенько:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
DATA(lo_messages) = NEW zcl_message_collection( ). IF ... MESSAGE e011(zca_test) INTO lo_messages->new_message. lo_messages->add( ). lt_messages = lo_messages->get( ). RETURN. ENDIF. IF ... MESSAGE w012(zca_test) INTO lo_messages->new_message. lo_messages->add( ). ENDIF. "finally lt_messages = lo_messages->get( ). |
Пример реализации:
Все события и герои вымышлены. Любые совпадения с реальными личностями случайны.
Акт первый
Консультант: Нам нужно массово списать материалы, но так чтобы в оборотах это было отражено в особом периоде.
Старый фрилансер: Ну дело житейское, разработчика своего попросите.
Консультант: Мы спрашивали, но он сказал, что это невозможно.
Старый фрилансер: Поверь мне, милая, если разработчик говорит, что нечто невозможно, то это значит, что он ленивая жопа.
Консультант: Посмотри, пожалуйста!
Старый фрилансер: Не вопрос, но задачку-то поставь. (далее…)
Как это часто бывает, есть много разных способов решить одну и ту же задачу. И на этот счет тоже есть варианты:
Новый для меня способ — использовать функциональный модуль SMUM_XML_PARSE, у которого очень простой синтаксис: (далее…)
Дано: есть вывод отчета в ALV, в котором стандартными средствами включен расчет среднего значения.
Итоговое среднее значение стандартно расчитывается так:
( 30 + 40 + 50 + 0 ) / 4 = 30
Предположим для примера, что в четвёртой строке нулевое значение связано не с нулевым значением, а с его отсутствием.
Соответственно от заказчика есть задание: требуется выводить среднее значение, не учитывающее пустые (нулевые) значения:
Один из способов решения задачи — грязно вторгнуться во внутреннюю кухню ALV. Приступим?