Strukturalna obsługa błędów w ABL cz. I

Zanim przejdziemy do omówienia strukturalnej obsługi błędów w języku ABL, trzeba przypomnieć pewne (oczywiste) fakty dotyczące tego języka.

ABL (wcześniej 4GL) był zawsze proceduralnym językiem bazodanowym o strukturze blokowej. Umożliwia on łączenie logiki biznesowej i interfejsu użytkownika w jednym pliku procedury zawierającym instrukcje, blok główny i bloki zagnieżdżone. Blokowa struktura jest tu niezwykle istotna, ponieważ z nią związane są zakresy transakcji, blokady rekordów a także obsługa błędów.

Od samego początku obsługa błędów (nazywana tradycyjną) była zaimplementowana w języku 4GL. Związane są z nią takie słowa kluczowe jak NO-ERROR, STATUS-ERROR, RETURN ERROR, bloki typu ON ERROR itd. Programiści dobrze wiedzą o co chodzi.

W wersji OpenEdge 10 język 4GL wyewoluował do ABL, zawierając oprócz poprzednich instrukcji także technologię programowania obiektowego i razem z tymi zmianami pojawiła się strukturalna obsługa błędów. Jest to model obsługi błędów spotykany w wielu językach, ale zazwyczaj jest kojarzony z językami obiektowymi i składnią typu: try i catch.

Instrukcja try umożliwia zdefiniowanie bloku kodu, z którym można powiązać zachowanie związane z obsługą błędów. Z kolei instrukcja catch służy do zdefiniowania obsługi błędu i powiązanie jej z typem błędu. Gdy system wykryje błąd w bloku try, wykona kod w bloku catch, który pasuje do błędu. Jeśli takiego bloku nie ma, to obsługa błędu jest przekazywana najczęściej w dół stosu wywołań, dopóki system nie znajdzie odpowiedniego bloku catch.

Strukturalna obsługa błędów posiada następujące cechy:

  • Wszystkie błędy są reprezentowane w postaci obiektów
  • Można zdefiniować własne typy błędów (klasy)
  • Można wywołać (throw) błąd explicite
  • Można obsłużyć (catch) określone typów błędów w określonym bloku ABL.
  • Możliwość propagacji błędu (ponowny throw).

Jeśli chodzi o implementację tej technologii w języku ABL, to należy przypomnieć, że jest to język o strukturze blokowej, gdzie prawie wszystkie bloki są blokami transakcyjnymi UNDO (poza zwykłym blokiem grupującym DO:…END.) i nie ma potrzeby budowania specjalnych bloków try, co ułatwia programowanie.

OK, napiszmy pierwszy prosty przykład własnej obsługi błędu, korzystając z faktu, że w tablicy Customer nie ma rekordu o polu CustNum = 1000.

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000.
    MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
    END CATCH.
END.

Wykorzystaliśmy tutaj blok transakcyjny DO ON ERROR UNDO,… W przypadku gdy rekord nie będzie odnaleziony zostanie “wyłapany” błąd systemowy i obsłużony przez nas w bloku CATCH:…END. Blok ten musi być na końcu bloku transakcyjnego. Taki “złapany” błąd przestaje istnieć w sesji ale co jeśli chcemy go przekazać po obsłużeniu do zewnętrznego bloku (np. bloku procedury)? Wystarczy następująca modyfikacja:

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000.
    MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
        UNDO, THROW mySysError.
    END CATCH.
END.

CATCH mySysError AS Progress.Lang.SysError:
    MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX.
END CATCH.

Teraz możemy wychwycić go i obsłużyć ponownie.
Należy dodać, że jeśli użyjemy tradycyjnej składni obsługi błędu NO-ERROR, to ma ona pierwszeństwo nad obsługą strukturalną. W poniższym przykładzie blok CATCH nie będzie więc wywołany.

DO ON ERROR UNDO, LEAVE:
    FIND FIRST Customer WHERE CustNum = 1000 NO-ERROR.
    IF AVAILABLE (Customer) THEN 
       MESSAGE Customer.Name VIEW-AS ALERT-BOX.
    CATCH mySysError AS Progress.Lang.SysError:
        MESSAGE mySysError:GetMessage(1) VIEW-AS ALERT-BOX ERROR.
        UNDO, THROW mySysError.
    END CATCH. 
END.

Poniżej przedstawiłem uproszczony schemat dziedziczenia klas. Progress.Lang.Error służy do wychwycenia dowolnego błędu. Progress.Lang.ProError służy do wychwycenia dowolnego rodzaju błędu Progress: systemu, aplikacji, SOAP. Progress.Lang.SysError służy do wychwycenia błędów języka ABL, np.: niepowodzenie instrukcji FIND, błąd indeksu; wszystko, co obsługuje NO-ERROR. Progress.Lang.AppError służy do wychwycenia błędów aplikacji zgłaszanych (throw) zgodnie z regułami biznesowymi ABL. Z tej klasy może dziedziczyć nasza własna klasa do obsługi błędów.

Zobaczmy teraz jak wychwytywać błędy w procedurach i przesyłać stosowne komunikaty do programu głównego.
W tradycyjnej obsłudze błędów mogliśmy skorzystać ze składni w programie wywołującym: IF ERROR-STATUS:ERROR THEN MESSAGE RETURN-VALUE VIEW-AS ALERT-BOX ERROR.
oraz w wywoływanym RETURN ERROR “Brak rekordu w bazie”.

W obsłudze strukturalnej mamy szersze pole do popisu. Przyjrzyjmy się poniższemu przykładowi.

/* main.p */
VAR INT i = 1.

RUN FindCustomer.p (1000).

CATCH syserr AS Progress.Lang.SysError:
    MESSAGE "Customer: " syserr:GetMessage(1)
       VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
  
CATCH apperr AS Progress.Lang.AppError:
  DO WHILE i <= apperr:NumMessages:
      MESSAGE "Country: " apperr:GetMessage(i)  
                          apperr:GetMessageNum(i)
         VIEW-AS ALERT-BOX INFO BUTTONS OK.
      i = i + 1.    
  END.      
END.

/************************************************************/

/* FindCustomer.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

DEFINE INPUT PARAMETER icustNum AS integer.

FIND customer WHERE customer.custnum = icustNum.
IF country NE "Poland" THEN
    UNDO, THROW NEW Progress.Lang.AppError("Niewłaściwy kraj", 555).
    
/* Można dodac więcej komunikatów własnej obsługi błędów */    
CATCH e AS Progress.Lang.AppError :
    e:AddMessage("Klient nie jest z Polski", 777).
    undo, throw e.     
END CATCH.

Na początku wywoływanej procedury FindCustomer.p znajduje się instrukcja BLOCK-LEVEL ON ERROR UNDO, THROW. która gwarantuje, że wszystkie nieobsługiwane błędy w blokach transakcyjnych, zostaną propagowane do obiektu wywołującego. Jeśli wywołamy tę procedurę z parametrem 1000 (a więc będzie brak rekordu Customer) błąd zostanie obsłużony w procedurze main.p w bloku CATCH syserr AS Progress.Lang.SysError (ponieważ będzie to błąd systemowy).
Załóżmy, że jeśli rekord zostanie znaleziony, będziemy chcieli sprawdzić czy na pewno klient jest z Polski a jeśli nie, to ustawić własny błąd. Definiujemy więc błąd typu AppError: …THROW NEW Progress.Lang.AppError(“Niewłaściwy kraj”, 555).
Po pewnym czasie chcielibyśmy dodać jeszcze jeden komunikat dla tego błędu. Możemy to zrobić bez problemu gdyż ta klasa posiada metodę AddMessage jak widać poniżej w Class Browser w Developer’s Studio (dla klasy SysError nie ma takiej możliwości).

Możliwości obsługi błędów w aplikacjach ABL jest więc całkiem sporo. Wiem, że niektórzy chcieliby wiedzieć jak poradzić sobie z serwerem aplikacji i czy można pisać własne klasy obsługi, ale o tym następnym razem.