Search  
Monday, October 23, 2017 ..:: Articole » Raportarea erorilor ::.. Register  Login
 Articole Minimize

Raportarea erorilor

Autor: Doug Hennig
Notă: Ştim cât este de greu să faci o aplicaţie fără erori. De cele mai multe ori, aplicaţiile sunt distribuite cu erori ascunse, şi care devin evidente numai în anumite circumstanţe. În această expunere vom studia implementarea în aplicaţii a unui mod facil de raportare a erorilor către dezvoltator.

Nu ştiu cum sunt utilizatorii dvs, dar dacă sunt ca ai mei, atunci raportarea erorilor se petrece cam aşa: primiţi un telefon de genul:

"Ai o eroare în programul tău." (este de remarcat faptul că dacă toate lucrurile merg bine, este programul lor, iar dacă ceva merge rău, este programul tău.)

"Ce mesaj de eroare era?" întrebaţi dvs.

"Nu-mi aduc aminte."

"Dar puteţi să-mi spuneţi ce făceaţi când a apărut eroarea?"

"Nu făceam nimic!"

"Voiam să întreb în ce situaţie apare eroarea? Ce funcţie a programului foloseaţi?"

"Nu-mi aduc aminte exact. Dar să ştii că e urgent şi trebuie să rezolvi problema imediat!"

Această situaţie apare atunci când raportează erorile. De multe ori se întâmplă să nu le raporteze de loc. Am urmărit odată o tipă care făcea ceva, şi la un moment dat a apărut un mesaj de eroare. Ea l-a închis cu "Ignore" fără să-l citească. "Ce era cu mesajul ăla?" am întrebat. "Aaaaa! Întotdeauna face aşa. Chiar, n-ai putea să faci să dispară mesajul? Ar merge treaba mai repede." Era un bug minor în cod, dar nimeni nu-l raportase. Pasul "închide mesajul de eroare" devenise şi el parte din procesul normal de exploatare!

În acest fel, este greu să înţelegi tipul erorii. Astfel, se impune implementarea unei posibilităţi ca utilizatorul să poată raporta ce s-a întâmplat. Caseta de dialog din Figura 1 este potrivită pentru dezvoltator, deoarece ea oferă informaţiile necesare pentru depanarea unei erori. Pe de altă parte, ea nu este aplicabilă unui utilizator, deoarece informaţiile prezentate nu îi sunt deloc utile (o situaţie similară apare când apăsăm butonul "Details" într-o fereastră din aceea cu "This program has performed an illegal operation and will be shut down". Spuneţi-mi, vă rog, de câte ori v-aţi uitat în "Details"?)

Figura 1. Caseta de dialog pentru programator.

 eroare1.gif

Pentru utilizator este mult mai potrivită o casetă de dialog ca cea din Figura 2. Ea înştiinţează utilizatorul că s-a întâmplat ceva, şi îi pune la dispoziţie două opţiuni de raportare a erorii: prin fax sau e-mail. Apoi utilizatorul are două opţiuni: să rămână în aplicaţie (dar nu în metoda care a generat eroarea, pentru că va obţine o nouă eroare) sau să închidă aplicaţia. Această expunere tratează modul în care se poate implementa în aplicaţie acest tip de casetă de dialog.

Figura 2. Caseta de dialog pentru utilizator.

 eroare2.gif

Îmbunătăţiri aduse schemei de tratare a erorilor

Voi face o scurtă trecere în revistă a schemei de tratare a erorilor implementată. Pentru detalii, vedeţi expunerile anterioare.

Dacă apare vreo eroare într-un obiect instanţiat din clasele conţinute în biblioteca SFCTRLS.VCX (sau o subclasă a lor) se activează metoda Error. Ea poate încerca să rezolve problema sau o poate transmite în sus în ierarhia claselor (de la obiect la clasă, apoi la clasa părinte, ş.a.m.d.) până la clasa de bază (cea conţinută în biblioteca SFCTRLS). Apoi eroarea este transmisă în sus în ierarhia containerelor - de la obiect la containerul său, apoi la containerul acestuia, etc, până când ajunge la formularul pe care se găseşte plasat obiectul. La oricare din paşi, ceva poate decide modul de rezolvare a erorii, şi atunci transmiterea erorii încetează. Odată ajuns la formular (clasa cea mai de sus în ierarhia claselor şi cea mai de sus în ierarhia containerelor), eroarea nu mai poate fi transmisă în ordine ierarhică, şi dacă nu s-a decis nimic în privinţa ei încă, atunci este transmisă rutinei globale de tratare a erorilor (metoda ErrorHandler a unei instanţe a clasei SFErrorMgr). Această rutină globală este apelată şi dacă eroarea survine în cod non-obiect (cum ar fi un fişier .PRG), deoarece este apelat de comanda ON ERROR. Acest mecanism de transmitere este denumit "Lanţul Responsabilităţii".

Clasa SFErrorMgr nu ştie cum să trateze anumite erori specifice. Ea doar oferă servicii de tratare a erorilor, cum ar fi înregistrarea erorii într-un fişier, afişarea unui mesaj de eroare şi rezolvarea situaţiei. Prin "rezolvarea situaţiei" nu înţeleg eliminarea erorii. Înţeleg doar deciderea acţiunii următoare: închiderea aplicaţiei, reîncercarea instrucţiunii care a generat eroarea sau rămânerea în aplicaţie prin renunţarea la restul codului din metoda care a generat eroarea.

Între timp au mai apărut câteva îmbunătăţiri la schema de tratare a erorilor descrisă mai sus.

Evitarea unui mesaj ambiguu

Poate aţi văzut chestia asta dar n-aţi ştiut care este cauza: când aplicaţia încearcă să deschidă un fişier inexistent, cum ar fi ABC.TXT, mesajul de eroare rezultat este "File oError does not exists". Dar nu are nici un sens: aplicaţia _nu_ a încercat să deschidă un fişier numit oError; mesajul ar fi trebuit să fie "File ABC.TXT does not exists". Motivul pentru care mesajul de eroare este greşit este funcţia TYPE; folosirea ei într-o condiţie de eroare poate modifica textul erorii. De exemplu, metoda Error a tuturor claselor din biblioteca SFCTRLS.VCX foloseşte TYPE('oError') pentru a determina dacă există un obiect global de tratare a erorilor. Dacă există, excelent. Dacă nu, partea parametru a mesajului (în cazul nostru, "ABC.TXT") este suprascrisă de expresia evaluată de comanda TYPE() (în cazul nostru, oError).

Modul în care am rezolvat problema aceasta este salvarea informaţiilor despre eroare într-un masiv (folosind funcţia AERROR(), înainte de a folosi comanda TYPE(). Astfel, dacă metoda Error determină că am transmis eroarea altui obiect (cum ar fi obiectul global de tratare a erorilor), mai întâi va apela metoda SetError a acelui obiect (dacă există), transmiţându-i masivul. Iată un fragment din metoda Error, care demonstrează acest lucru:

AERROR(laError)
* restul codului aici
DO CASE
* Celelalte cazuri aici
CASE TYPE('oError.Name') = 'C' AND ;
	PEMSTATUS(oError, 'ErrorHandler', 5)
	IF PEMSTATUS(oError, 'SetError', 5)
		oError.SetError(lcMethod, tnLine, @laError)
	ENDIF PEMSTATUS(oError, 'SetError', 5)
	lcReturn = oError.ErrorHandler(tnError, lcMethod, ;
		tnLine)

Metoda SetError a clase SFErrorMgr salvează informaţiile transmise într-o proprietate masiv numită aErrorInfo şi setează proprietatea lErrorInfoSaved la .T. Când metoda ErrorHandler este apelată pentru a trata eroarea, ea verifică proprietatea lErrorInfoSaved; dacă este .T., înseamnă că metoda SetError a fost apelată deja şi aErrorInfo conţine valorile corecte. Dacă nu, atunci este folosită comanda AERROR() pentru a obţine informaţiile despre eroare şi apoi este apelată metoda SetError pentru a le stoca în aErrorInfo.

Această modificare (apelarea metodei SetError înaintea comenzii TYPE()) determină înregistrarea exactă a informaţiilor despre eroare, şi, implicit, afişarea corectă a mesajului de eroare.

Gestionarea obiectelor incluse în subclase

Metoda Error a claselor din SFCTRLS.VCX plasează ca prefix la metoda în care a apărut eroarea (transmisă ca al doilea parametru lui oError) numele obiectului şi un punct, astfel încât arată cam aşa: NumeObiect.Metodă. Pe măsură ce eroarea este transmisă în sus în ierarhie, fiecare obiect adaugă propriul său nume şi un punct la început. Această manevră face mult mai uşoară localizarea obiectului în cauză, pentru că şirul va deveni ceva de genul "frmClienti.cntAdresa.txtOras.Valid", în loc de "Valid" (ceea ce s-ar fi obţinut dacă trimiteam numai numele metodei). Mai există un avantaj: dacă şirul are măcar un punct, atunci înseamnă că eroarea nu a survenit în una din metodele obiectului curent, ci în unul din obiectele din "Lanţul responsabilităţii"; ca atare, metoda Error nu va încerca să rezolve eroarea, ci doar va transmite mesajul "return" către metoda care a apelat-o.

Dar există şi o problemă în această schemă: dacă creaţi o clasă container care conţine câteva controale, apoi subclasaţi acest container şi puneţi o instanţă a acelei clase pe un formular, în momentul apariţiei unei erori într-unul din obiectele incluse în container metoda Error va primi numele metodei cu eroarea ŞI numele controlului. De exemplu, va fi transmis "cmdSalvare.Click" în loc de "Click", cum ar fi trebuit. Deci până la urmă se va obţine un şir de genul "cmdSalvare.cmdSalvare.Click", ceea ce va crea confuzie în codul care trebuie să decidă dacă eroarea trebuie procesată local sau trebuie returnat şirul "Return" (un punct în denumire înseamnă că trebuie returnat, dar în acest caz nu....).

Soluţia este să verificăm dacă numele obiectului există deja în şirul transmis. Situaţia aceasta nu se poate întâmpla decăt în cazul descris mai sus, aşa că dacă se întâmplă, dăm afară numele obiectului (pentru că este posibil să nu arate aşa cum ne aşteptăm - un exemplu este "CMDSALVARE.Click") şi apoi îl punem la loc. Iată codul pentru metoda Error:

lcName   = UPPER(This.Name) + '.'
lcMethod = UPPER(tcMethod)
IF lcMethod = lcName OR '.' + lcName $ lcMethod
	lcOrigMethod = SUBSTR(tcMethod, RAT('.', tcMethod) + 1)
ELSE
	lcOrigMethod = tcMethod
ENDIF
lcMethod = lcName ...
lcMethod = This.Name + '.' + lcOrigMethod

Debugger-ul trebuie să afişeze metoda corectă!

Dacă apare vreo eroare în timpul dezvoltării aplicaţiei, una din opţiunile disponibile în caseta de eroare este afişarea debugger-ului. În acest fel este foarte simplu să depanez erorile, şi chiar să o rezolv temporar, fără să părăsesc debugger-ul (de exemplu, prin declararea variabilei care lipseşte în fereastra de comenzi, sau prin atribuirea valori corecte variabilei, apoi folosirea comenzii Set Next Statement în debugger, şi mă întorc la linia care a generat eroarea). Una din problemele versiunii lui SFErrorMgr din expunerea anterioară era faptul că erau multe proceduri în stiva de apelare până la metoda care a generat eroarea (din cauza "Lanţului responsabilităţii") aşa că trebuia dată comanda Step Out până la întoarcerea la metoda dorită. În versiunea din această expunere, metoda SFErrorMgr.ErrorHandler întoarce mesajul "Debug" (definit în constanta ccMSG_DEBUG în fişierul SFERRORS.H) şi fiecare metodă Error, cu excepţia primeia întoarce acest şir (am menţionat mai devreme că metodele returnează şirul atât timp cât în numele metodei care a generat eroare există caracterul punct (.)). În fine, metoda Error iniţială afişează Debugger-ul, dacă primeşte şirul corespunzător.

Încă un aspect şi am terminat: datorită faptului că Debugger-ul este apelat din interiorul metodei Error, el va afişa codul acestei metode, ca atare va trebui să daţi Step Out pentru a reveni la codul metodei care a generat eroarea. Eu, fiind un tip comod, (pardon, eficient), prefer să evit munca suplimentară pe cât posibil. Pentru a determina debugger-ul să afişeze codul metodei dorite, pot să simulez combinaţia de taste pentru funcţia Step Out. Combinaţia este Shift+F7. Imediat ce controlul este redat ferestrei de comenzi, se execută combinaţia de taste şi debugger-ul se va duce la metoda pe care o vreau. Dar datorită modului în care este implementat debugger-ul (sau poate datorită unui bug) figura ţine numai dacă debugger-ul rulează în propria sa fereastră (opţiunea "Debugger Frame" din Tools-Options). Deci, dacă debugger-ul rulează în propria sa fereastră, voi face mişcarea asta; dacă nu, nu. Iată codul:

CASE lcReturn = ccMSG_DEBUG
DEBUG
	IF WEXIST('Visual FoxPro Debugger')
		KEYBOARD '{SHIFT+F7}' PLAIN
	ENDIF WEXIST('Visual FoxPro Debugger')
	SUSPEND

Clasele mesajelor de eroare

Acum este momentul să studiem cum se face o casetă de dialog ca cea din Figura 2. Metoda ErrorHandler a clasei SFErrorMgr apelează metoda DisplayError pentru a afişa un mesaj de eroare către utilizator. Metoda DisplayError instanţiază un obiect din clasa specificată în proprietatea cMessageClass; în mod implicit, această proprietate conţine valoarea SFErrorMessage. În versiunea anterioară, SFErrorMessage era o clasă Form care afişa un mesaj într-un editbox şi avea butoane pentru a-i permite utilizatorului o opţiune (Cancel, Quit, ş.a.m.d.). În versiunea din această expunere, SFErrorMessage este folosit numai pentru a afişa mesajele de eroare unui programator, în timpul dezvoltării programului, deci are clasa de bază Custom şi foloseşte o casetă MESSAGEBOX().

Au fost făcute câteva modificări pentru a face clasa SFErrorMgr şi mai flexibilă. Metoda DisplayError transmite o referinţă a clasei pe care a instanţiat-o înapoi lui SFErrorMgr, apoi apelează metoda CreateErrorMessage pentru a construi textele care vor fi afişate în caseta de eroare, şi apelează metoda SetDialogProperties (care nu are cod în această clasă) pentru a permite şi alte acţiuni asupra casetei de eroare înaintea afişării ei (metoda Show). În acest fel este mai uşor să modific clasa casetei de eroare (modific valorile proprietăţilor cMessageClass şi cMessageLibrary); mai mult chiar, pot să subclasez SFErrorMgr şi să pun codul în metoda SetDialogProperties pentru a folosi o casetă de eroare particularizată. De exemplu, într-o aplicaţie am subclasat SFErrorMgr şi codul acestei metode ia informaţiile din altă aplicaţie şi le afişează aici. În continuare aveţi codul metodei DisplayError:

loMessage = CREATEOBJECT(.cMessageClass, ;
	.cMessageLibrary, '', This)
loMessage.cTitle = This.cTitle
loMessage.cErrorMessage = This.CreateErrorMessage()
This.SetDialogProperties(loMessage)
loMessage.Show()
lcChoice = IIF(VARTYPE(loMessage) = 'O', ;
	loMessage.cChoice, 'Cancel')
RETURN lcChoice

În completare, au fost adăugate două noi proprietăţi: cAppName (numele aplicaţie rulate de utilizator) şi cVersion (numărul de versiune al aplicaţiei). Aceste proprietăţi ar trebui să capete valori la instanţierea lui SFErrorMgr şi sunt utile la raportarea erorilor. De exemplu, utilizatorii ar putea raporta o eroare la versiunea 6.1 care a fost eliminată la versiunea 6.2, aşa că nu veţi fi nevoit să pierdeţi timpul verificând...

Au fost adăugate şi două noi clase - SFErrorMessageDialog şi SFErrorMessageDialogEmail în biblioteca SFERRORMGR.VCX. Clasa SFErrorMessageDialog arată ca în Figura 2, numai că nu are buton pentru e-mail. Ea ar trebui folosită dacă utilizatorul nu poate trimite e-mail-uri (sau dacă nu vreţi să primiţi e-mail-uri de la el, hi, hi, hi...). Metoda Init acceptă o referinţă către obiectul SFErrorMgr care a apelat-o şi o stochează în proprietatea oErrorMgr - vom vedea imediat la ce este folosită. Metoda Click a butonului Print apelează metoda PrintError a formularului. PrintError pune în nişte variabile valorile proprietăţilor cErrorMessage, cAppName şi cVersion (variabilele sunt private, nu locale, pentru fi vizibile în raport), crează un cursor temporar cu o singură înregistrare (din cauza raportului - trebuie să fie măcar un cursor deschis în zona de lucru pentru ca raportul să funcţioneze corect, chiar dacă nu foloseşte nimic din cursorul respectiv) şi apoi tipăreşte raportul ERROR.FRX. Acest raport este banal: tipăreşte doar nişte informaţii citite din variabilele menţionate mai devreme, plus alte chestii scrise "la mână", cum ar fi numărul de fax. Iată codul metodei PrintError:

LOCAL lnSelect
PRIVATE lcMessage, ;
	lcAppName, ;
	lcVersion
WITH This
	lnSelect = select()
	CREATE CURSOR TEMP (FIELD1 I)
	APPEND BLANK
	lcMessage = .cErrorMessage
	lcAppName = .oErrorMgr.cAppName
	lcVersion = .oErrorMgr.cVersion
	REPORT FORM ERROR NEXT 1 NOCONSOLE TO PRINT PROMPT
	USE
	SELECT (lnSelect)
ENDWITH

Clasa SFErrorMessageDialogEmail este o sublclasă a lui SFErrorMessageDialog care are şi un buton de e-mail. Ea are în plus câteva proprietăţi: cRecipient (destinatarul mesajului), cMessage (corpul mesajului) şi cSubject. cRecipient este scris direct în acest exemplu, dar o metodă mai deşteaptă este subclasarea lui SFErrorMgr şi în metoda SetDialogProperties se scrie valoarea corespunzătoare în această proprietate (poate interogând aplicaţia, fişierul de configurare sau chiar Windows Registry). cSubject este setat în metoda Init ca fiind "Error in" plus numele aplicaţiei şi versiunea, dar poate fi modificat după dorinţă în aceeaşi metodă (SetDialogProperties). cMessage este textul cErrorMessage, dar poate fi modificat şi el.

Metoda Click a butonul de e-mail apelează metoda SendEmail a formularului. Iniţial am fost tentat să folosesc un apel API către funcţia ShellExecute şi să "mailto:" mesajul ca în exemplul următor:

lcFile = FULLPATH(SYS(3) + '.TXT')
STRTOFILE(This.cErrorMessage, lcFile)
lcMessage = 'mailto:support@xxtechsupport.com' + ;
	'?Subject=' + This.cSubject + ;
	'&Attach="' + lcFile + '"' + ;
	'&Body=Vezi fişierul ataşat pentru detalii'
	ShellExecute(lcMessage) 

Din păcate, chestia asta nu funcţionează corect. Cu toate că trimite mesajul, este imposibil să îi agăţ un file-attach. Mai mult, de fapt nu trimite mesajul, ci doar afişează pe eran clientul implicit de e-mail, cu un mesaj nou şi toate valorile completate. Dar utilizatorul este cel care trebuie să facă un click pe butonul "Send". Pentru a evita situaţia aceasta, am folosit altă clasă, numită SFMAPI (care va fi descrisă într-o expunere viitoare, dar este inclusă în fişierul ataşat acestei expuneri).

Metoda SendMessage apelează metoda AddRecipient a obiectul SFMAPI (numit oMail în formular) pentru a scrie adresa destinatarului, apoi stabileşte subiectul mesajului şi restul proprietăţilor lui. După aceea face un lucru interesant: determină obiectul SFErrorMgr să instanţieze un formular care strânge toate informaţiile despre eroare într-un fişier text pe care îl ataşează mesajului. Drept rezultat, veţi primi mult mai multe informaţii, deoarece fişierul text conţine şi valorile variabilelor (obţinute cu ajutorul comenzii LIST MEMORY), informaţii despre trigger-ele care au generat eroarea, ş.a.m.d.). În fine, urmează trimiterea mesajului:

LOCAL lcAttachment, ;
	llLog, ;
	lcLog
This.cMessage = IIF(EMPTY(This.cMessage), ;
	This.cErrorMessage, This.cMessage)
WITH THIS.oMail
	.AddRecipient(This.cRecipient)
	.cSubject = ALLTRIM(This.cSubject)
	.cMessage = ALLTRIM(This.cMessage)
ENDWITH

* Rutina de tratare a erorii trebuie să creeze
* un fişier text cu informaţiile despre eroare.
* Acest fişier va fi ataşat mesajului.

lcAttachment = SYS(3) + '.TXT'
	WITH This.oErrorMgr
	llLog = .lLogToTable
	lcLog = .cErrorLogFile
	.lLogToTable = .F.
	.cErrorLogFile = lcAttachment
	.LogError()
	.lLogToTable = llLog
	.cErrorLogFile = lcLog
ENDWITH
This.oMail.AddAttachment(lcAttachment)

* Trimit mesajul.

This.oMail.Send()
ERASE (lcAttachment) 

Testarea clasei

TEST.PRG demonstrează cum se foloseşte rutina de tratare a erorilor şi cum se selecteaza clasa mesajului afişat. Acest program crează o instanţă a lui SFErrorMgr, stabileşte proprietăţile necesare, apoi apelează formularul TEST. Faceţi un click pe butonul "Click Me". Acest buton are două erori în metoda Click, aşa că veţi vedea mesajul de eroare.

PUBLIC oError
oError = CREATEOBJECT('SFErrorMgr', 'SFErrorMgr')
WITH oError
	.cAppName = 'Test Application'
	.cVersion = '1.0'
	.cReturnToOnCancel = 'test.prg'
	.cMessageClass = 'SFErrorMessageDialogEmail'
	.cUser = 'DHENNIG'
	.lShowDebug = .F.
ENDWITH
DO FORM TEST
READ EVENTS
CLEAR ALL

Încercaţi să modificaţi proprietatea cMessageClass, scriind în ea SFErrorMessage sau SFErrorMessageDialog pentru a vedea diferenţele. Dacă folosiţi SFErrorMesage, setaţi proprietatea lShowDebug la .T., pentru a vedea cum funcţionează opţiunea cu debugger-ul.

Concluzie

Dacă oferiţi utilizatorilor o metodă să raporteze erorile, vă faceţi dvs. înşivă o favoare. În primul rând depanarea lor este mai uşoară, iar în al doilea rând, aspectul aplicaţiei este muuuult mai profesional. De asemenea, utilizatorii vor şti că nu este vina lor. Sper ca aceste clase să vă fie de folos....

Descărcaţi Erori5.zip.


    

 Google Ads Minimize

    

Copyright 2002-2013 Profox   Terms Of Use  Privacy Statement