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.