Proiectare prin contract

Dezvoltarea software-ului modern este însoțită de o serie de acțiuni menite să-i îmbunătățească calitatea. O astfel de metodă este proiectarea contractelor.







Ca orice altceva, această metodă nu este un panaceu pentru erorile de proiectare și de codificare, dar ne permite să formalizăm în mare parte aceste două procese. Formalizarea include, de asemenea, relațiile dintre dezvoltatori, deoarece această metodă aparține grupului de autocunoașteți: specificațiile sunt descrise direct în program și nu în documentele însoțitoare.

"Design Contract" este traducerea termenului englez "Design by Contract *". Un alt termen pe care îl vom avea nevoie este "afirmația" - o afirmație.

Istoria mecanismului examinat a început cu o jumătate de secol în urmă. În 1950, la o conferință de la Cambridge, Alan Turing a sugerat includerea în cerințele programului care trebuie verificate în timpul executării și din care se poate deduce corectitudinea întregului program. Ideea unor astfel de cerințe a fost apoi luată în considerare de Floyd, Hoar, Dijkstra și alții (se pot aminti faimoasele triplete Hoare, descrise nu într-o singură carte despre dezvoltarea programelor corecte).

Pentru a specifica aceste cerințe, s-au dezvoltat limbi de specificare a specificațiilor formale, cum ar fi Z și VDM, precum și versiunile orientate pe obiecte. Cu toate acestea, în ceea ce privește limbile de programare, practic nu au oferit sprijin pentru descrierea cerințelor la nivelul limbii. Cel mai vechi limbaj de programare cu un astfel de sprijin a fost Algol W. Statele Unite au sprijinit descrierea, care a fost predecesorul a ceea ce a fost dezvoltat pentru Eiffel, dar în CLU aceste descrieri erau imposibil de realizat. Însăși conceptul de "design contract" a fost introdus pentru prima dată de Meyer în 1987 și este susținut în Eiffel. De atunci, a atras atenția sporită din partea dezvoltatorilor altor limbi de programare.

Chiar mai devreme în caietul de sarcini Java includ suport integrat pentru mecanismele de bază ale punerii în aplicare a „design-contract“, dar din cauza calendarului strâns, îngrădește eliberarea primei versiuni, acestea au fost mai târziu a scăzut (ca, într-adevăr, și o serie de alte mecanisme orientate-obiect).

3. Protejează "programarea de protecție"

„Programarea defensivă“ este cunoscut pentru o lungă perioadă de timp: programul trebuie să fie scris, astfel încât acesta este „inteligent“ sa comportat cum era de așteptat, și nu pe datele de intrare așteptate, care este, astfel încât să nu „cadă“ sub nici o intrare. De fapt, în cazul în care acesta nu este un produs finit, ci o bibliotecă de clasă, care poate fi utilizat în mai mult de un proiect, și în diferite, această abordare nu numai că nu contribuie la dezvoltarea, ci, dimpotrivă, împiedică în mod substanțial.

Să luăm în considerare un exemplu simplu. Să se solicite scrierea unei funcții pentru calculul rădăcinii pătrat: sqrt (x: REAL): REAL

-- Returnează rădăcina pătrată a lui "x"

Se pune întrebarea: funcționează această funcție cu argumente negative? În urma regulii de "protecție de la un nebun", este necesar să se prevadă un astfel de tratament. Din păcate, nu este clar ce. Puteți reveni la argumentele negative ale propriei lor, sau pentru a calcula pentru ei rădăcina valorilor opuse ale argumentului, sau să emită un mesaj de eroare (cu variante: consola, în caseta de dialog), sau ridica excepții, sau. Acum să ne imaginăm că trebuie să calculam expresia arcsin (sqrt (In (x)) / y)

Imaginați-vă că diferite funcții au fost scrise de oameni diferiți folosind "programarea defensivă". Pentru a răspunde la întrebare, care este numărul tuturor variantelor posibile ale comportamentului funcției cu argumente inadmisibile, este lăsat cititorului.

4. Precondiții și postcondiții

-- Returnează rădăcina pătrată a lui "x" - "x" nu trebuie să fie negativă

-- Returnează rădăcina pătrată a lui 'x' necesită non_negative_x: x = 0

Aici "necesită" înseamnă o secțiune de precondiție, următoarea linie este una dintre precondițiile cu eticheta "non_negative_x" și expresia calculată "x = 0 ". Dacă sunt date mai multe precondiții, atunci pentru execuția normală a programului toate trebuie să fie adevărate. În caz contrar, se va ridica o excepție.

Acum, argumentul este verificat pentru non-negativitate automat de fiecare dată când se numește funcția sqrt și va fi imediat raportată o încălcare a acestei condiții. Funcția noastră este destul de simplă, iar precondiția este evidentă. În cazuri mai complexe, întrebarea este legitimă: este întotdeauna posibilă găsirea unei condiții prealabile adecvate? Din păcate, răspunsul este nu. De exemplu:

• Setarea precondiției unei funcții este echivalentă cu calculul funcției însăși, pentru care este specificată (să găsească o valoare a cărei factorială este egală cu cea specificată);

• sarcina este destul de complicată pentru o specificare completă formală;

• O condiție prealabilă formală necesită introducerea unui număr mare de funcții auxiliare care nu au nici un sens dincolo de sfera acestei precondiții și așa mai departe.

-- Returnează rădăcina pătrată a 'x' necesită non_negative_x: - 'x' trebuie să fie - nonnegative

-- Returnează rădăcina pătrată a lui "x" - Rezultatul nu este negativ. Necesită non_negative_x: x = 0, dar, urmând stilul formal, puteți rescrie acest lucru: sqrt (x: REAL): REAL

-- Returnează rădăcina pătrată a lui 'x' necesită non_negative_x: x gt = 0 asigură non_negative_result: Rezultat = 0

Acum, funcția sqrt nu numai că impune restricții asupra argumentului, dar asigură, de asemenea, că sunt îndeplinite constrângerile asupra rezultatelor. Cu alte cuvinte, implementarea acestei funcții asigură că rezultatul nu este negativ pentru un argument non-negativ. Cu toate acestea, funcțiile cu această proprietate includ nu numai funcția rădăcină pătrată. De exemplu, o funcție care întoarce întotdeauna 5 va funcționa, de asemenea. Aceasta înseamnă că postcondiția, și anume așa-numita condiție, găsită în secțiunea de asigurare și evaluată după execuția corpului funcției, nu este suficientă. Este necesar să adăugăm că pătratul rezultatului trebuie să fie egal cu argumentul. Opțiunea dorită este prezentată mai jos: sqrt C: REAL): REAL

-- Returnează rădăcina pătrată a lui 'x' necesită non_negative_x: x gt; = 0 asigurați ne_egative_resuIt: Rezultat = 0 definiție: Rezultat l 2 - x

De fapt, am stabilit condiția prealabilă pentru domeniul definiției funcției, iar în postcondiția, domeniul valorilor sale și relația dintre argument și rezultat. În cazul general, precondiția determină când metoda poate fi executată în mod normal, iar postcondiția - ceea ce este garantat după ce este executat.

Să facem acum implementarea: sqrt (x: REAL): REAL este

-- Returnează rădăcina pătrată a lui 'x' necesită non_negative_x: x gt; = 0 dacă x = 0 atunci

-- calcularea rădăcinii pătrate altfel

-- gestionarea erorilor se încheie

Ceva nu este în neregulă aici: la urma urmei, am încercat doar să evităm orice manipulare a erorilor. Principiul "non-redundanței" spune: nu puteți verifica în corpul de rutină condiția specificată în precondiția sa. Este atât de adevărat. Ce se întâmplă dacă precondiția este încălcată? Vom răspunde la această întrebare în secțiunea următoare.

Mai întâi revenim la noțiunea de "proiectare, dar contract". Cuvântul "contract" definește în primul rând clar că este vorba de formalizarea relației dintre anumite părți. În acest caz, o parte este numită "furnizor", iar cealaltă este "client". Furnizorul se obligă să furnizeze o anumită funcționalitate clientului. Dar numai atunci când toate precondițiile sunt îndeplinite. În ceea ce o privește, garantează că după execuție vor fi îndeplinite toate condițiile ulterioare. Astfel, o diviziune clară a drepturilor și responsabilităților.







Din acest motiv, programul în sine este simplificat, deoarece postcondițiile unei rutine pot fi precondițiile ce urmează, ca și în In (sqrt (x)).

După furnizarea condiției x = 0, nu este nevoie să introduceți verificarea argumentului In. Prin definiție, sqrt trebuie să returneze un rezultat ne-negativ.

Un alt avantaj al contractului este că nu va fi necesar să căutați vinovatul în cazul unei încălcări a pre- sau postconditionării. Dacă precondiția este încălcată, atunci clientul nu a furnizat executarea acesteia, iar eroarea se află în codul clientului. În cazul în care postcondiția este încălcată, furnizorul nu și-a îndeplinit obligațiile și eroarea se află în codul furnizorului. În orice caz, cu astfel de încălcări, va apărea o situație excepțională. Cu toate acestea, există o subtilitate aici.

Dacă condiție prealabilă este încălcată, atunci o excepție este ridicată în corpul clientului, deoarece furnizorul nu poate afecta argumentele transmise acestuia, și starea generală a sistemului înainte de accesarea acestuia. Dacă postconditia este încălcat, atunci o excepție este ridicată în corpul furnizorului, deoarece el poate avea informații suplimentare cu privire la modul în care să se ocupe de această situație sau chiar a restabili o stare normală și să încerce încă să își finalizeze activitatea, folosind, de exemplu, un algoritm diferit.

Totuși, nu trebuie să credem că declarațiile (la care se aplică pre- și postcondițiile) pot fi folosite ca mijloc de procesare a cazurilor speciale. Următorul fragment de cod poate fi considerat un obiectiv dorit, dar imposibil de atins: read_user_input este

-- Citiți datele de intrare până la - până când sunt introduse corect

-- citirea datelor asigură valid_input - condiție postcondiționată: datele - salvarea corectă introdusă - reluarea manualului de excepție - reîncercați execuția corpului - sfârșitul procedurii curente

Se pare că totul ar trebui să funcționeze așa cum ar trebui: datele sunt citite, condiția postcondiționată este verificată, dacă nu este executată, atunci se ridică o excepție, operatorul de excepție pornește mai întâi executarea corpului procedurii. Ieșirea din această bucla implicită are loc numai atunci când datele sunt introduse corect. Dar același efect poate fi obținut folosind instrucțiuni condiționale și instrucțiuni de buclă. Prin urmare, declarațiile nu pot fi considerate ca un mijloc de organizare a fluxului de control în program. Încălcarea oricărei declarații este un semnal despre existența unei erori în program. Mai exact, încălcarea precondiției - un semnal despre eroarea în client, încălcarea postcondiției - un semnal despre eroarea furnizorului.

Rețineți că precondițiile și postcondițiile se referă la interfață și nu la implementare. Într-adevăr, este necesar ca utilizatorul clientului să știe cum să folosească această proprietate a acelei clase și ce să se aștepte de la ea. Este nevoie de cel puțin informații atât de multe ca și numele proprietății, tipul acesteia, numărul de argumente și tipul acestora, descrierea verbală a proprietății. În acest scop, limbajul Eifel a introdus conceptul unei forme scurte a clasei, în care toate detaliile implementării și proprietățile neexprimabile ale clasei sunt omise. În descrierile tuturor celorlalte proprietăți există atât precondiții, cât și postcondiții. O scurtă formă a clasei este generată cu ajutorul unor utilități speciale din codul sursă și servește ca documentație pentru alți dezvoltatori.

6. Declarații despre proprietățile unui obiect

De multe ori trebuie să se confrunte cu obiecte pe tot parcursul ciclului lor de viață, păstrează o parte din proprietățile la fel. Ia-o listă de două ori legată (pentru simplitate afișate numai atributele de clasă): caracteristica BILINKED_LIST clasa - Acces la elemente învecinate următor: BILINKED_LIST - următorul element prev: BILINKED_LIST - sfârșitul articolului precedent - clasa BILINKED_LIST

Pentru orice două noduri x și y astfel încât x. următorul = y, y.prev = x va fi, de asemenea, executat. Și această proprietate este păstrată pe întreaga durată de viață a listei duble conectate. Pentru a exprima această proprietate, există o declarație corespunzătoare numită "clasă invariantă". Descrie condițiile care trebuie să fie întotdeauna adevărate. După adăugarea de text secțiunea clasa invariante ar fi după cum urmează: caracteristica BIL.INKED_L.IST clasa - Acces la elementele învecinate următor: BILINKED_LIST - următorul element anterioare: BILINKED_LIST - elementul precedent consistent_next invariante: următor / = nu apare presupune next.prev = consistent_prev curent: Prec / = nu apare presupune prev.next = sfârșitul curent - clasa BILINKED_LIST

Aici curent se referă la obiectul curent (un sinonim pentru acest lucru în Java și C + +).

Deoarece invarianta clasei trebuie să fie întotdeauna adevărată, ea este testată înainte și după proprietatea furnizorului. Rezumând toate cele de mai sus, obținem următoarea schemă (pentru cei care sunt familiarizați cu tripletele Hoare, se va părea foarte asemănător cu ei):

Adică, înainte de executarea corpului de rutină, clasa invariantă și precondiția acestei rutine trebuie îndeplinite. După executarea corpului de rutină, trebuie îndeplinită condiția ulterioară a rutinei date și invariabilitatea clasei.

Astfel, pentru ca invarianta clasei să fie executată după execuția corpului de rutină, ea trebuie executată înainte de a fi executată. Aceasta înseamnă că invarianta trebuie îndeplinită înainte de execuția cadrului rutinei anterioare și, prin urmare, înainte de execuția precedentă față de cea anterioară, etc. Unde se termină acest lanț? Pentru a răspunde la această întrebare, luați în considerare problema de cealaltă parte. Ce rutină se efectuează mai întâi? Desigur, procedura de creare a unui obiect (constructor).

Chiar înainte de a fi chemat, obiectul nu a fost încă creat și nu poate fi vorba despre un invariant al clasei de vorbire. De fapt, procedura de creare are o schemă de corectitudine puțin diferită:

Aici, DEFAULT înseamnă starea obiectului imediat după inițializare, dar înainte de procedura de creare (de exemplu, în Eiffel, toate atributele obiectului sunt resetate la zero în această stare). Din cele de mai sus, putem trage o concluzie remarcabilă cu privire la rolul procedurii de creare a unui obiect: asigură implementarea invarianților clasei. Toate celelalte proprietăți ale obiectului se află într-o poziție mai favorabilă: trebuie doar să mențină această stare.

Până acum, am luat în considerare exemple de atribuire a contractelor fără a ține cont de structura ierarhică a moștenirii de clasă. Cu o astfel de structură, totul devine puțin mai interesant. Mai întâi ne întoarcem la invarianta clasei. Să presupunem că, în loc de o listă simplă, avem nevoie de o listă ordonată. O posibilă implementare este prezentată mai jos: class SORTED_LIST inehrit

BILINKED_LIST - lista este dublu conectată COMPARABIL - Elementele din listă sunt sortate invariant prev_is_less:

-- Elementul anterior nu mai este curent prev / = Verificarea nevalidă prev lt = Current next_is_greater:

-- Următorul element nu este mai mic decât cel curent următor / = Void next next gt; Clasa curentă curentă S0RTED_LIST

Evident, BILINKED_LIST clasa de client, și rutine care sunt moștenite de la BILINKED_LIST, au dreptul să se aștepte că adăugarea unui copil la BILINKED_LIST nu le va afecta (ele pot fi chiar conștienți de astfel de descendenți). Prin urmare, invarianta de clasă este de fapt unirea „și“ invariante directe și invarianți strămoșilor săi. Drept urmare, descendentul nu poate distruge contractele strămoșilor.

Plecând de la același principiu, sunt construite pre- și postcondiții. Să ne întoarcem la un alt exemplu. Să fie necesară implementarea unei clase pentru sortarea matricei. Cel mai simplu sortare, care vă permite să sortați numai matricele cu perechi de elemente diferite, poate arăta astfel: clasa SIMPLE_S0RTER [G -gt; COMPARABILITATE] - Sortare sortare (date: ARRAY [G]) este

-- date de filtrare matrice nr - Crescator necesită non_void_data: date / = Void - array există different_elements: all_different (date) - toate elementele de matrice sunt diferite asigura sorted_data: is_sorted (date) - la ieșirea din matrice este sortat final - clasa SIMPLE_S0RTER

Clientul poate folosi variabila sortare de tipul SIPLE_S0RTER pentru a sorta datele sale: sorter.sort (my_data)

Deoarece clientul nu cunoaște tipul dinamic al variabilei sortare, acesta poate satisface doar acele precondiții care sunt declarate în clasa SIMPLE_SORTER. Prin urmare, orice modificare a contractului în condiții prealabile le poate doar "simplifica", făcându-le mai ușoare. Prin urmare, când suprascrieți o proprietate a clasei, precondițiile sunt combinate utilizând operația OR. Pentru a accentua acest lucru, cuvântul cheie altceva este adăugat la cuvântul cheie solicitat. Pe de altă parte, clientul se poate aștepta ca postcondiția să fie executată indiferent de tipul dinamic al sorterului. În consecință, postcondițiile, ca și invarianții, trebuie combinate în conformitate cu "eu". În acest caz, după asigurare, atunci se adaugă. De exemplu, o clasă care permite deja să sortați matrice cu elemente repetate, în cazul în care nu sunt egale cu Nul, și asigură că tipul va fi stabil, poate arata astfel: clasa ROBUST.SORTER [G -gt; COMPARABILITATE] moștenire

SIMPLE_SORTER redefinește sortarea - procedura de sortare este înlocuită - în această clasă, sortarea funcțiilor de sfârșit (date: ARRAY [G]) este

-- Sortare o matrice de date -PO Crescator necesită non_void_data altceva: date / = Void - matrice există non_void_elements: nu are (date, void) - nu există elemente goale asigură apoi stable_data: is_stable (clona vechi (date), date) - - sortarea este stabilă de clasă finală R0BUST_S0RTER

Precondiția completă de sortare în R0BUST_S0RTER este:

(date / = Void și thea all_different (data)) sau altceva (date / = Void și apoi nu are (date, Void))

Dar post-condiție (proiectare vechi face referință la valoarea în picioare, după expresia calculată înainte de începerea procedurii): is_sorted (date) și apoi is_stable (vechi clona (date), date)

Un alt aspect al utilizării declarațiilor este prețul care trebuie plătit pentru program. La urma urmei, mărimea programului crește, iar viteza de execuție scade și, probabil, foarte semnificativ. Răspunsul este simplu: când eliberați programul este compilat astfel încât să nu fie incluse în modulul obiect declarații. Declarațiile sunt relevante numai la momentul depanării și ca mijloc de auto-documentare. Încălcarea lor este un semnal despre o eroare în program. Când depanarea este terminată, nu există o încălcare a declarației și, deoarece declarațiile nu au (sau mai degrabă nu ar trebui să aibă) efecte secundare, excluderea acestora din program nu afectează funcționarea acesteia în nici un fel.







Articole similare

Trimiteți-le prietenilor: