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

W poprzednim odcinku obiecałem napisać o tym jak utworzyć własną klasę obsługi błędów oraz pokazać, że strukturalną obsługę błędów można stosować także w przypadku procedur uruchamianych na serwerze aplikacji. Obie te kwestie pokażę na jednym przykładzie.

W Developer’s Studio klasę definiujemy najwygodniej przy pomocy wizarda, tak jak każda inną klasę. Musimy pamiętać, że dziedziczy ona z klasy Progress.Lang.AppError oraz że trzeba zdefiniować ją jako Serializable, co oznacza, że instancja klasy może być przekazywane przez wartość pomiędzy sesjami AVM (ABL Virtual Machine).

Poniżej widzimy gotową klasę Errors.myError zawierającą właściwości: Errormessage, ErrorNum oraz ProcName oraz dwie metody GetErrorMessage() i GetVerboseErrorMessage(). Posłużą one do własnej obsługi błędów.
Pierwsza metoda zwraca jedynie komunikat wraz z numerem błędu. GetVerboseErrorMessage() zawiera także nazwę procedury, w której ten błąd wystąpił. Gdybyśmy tworzyli program złożony z obiektów moglibyśmy użyć nazwy klasy i metody aby mieć więcej informacji o lokalizacji wystąpienia błędu itp.

USING Progress.Lang.*.
USING Progress.Lang.AppError.

BLOCK-LEVEL ON ERROR UNDO, THROW.

CLASS Errors.myError INHERITS AppError SERIALIZABLE: 

    DEFINE PUBLIC PROPERTY ErrorMessage AS CHARACTER NO-UNDO 
    GET.
    SET. 

    DEFINE PUBLIC PROPERTY ErrorNum AS INTEGER NO-UNDO 
    GET.
    SET. 
    
    DEFINE PUBLIC PROPERTY ProcName AS CHARACTER NO-UNDO 
    GET.
    SET. 

    CONSTRUCTOR PUBLIC myError (
        INPUT pErrorNum AS INTEGER,
        INPUT pErrorMessage AS CHARACTER,
        INPUT pProcName AS CHARACTER):            
        SUPER ().
        ErrorNum = pErrorNum.            
        ErrorMessage = pErrorMessage.
        ProcName = pProcName.
    END CONSTRUCTOR.

    METHOD PUBLIC CHARACTER GetErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum).
        RETURN res.
    END METHOD.

    METHOD PUBLIC CHARACTER GetVerboseErrorMessage(  ):
        DEFINE VARIABLE res AS CHARACTER NO-UNDO.
        res =  ErrorMessage + ". " + STRING(ErrorNum) + ". Proc: " + ProcName.
        RETURN res.
    END METHOD.    
    
END CLASS.  

Poniższy przykład jest analogiczny do tego z poprzedniego odcinka – został zmodyfikowany do uruchomienia na serwerze aplikacji, oczywiście z własną obsługą błędów.

USING Errors.myError.

VAR INT i = 1.
DEFINE VARIABLE hServer AS HANDLE NO-UNDO.
DEFINE VARIABLE lReturn AS LOGICAL NO-UNDO.

CREATE SERVER hServer.
lReturn = hServer:CONNECT("-URL http://localhost:8810/apsv
  -sessionModel Session-managed"). 

IF NOT lReturn THEN DO:
  DELETE OBJECT hServer NO-ERROR.
  RETURN ERROR "Failed to connect to the ABL web application: " + RETURN-VALUE.
END.

RUN catcherror-pasoe-myerror.p ON hServer (INPUT 3000).

CATCH myerr AS MyError:
    MESSAGE myerr:GetVerboseErrorMessage()
        VIEW-AS ALERT-BOX INFO BUTTONS OK.
END.
/************************************************************/

/* catcherror-pasoe-myerror.p */
BLOCK-LEVEL ON ERROR UNDO, THROW.

USING Errors.myError.

DEFINE INPUT PARAMETER icustNum AS integer.
DEFINE VARIABLE ProcName AS CHARACTER.


FIND customer WHERE customer.custnum = icustNum.
if country NE "Poland" THEN DO:
    ProcName = ENTRY(1, PROGRAM-NAME(1),' ').
    UNDO, THROW NEW myError(555, "Bad country", ProcName).

END. 

Ponieważ użyłem metody GetVerboseErrorMessage() otrzymuję cały komunikat z nazwą procedury w której wystąpił błąd.

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łuż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.