obsługa błędów

0

Zastanawiam się jaki jest najlepszy sposób obsługi błędów w aplikacji desktopowej. Do tej pory stosowałem zwykłe kody błędów zwracane przez funkcje/metody podobne do tych z WinAPI. Prowadziło to do różnych niedogodności jak np. duża ilość instrukcji if (lub case/switch), czy zwracanie wartości przez parametry wyjściowe. W nowym programie chciałem zastosować wyjątki, ale myślę jak zrobić to w poprawny/najlepszy sposób.

Do tej pory wpadłem na dwie koncepcje:

<image> http://przeklej.net/image_details/121422.html </image> ```delphi

type TMyException = class
public
class procedure RaiseError1(Msg:string);
class procedure RaiseError2(const Msg: string; const Args: array of const);
...
end;

class procedure TMyException.RaiseError1(Msg: string);
begin
Logger(Msg);
raise Exception.Create(Msg);
end;

class procedure TMyException.RaiseError2(const Msg: string;
const Args: array of const);
begin
Logger(Msg, Args);
raise Exception.CreateFmt(Msg, Args);
end;

Kod testowy:
```delphi
 
try
	TMyException.RaiseError1('example 1');
except
	// on E: Exception do ShowMessage(E.message); // dodatkowe dzialanie, gdy wystąpi wyjątek
end;

Zalety:

  • obsługa wszystkich błędów w jednym miejscu - klasa TMyException
  • proste logowanie występujących błędów - obiekt Logger występuje tylko w klasie TMyException
    Wady:
  • brak możliwości reagowania na określony typ wyjątku

user image

 
type
// każda klasa posiada swoje typy wyjątków
  ETEST1 = class(Exception);
  ETEST2 = class(Exception);
  ...
  
// kod testowy
try
	raise ETEST1.Create('example 1');
except
	on E: ETEST1 do Logger(E.message);
end;

Zalety:

  • selektywna obsługa błędów, każdy rodzaj wyjątku może wykonywać inne akcje
  • typy wyjątków ograniczone są tylko do klasy, w której występują
    Wady:
  • problem z logowaniem błędów - każdy wyjątek musi być oddzielnie obsłużony
  • obiekt Logger musi być dostępny w każdej klasie

Macie pomysły na inne zalety/wady/problemy związane z przedstawionymi koncepcjami? Może są jakiś inne sprawdzone rozwiązania obsługi i logowania błędów?

0

1.Application.OnException (nie jestem pewien, lecz to powinno być tym, czego szukasz)
2.Lub:

Procedure RaiseException(const Obj: Exception);
Begin
 Log('Exception of class %s raised at address 0x%x: %s', [Obj.ClassName, uint32(ExceptAddr), Obj.Message]);
 raise Obj;
End;
{...}
RaiseException(ETest1.Create('foo'));
0

zamiast tak kombinować możesz wziąć np. madexcept (darmowy dla niekomercyjnych zastosowań) albo w pakiecie JEDI coś podobnego jest i dopisać logowanie błędów razem z całą ścieżką co było wywoływane i w którym miejscy (w której linii) wystąpił błąd

0

Może rzeczywiście niezbyt dokładnie wyjaśniłem o co mi chodzi. Załóżmy, że piszę program, który składa się z pewnych dużych klas(zbiorów klas) np. klasa obsługująca komunikację z urządzeniem, klasa przechowująca zbierane dane, klasa generująca raporty, etc. W każdej z klas znajdują się metody, które mogą wygenerować błędy np. metody ReadFrame i WriteFrame służące do odbierania i wysyłania komunikatów z/do urządzenia. W momencie gdy połączenie między urządzeniem a komputerem zostanie zerwane obie metody mogą w zależności od sposobu obsługi błędów: zwrócić kod błędu albo wyrzucić wyjątek (User-Defined Exception). W takim przypadku zazwyczaj stosowałem kody błędów, ale chciałem spróbować drugiego sposobu, czyli zabawy z wyjątkami. I tu pojawia się problem, bo zastanawiam się co będzie lepszym rozwiązaniem:

  • zdefiniowanie jednej wielkiej klasy obsługującej w ogólny sposób wszystkie wyjątki, ze wszystkich modułów (koncepcja 1 z poprzedniego posta)
  • zdefiniowanie wyjątków we wnętrzu każdej z dużych klas (koncepcja 2 z poprzedniego posta)

Główne wady jakie przychodzą mi na myśl to:

  • pierwsza koncepcja wygeneruje sporą liczbę powiązań pomiędzy klasami głównymi a klasą TMyExceptions, ale łatwiej będzie zmodyfikować sposób obsługi błędów
  • druga koncepcja rozproszy obsługę błędów na poszczególne klasy umożliwiając ich selektywną obsługę (User-Defined Exception), ale prawdopodobnie będą problemy z hermetyzacją, bo będę musiał jakoś upublicznić jakie wyjątki dana klasa może wygenerować i zaimplementować ich obsługę w klasach, które ją wykorzystują. Ewentualna rozbudowa będzie dość czasochłonna, bo wymagana będzie osobna modyfikacja każdej klasy.

Gdybym miał pewność, że obsługa błędów nigdy nie będzie zmieniana/rozbudowywana to wybór rozwiązania nie miałby aż takiego znaczenia. Niestety nie mam takiej pewności, więc wolę się wcześniej podwójnie zastanowić niż później mieć problemy z utrzymaniem kodu. Tym bardziej, że nie mam jeszcze zbyt dużego doświadczenia w obsłudze błędów za pomocą wyjątków.

@Patryk27
Application.OnException odpada, bo zmienia domyślne zachowanie w momencie gdy wyjątek nie jest przechwycony przez kod aplikacji.
Co do drugiej opcji to muszę przyznać, że jest ciekawa bo gdyby procedurę RaiseException opakować w klasę to połączone zostaną dwie przedstawione przeze mnie koncepcje tzn.:

  • wszystkie wyjątki mogą być obsługiwane w jednym miejscu, co ułatwia logowanie
  • jest możliwość selektywnej obsługi błędów
    Muszę jeszcze nad tym pomyśleć, może znajdę jakieś wady ;)

@abrakadaber
madexcept odpada, bo nie udostępnia kodów źródłowych, a zależy mi na samej koncepcji jak powinno się rozwiązać problem obsługi błędów. Co do pakietu JEDI to rzucę na to okiem.

0

ale to co piszesz jest w sprzeczności z sobą. Obsługa wyjątków a ich generowanie to są dwie różne i oddzielne rzeczy. Czym innym jest wywalenie wyjątku bo np. komunikacja padła a czym innym ich obsłużenie aby zamiast "brzydkiego" okienka z błędem i zamknięcia aplikacji mieć trochę przyjemniejsze okno, logowanie tego co się stało i ew. reakcję na to. Co do pomysłu @Patryk27 z RaiseException to powiedz mi co zrobisz z błędem, który wyrzuci jakaś wewnętrzna klasa (nie Twoja)?

Ogólnie jeśli błąd jest spodziewany (np. utrata połączenia, brak pliku, itp) to się go obsługuje tam, gdzie się go spodziewamy. Każdy inny błąd albo puszczamy tak jak jest albo robimy wspólną obsługę (np. w OnException chociaż tutaj nie wszystkie błędy są łapane albo korzystamy z gotowców, jak np. wspomniane już madexcept albo eurekalog).

Musisz sobie zdać sprawę, że nie tylko Twoje klasy mogę rzucić błędem a otaczanie każdej metody blokiem try except to pomyłka

0

@abrakadaber: Generowanie i obsługa jest ze sobą bardzo powiązana, bo każdy nieobsłużony wyjątek zazwyczaj spowoduje "zamknięcie" aplikacji. Samo generowanie wyjątków nie jest tutaj problemem, ale odpowiednia ich obsługa.

Jeśli dobrze zrozumiałem "wewnętrzna klasa" to mogę zrobić np. tak:

 procedure TMyClass.Send();
begin
if not NotMyClass.METODA then 	// <-- metoda z klasy wewnętrznej, która w normalnym przypadku 
					// zwraca true jeśli wszystko ok, a false jeśli wystąpi błąd; 
					// dodatkowo może zwrócić jakiś ukryty wyjątek
    RaiseException(ETEST1.Create('oczekiwany błąd'));
end;
 // obsługa błędów
try
    try
	MyClass.Send()  // metoda zdefiniowania wyżej
    except
	on E: ETEST1 do;  // tego sie spodziewam
	else
	    RaiseException(EINTERNALERROR.Create('nieoczekiwany błąd')); // wszystkie inne wyjątki
    end;
except
end;

Jako że logowanie jest w RaiseException to wszystkie błędy mam zapisane.

To że nie należy nadmiernie korzystać z try except to jest jasne, ale są metody, które np. zwracają dobre dane pod warunkiem, że istnieje poprawne połączenie. Jeśli połączenie zostało zerwane to metoda musi jakoś zwrócić błąd, czy to w formie prostego BOOLa(kodu błędu), czy w formie wyjątku.

Ogólnie jeśli błąd jest spodziewany (np. utrata połączenia, brak pliku, itp) to się go obsługuje tam, gdzie się go spodziewamy

Właśnie o to mi chodzi, czyli obsługę spodziewanego wyjątku. Można to zrobić tak:

 
try
    MetodaGenerującaWyjatki;
except
    on E: Wyjatek1 do Log('Wyjatek1');
    on E: Wyjatek2 do Log('Wyjatek2');
    ...
    on E: Wyjatekn do Log('Wyjatekn');
else
    Log('inne');
end;

z tym że takie podejście rodzi problemy z utrzymaniem kodu. Co w przypadku jak będę musiał np. dodać wyświetlanie MessageBoxa z rodzajem błędu? Będę musiał w każdym bloku try except dodać wyświetlanie komunikatu. Dla kilku bloków nie jest zbyt duży problem, ale dla dziesiątek robi się uciążliwe, a dla setek, czy tysięcy wolę nie myśleć. Dodatkowo rośnie liczba powiązań pomiędzy klasami.

0

tylko wtedy MUSISZ (prawie) każdą metodę wziąć w blok try except co jest wg mnie bezsensem. Tak samo jak za bezsens uważam opakowywanie już zdefiniowanych wyjątków przez własne klasy. Jak już pisałem - nie tędy droga - jak chcesz łapać wszystkie błędy to weź gotowca. Ja akurat korzystam z madExcept i od tego czasu naprawianie błędów jest dużo prostsze. Jak chcesz go używać komercyjnie albo chcesz źródła to chyba 159ojro (EurekaLog 249zielonych) to nie jest jakaś kosmiczna cena. Cała obsługa sprowadza się do pacnięcia komponentu na formę, przeklikaniu się przez ustawienia co ma się robić jak wystąpi błąd i ew. obsłużenie JEDNEGO zdarzenia jeśli chcesz obsługiwać wyjątki po swojemu.

Jak chcesz za free to ściągnij sobie pakiet JEDI i tam też jest coś podobnego.

Oczywiście można z uporem maniaka wstawiać wszędzie try except ale po co...?

0

@abrakadaber: Tak jak pisałem wcześniej, blok try except chcę stosować tylko tam gdzie konieczne jest sprawdzenie czy nie wystąpił jakiś błąd, czyli prawie wszędzie gdzie wcześniej korzystałem z kodów błędów. Tych bloków się nie ominie, bo sterują przepływem w przypadku wystąpienia błędu, którego się spodziewam. Jeśli mam w klasie dane, na których przeprowadzam jakieś operacje i dodatkowo jestem pewien, że dobrze przeprowadziłem ich walidację to mogę pominąć blok try except, bo oczekiwany błąd nie wystąpi.

Dlaczego chcę tak zrobić? Najlepiej zobrazują to dwa przykłady:

//podejście wykorzystujące kody błędów
function TMyClass.TrySendShutDown():Integer;
var
	DevHandle: THandle;
	DevRecord: TDevRecord;
begin
	DevHandle := GetHandle(DEV1);
	if DevHandle <> 0 then
	begin
		DevRecord := GetDevRecord(DevHandle)
		if DevRecord.Status <> 0 then
		begin
			pauseDevice(DevHandle);
			clearDeviceWorkQueue(DevHandle);
			closeDevice(DevHandle);
			Result := SUCCESS;
		end
		else
			Result := EDeviceRecordError;
	end
	else
		Result := EDeviceHandleError;
end;

function TMyClass.SendShutDown():Boolean;
var
	DevHandle: THandle;
begin
	Result := False;
	case TryToSendShutDown() of
		SUCCESS: Result := True;
		EDeviceHandleError: log('abc');
		EDeviceRecordError: log('xyz');
	end;
end;
//podejście wykorzystujące wyjątki
procedure TMyClass.TryToSendShutDown();
var
	DevHandle: THandle;
	DevRecord: TDevRecord;
begin
	DevHandle := GetHandle(DEV1); // <- zamiast zwrócić kod błędu generuje wyjątek EDeviceHandleError 
	DevRecord := GetDevRecord(DevHandle); // <- zamiast zwrócić kod błędu w polu Status generuje wyjątek EDeviceRecordError 
	
	pauseDevice(DevHandle);
	clearDeviceWorkQueue(DevHandle);
	closeDevice(DevHandle);
end;

function TMyClass.SendShutDown(): Boolean;
var
	DevHandle: THandle;
begin
	Result := False;
	try
		TryToSendShutDown();
		Result := True;
	except
		on E: EDeviceHandleError do log('abc',e);
		on E: EDeviceRecordError do log('xyz',e)
	end;
end; 

O ile metody SendShutDown niewiele się od siebie różnią, to patrząc na metody TryToSendShutDown można zauważyć dużą różnicę w czytelności.

Gotowe rozwiązania typu madExcept podmieniają domyślną obsługę nieobsłużonych wyjątków, czyli zamiast zwykłego komunikatu o błędzie wyświetlają zgrabne okienko z całą masą informacji. Tak przynajmniej zrozumiałem z informacji i filmików ze strony madshi.net. W tym wątku chodzi mi o obsługę wyjątków, których się spodziewam i które sam zdefiniuje.

Powracając do głównego tematu.
Gdzie najlepiej zdefiniować wyjątki?
a) w jednym dużym module, który będzie zawierał wszystkie wyjątki i dołączany będzie do każdej klasy
b) w mniejszych modułach np. w klasach wykorzystujących te wyjątki
c) inne rozwiązania?

Jak obsłużyć/generować takie wyjątki, tak aby zachować możliwość łatwej rozbudowy?
a) definiowanie zachowania w każdym bloku try except np logowanie błędów
b) generowanie wyjątków za pomocą własnej funkcji RaiseException zaproponowanej przez @Patryk27
c) inne?

1 użytkowników online, w tym zalogowanych: 0, gości: 1