Пример Best Practices – использование локальных буферов
Локальные буферы записей могут быть использованы на уровне блоков процедур, таких как внутренние процедуры, функции, методы классов и т.д. Использование локальных буферов может предотвратить возникновение трудно обнаруживаемых ошибок и позволяет сделать код более надежным и легко сопровождаемым.
Глобальные буферы можно рассматривать как подкласс глобальных данных. Согласно Steve McConnell’s Code Complete:
Сущностью создания программ, размер которых превышает несколько сотен строк кода, является управление сложностью. Единственный способ осмысленно писать большую программу – это разбить ее на части так, что Вам нужно думать только об одной части за один раз. Модуляризация является самым мощным инструментом для разбиения программы на части. Глобальные данные создают дыры в Ваших возможностях модуляризации.
Согласно Progress Handbook:
Определение буферов, видимость которых явно определена для текущей процедуры, может уменьшить шансы, что ваш код как-то наследует определение буфера из другой процедуры в стеке вызовов. Умолчания, которые обеспечивает 4GL, могут быть полезны, но в серьезной бизнес-логике явное указание всех Ваших определений может спасти Вас от непредвиденных ошибок, когда умолчания не работают, как ожидалось.
Глобальные буферы могут вызывать много разных проблем, но они могут быть сведены к нескольким основным.
Проблема перепозиционирования
Длинные и сложные процедуры могут состоять из тысяч строк кода и содержать сотни вызовов других процедур. При этом трудно гарантировать, что глобальный буфер, позиционированный в процедуре, не был перепозиционирован в одной из вызываемых процедур. В приведенном ниже примере (Программа 15) все выглядит хорошо.
Программа 15. Локальные буферы – Пример 1
PROCEDURE ValidateContract: FIND Contract OF Task NO-LOCK. RUN contractIsActiveWarning. RUN ContractChecks. /* ......................... */ /* another 500 lines of code */ /* ......................... */ IF Contract.TaxCode = "" THEN … /* Error here */ END PROCEDURE.
Сначала мы позиционируем глобальный буфер Contract на некоторую запись, а затем используем этот буфер в других процедурах (contractIsActiveWarning, ContractChecks, и в самой ValidateContract). Но мы не можем гарантировать, что в какой-то из вызываемых процедур этот буфер не будет перепозиционирован или освобожден.
Если буфер был освобожден в какой-то процедуре, в следующей вызываемой процедуре возникнет неожиданная ошибка из-за недоступности записи.
И это еще хорошо, так как скорее всего данные не будут разрушены.
В то же время при множестве вызываемых процедур совершенно не очевидно, какие из них используют глобальный буфер и могут быть ответственны за его освобождение, что потребует нетривиальной отладки.
Если же буфер был перепозиционирован, проблема еще серьезнее. Может быть выбрана неправильная запись контракта – и выполнен неправильный алгоритм обработки, что приведет к повреждению данных.
Во избежание таких проблем рекомендуется использовать локальные буферы, даже если процедура выглядит простой.
Проблема блокирования
Запись, заблокированная с EXCLUSIVE-LOCK, сохраняет этот уровень блокирования до конца транзакции, и затем блокировка понижается до SHARE-LOCK, если область видимости записи больше, чем область действия транзакции и запись активна в буфере. Если запись осталась заблокирована в persistent процедуре, да еще выполняющейся на AppServer – проблема очень серьезна.
В следующем примере (Программа 16) код выглядит вполне безопасным:
Программа 16. Локальные буферы – Пример 2
PROCEDURE updateRecord: DO TRANSACTION: FIND FIRST StatHist EXCLUSIVE-LOCK. /* ... code ... */ RELEASE StatHist. END. /* Transaction */ END PROCEDURE.
Запись StatHist освобождается в конце транзакции и не может вызвать проблем блокирования. Проблема в том, что оператор RELEASE вовсе необязательно будет выполнен. В сложной процедуре всегда существует вероятность, что изменение алгоритма приведет к почти «невидимому» разветвлению кода, в котором RELEASE не будет выполнен (Программа 17). По ошибке добавленный RETURN приведет к тому, что RELEASE не будет выполнен, и запись останется в SHARE-LOCK после завершения транзакции.
Программа 17. Локальные буферы – Пример 3
PROCEDURE updateRecord: DO TRANSACTION: FIND FIRST StatHist EXCLUSIVE-LOCK. /* ... code ... */ IF bExp THEN RETURN. /* ... code ... */ RELEASE StatHist. END. /* Transaction */ END PROCEDURE.
Хотя и настоятельно рекомендуется избегать множественных выходов из процедур, особенно если они содержат транзакции, контролировать большой и сложный код нелегко. В то же время использование локального буфера гарантирует освобождение записи при выходе из процедуры, что бы в ней не произошло. Смотри пример (Программа 18).
Программа 18. Локальные буферы – Пример 4
PROCEDURE updateRecord: DEFINE BUFFER StatHist FOR StatHist. DO TRANSACTION: FIND FIRST StatHist EXCLUSIVE-LOCK. /* ... code ... */ IF bExp THEN RETURN. /* ... code ... */ END. /* Transaction */ END PROCEDURE.
Рекомендуется всегда работать с локальными буферами, если Вы блокируете записи с EXCLUSIVE-LOCK.
Проблема дублирования и повторного использования кода
Рассмотрим пример (Программа 19). Процедура updateRecord блокирует запись StatHist и вызывает процедуру updateComments, которая изменяет поле StatHist.Comments. Процедура updateComments использует глобальный буфер, и любая процедура, вызывающая ее, также должна использовать глобальный буфер.
Программа 19. Локальные буферы – Пример 5
PROCEDURE updateRecord: FIND FIRST StatHist EXCLUSIVE-LOCK. RUN updateComments("test"). END PROCEDURE. PROCEDURE updateComments: DEF INPUT PARAM pcText AS CHAR NO-UNDO. StatHist.Comments = pcText. END PROCEDURE.
Если Вы решите изменить updateRecord так, чтобы использовать локальный буфер, Вы больше не сможете использовать updateComments. Если updateComments вызывается из многих мест, Вам придется создать новую процедуру, что приводит к дублированию и ухудшению сопровождаемости кода. Следующий пример (Программа 20) работать не будет – Вы получите сообщение, что поле обновить невозможно – так как позиционированный в updateRecord локальный буфер не доступен в updateComments.
Программа 20. Локальные буферы – Пример 6
PROCEDURE updateRecord: DEF BUFFER StatHist FOR StatHist. FIND FIRST StatHist EXCLUSIVE-LOCK. RUN updateComments("test"). END PROCEDURE.
Для решения этой проблемы можно определить буфер явно, как параметр процедуры. Следующий пример (Программа 21) обеспечивает большую гибкость для разработчика, позволяя использовать в процедуре updateRecord либо глобальный, либо локальный буфер.
Программа 21. Локальные буферы – Пример 7
PROCEDURE updateRecord: DEF BUFFER lStatHist FOR StatHist. FIND FIRST lStatHist EXCLUSIVE-LOCK. RUN updateComments(BUFFER lStatHist, "test"). END PROCEDURE. PROCEDURE updateComments: DEF PARAMETER BUFFER pStatHist FOR StatHist. DEF INPUT PARAM pcText AS CHAR NO-UNDO. pStatHist.Comments = pcText. END PROCEDURE.
Явная передача буфера как параметра делает updateComment более пригодной для повторного использования и улучшает сопровождаемость кода.
Качество процедур и повторное использование
Хорошая процедура должна зависеть только от входных параметров. Правильное имя процедуры и явное описание всей входной информации значительно улучшает качество кода. Сравните:
RUN findParentRecords.
и
RUN findParentRecords (BUFFER Contract, BUFFER Contract_Config).
Заключение
Обобщая выше сказанное, можно дать две простые рекомендации:
- Если некоторая запись используется только внутри процедуры, следует использовать локальный буфер.
- Если для некоторой процедуры требуется буфер, позиционированный на некоторую запись, следует передавать буфер как параметр.