Ещё по теме

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

«Agile-ра­зра­бо­тки для SAP: освойте методику Scrum!»
Александр Биличенко:
интересная статья, но на мой взгляд слишком обременена ежедневными совещаниями. это большие затраты времени
«Осо­бе­нно­сти удаления бло­ки­ро­вки в SM12»
Олег Точенюк:
Кандидатам на базис или и функциональным консультантам?

База знаний

Как улучшить архитектуру программы, используя class-based exceptions

435
4

Содержание

Введение

Полезные приемы и лучшие практики

Замена возвращаемого параметра исключением

Условия консистентности и дополнительные проверки в методах

Геттеры в условных операторах и условиях выборки

Моментальный выход из программы с возвратом результата

Передача сообщений наверх, а затем оборачивание в BAPIRET

Явная передача сообщения при ошибке в подпрограмме

Ошибка на низком уровне и вопрос о продолжении программы

Обработка маловероятных (редких) ошибок

Обработка критических ошибок

Разделение логики отображения сообщений и генерации ошибок (уменьшение связанности системы)

Делегирование обработки ошибок

Использование особенностей ООП

Заключение

Введение

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

В этой статье я кратко расскажу про использование class-based exceptions, и на примерах рассмотрю, какие задачи они помогают решить, и как их использование способствует улучшению архитектуры приложения по сравнению с другими техниками. Я намеренно опускаю обсуждение целесообразности и преимуществ разных типов исключений, так как эта тема дает начало дискуссиям, которые не есть цель данной статьи. Скажу лишь, что официальная позиция SAP по этому поводу такова:

 “Use class-based exceptions” (https://help.sap.com/http.svc/rc/abapdocu_731_index_htm/7.31/en-US/abenclass_exception_guidl.htm).

Многие опытные разработчики, начинавшие писать на ABAP еще во времена SAP NW 6.4, привыкли к определенным техникам программирования, и боятся или не хотят изучать инструменты, появившиеся в языке с тех пор. Это ограничивает возможность эффективного построения архитектуры приложения лишь набором техник и практик программирования, которые были актуальны более десяти лет назад.

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

В этой статье не будет описано, почему программные ошибки всегда должны быть обработаны. Также не будет рассмотрено, почему нужно обрабатывать технические ошибки и в том случае, когда с точки зрения бизнес-логики такая ошибка не может произойти. Это обширные темы, достойные отдельных статей, к тому же уже не раз обсуждавшиеся. Поэтому я, в предположении, что для читателя вопрос “нужно ли обрабатывать ошибки” решен в пользу здравого смысла,  приступлю к описанию того, как class-based exceptions могут при обработке ошибок помочь.

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

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

В статье я буду ссылаться на теоретические понятия такие, как связанность (coupling), ответственности (concerns), читабельность (readability) и некоторые другие. Я не буду вдаваться в подробности того, что это за сущности, и как именно они определяют хороший код, так как, скорее всего, читателю это известно; если же нет -  информацию можно найти в статьях про основные принципы программирования.

Описанные примеры работают для версий SAP NW 7.X, в более ранних версиях некоторые компоненты отсутствуют.

Полезные приемы и лучшие практики

Замена возвращаемого параметра исключением

Часто возникает необходимость контролировать результат выполнения некой операции в подпрограмме. Часто в подобных случаях используется возвращаемый оператор bool-like типа (вроде ev_error TYPE flag), который содержит информацию о том, было ли действие выполнено корректно, или произошла ошибка. Однако не всегда такое решение оптимально.

Когда такой подход оправдан:

  • удаленный вызов подпрограммы, когда в силу особенностей передающего протокола результат может быть передан только в виде набора данных;
  • проверка истинности, то есть методы вида is_<...>, например, проверка существования номера материала в системе;
  • в случае, когда получение символьного обозначения результата выполнения действия является задачей самой подпрограммы, например, заполнения поля таблицы значениями true/false в зависимости от того, соответствует ли строка заданному условию.

Когда подобного подхода лучше избегать:

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

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

Условия консистентности и дополнительные проверки в методах

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

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

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

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

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

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

Геттеры в условных операторах и условиях выборки

В ABAP методы c returning-параметром возможно использовать в качестве параметров в логических условиях. Это довольно удобно использовать для методов, выполняющих проверки, или для методов, возвращающих определенное значение. И если простейшие варианты просто возвращают хранимое где-то в программе значение, то более сложные выполняют действия, подразумевающие возможность ошибки – например, чтение из БД или арифметические операции.

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

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

  lo_document_list->get_document_item_count(
    EXPORTING
      iv_document_no    = lv_document_no
    IMPORTING
      ev_item_count     = lv_item_count
      ev_document_exist = lv_document_exist
  ).

  IF lv_document_exist = abap_true.
    IF ev_item_count > 5.
      some_action( ).
    ENDIF.
  ELSE.
    MESSAGE 'Документ не найден' TYPE 'E'.
  ENDIF.

Листинг 1. Передача статуса операции через параметр

  TRY.
      IF lo_document_list->get_document_item_count( lv_document_no ) > 5.
        some_action( ).
      ENDIF.
    CATCH zcx_demo_not_found INTO lo_error.
      MESSAGE lo_error TYPE 'E'.
  ENDTRY.

Листинг 2. Исключение в случае ошибки

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

Моментальный выход из программы с возвратом результата

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

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

Пример: ФМ, возвращающий дату предполагаемой отгрузки партии, если настройки не найдены, возвращает дату по умолчанию (текущая дата + n рабочих дней, где n хранится в stvarv), если настройка в stvarv не найдена, возвращает текущую дату + 5, если партии не существует, возвращает пустое поле либо генерирует исключение (в зависимости от подхода).

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

  1. поднимать обработку ошибок (в том числе и проверки на факт ошибки) на самый верхний уровень, где происходит заполнение возвращаемых структур данными;
  2. опускать заполнение выходных структур данными на низкие уровни, где ожидается возникновение ошибки.

В первом случае проблема в том, что, поднимая проверки наверх, вам приходится разделять какие-то атомарные действия (вроде заполнения заголовка данными) на множество маленьких шагов, которые не должны быть разделены на верхнем уровне. Либо, стараясь сохранить уровень абстракции, разработчик применяет проверки, пытающиеся обнаружить ошибки на более низких уровнях по косвенным параметрам. Это приводит к ложным срабатываниям - например, как понять: переменная пуста потому, что запись, найденная в БД по этому ключу, содержит пустое значение для запрашиваемого поля, или потому, что запись с таким ключом не найдена? Ограничения вроде “да в этом поле не может быть пустых значений” создает лишние неявные связи (вроде необходимости явно контролировать пользовательский ввод при заполнении таблицы данными), которые, в конечном итоге, не дают стопроцентной гарантии, что это значение не может быть пустым, но при этом здорово усложняют общую архитектуру проекта. Задача разработчика предусмотреть, в том числе, и те случаи, которые невозможны с точки зрения бизнеса, но возможны технически.

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

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

Это ограничение можно элегантно обойти, используя class-based exceptions. Рассмотрим два простейших варианта, но в зависимости от требований можно пойти и дальше:

1. Создать несколько классов исключений, а именно столько, сколько алгоритмов заполнения выходных данных в случае ошибки существует. Если обратиться к примеру, количество таких алгоритмов - 2 (дефолтная дата из stvarv, дефолтная дата из программы). В этом случае на самом высоком уровне объявляется TRY оператор, который оборачивает всю (или по крайней мере всю уязвимую к таким ошибкам) логику, и в него добавляются CATCH-блоки для каждого типа описанных выше исключений, в каждом из которых осуществляется заполнение согласно соответствующему алгоритму (пример в листинге 3). Этот вариант хорош тем, что позволяет, во-первых, отделить ответственность по заполнению выходных данных при ошибке от всей остальной программы, во-вторых, не замусоривать интерфейсы вызовов подпрограмм передачей ненужных значений, а в-третьих, возможностью красиво инкапсулировать логические части в абстрактные блоки, чтобы удобно работать с ними на всех уровнях.

FUNCTION zfm_demo_001.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  EXPORTING
*"     REFERENCE(EV_DATE) TYPE  DATUM
*"----------------------------------------------------------------------

  TRY .
      PERFORM some_action_1.
      PERFORM some_action_2.
*     ...
      PERFORM some_action_n.

    CATCH zcx_error_one.
      PERFORM get_date_plus_5    CHANGING ev_date.
      RETURN.

    CATCH zcx_error_two.
      PERFORM get_date_from_syst CHANGING ev_date.
      RETURN.

  ENDTRY.

ENDFUNCTION.

Листинг 3. Различные способы заполнения выходных данных в случае ошибки

2. К сожалению, иногда данные, на основе которых будет формироваться выходной параметр, будут доступны только непосредственно после ошибки. Нужно стараться избегать размазывания логики заполнения этих данных по программе и добавления этой логики в другие ответственности. Поэтому необходимо как-то передать эти данные наверх.
Но не стоит забывать, что class-based exceptions являются объектами, а значит, нам доступен весь специфичный объектам функционал. То есть, мы можем реализовать передачу необходимых данных вместе с объектом исключения. Не стоит реализовывать логику заполнения прямо внутри этого объекта, так как вы нарушите единственность его ответственности (он ответственен за информацию об исключительной ситуации). Вместо этого просто передайте необходимые данные в конструктор объекта-исключения (через оператор RAISE EXCEPTION TYPE) и добавьте метод-геттер этих данных, либо имплементируйте классу интерфейс, описывающий логику взаимодействия с этими данными. Используя интерфейс, вы можете, например, передать объект в подпрограмму, которая будет заполнять выходные параметры, и там уже работать с ним как с обычным объектом.
Это позволяет сохранить целостность программы и не оставить логику заполнения выходного параметра изолированной от произошедшей ошибки, что исключит возникновение неявной связанности между этими местами в программе.

В обоих случаях при переходе к class-based exceptions мы добились хороших результатов:

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

Передача сообщений наверх, а затем оборачивание в BAPIRET

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

Зачастую процесс, выполняемый ФМ, состоит из множества шагов, и на каждом шаге может возникнуть ошибка, из-за которой продолжение обработки невозможно. Не самое оптимальное решение, которое, тем не менее, часто встречается - проверка результата после каждого шага и ручное заполнение структуры BAPIRET (обычно через MESSAGE в dummy-переменную, а затем копирование из sy в BAPIRET), добавление ее к выходной таблице, и выход из ФМ.

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

1. На первом уровне ФМ оберните весь код конструкцией вроде этой (см. листинг 4):

  DATA:
    lo_error        TYPE REF TO cx_root,
    lo_message_list TYPE REF TO if_reca_message_list.

  lo_message_list = cf_reca_message_list=>create( ).

  TRY .
      PERFORM some_action_1.
      PERFORM some_action_2.
*     ...
      PERFORM some_action_n.

    CATCH zcx_demo_error INTO lo_error.
      lo_message_list->add_from_exception( lo_error ).
      lo_message_list->get_list_as_bapiret(
        IMPORTING
          et_list = et_bapiret
      ).

  ENDTRY.

Листинг 4. Запись текста сообщения в BAPIRET

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

2. В любом месте внутри ФМ вызовите исключение с описанием ошибки, соответствующим возникшей проблеме.

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

Явная передача сообщения при ошибке в подпрограмме

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

MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
    WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.

или иное, например, запись в лог.

Именно с этим может быть связано две опасных ситуации.

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

Вторая ситуация намного более серьезная - когда при обработке ошибки также сообщение не было перезаписано с использованием конструкции MESSAGE … RAISING <exception>, но при этом поля сообщения в sy не были заполнены до этого, то есть пусты (если быть точным, поле sy-msgty). В этом случае при вызове сообщения через стандартную конструкцию (см. листинг ссылка?) произойдет дамп с ошибкой MESSAGE_TYPE_UNKNOWN (Рис. 1).

Рис. 1. Дамп в случае пустого типа в sy

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

На последнем остановимся

Ограниченный доступ

Для прочтения полной версии статьи необходимо зайти как зарегистрированный пользователь.

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

Вячеслав Шиболов (Рейтинг: 509) 11:00, 12 февраля 2018

>> Объектные исключения пришли на смену классическим, начиная с релиза NW 6.4.
 
Добрый день, Илья!
Подскажите, пожалуйста, что такое NW 6.4? SAP NetWeaver? На сколько я понимаю, версии SAP NetWeaver начинаются от 7.0 и выше. Может быть вы имели ввиду SAP WAS 6.40? Тогда это не SAP NW. Поясните, пожалуйста.
13:26, 12 февраля 2018

Илья Казначеев (Рейтинг: 170)

Вячеслав, добрый день,
Да, вероятно, здесь не совсем корректное название, т.к. первый NW был действительно без версии и назывался "NetWeaver 2004", который работал на основе базиса версии 6.4. Так чтоп правильнее будет написать "NW'04" и или "Basis 6.4", потому что имелось в виду как раз NW'04 на основе Basis 6.4.
13:34, 12 февраля 2018

Вячеслав Шиболов (Рейтинг: 509)

Спасибо.
В таких случаях, точнее указывать версию SAP_BASIS, версия которой более стабильна в нумерации, чем SAP NW или AS.
14:16, 12 февраля 2018

Илья Казначеев (Рейтинг: 170)

Справедливо

Любое воспроизведение запрещено.
Копирайт © «Издательство ООО «Эксперт РП»