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.