În cele două articole anterioare din TSM- Patru idei pentru îmbunătățirea software design-ului și Usable software design-am arătat cum să creăm un software design mai bun și am definit ideea de usable software design. Designul Software Utilizabil provine din observația simplă că dezvoltatorul este utilizatorul unui design software. Ipoteza mea este că utilizând principiile și practicile din Utilizabilitate în designul software se vor crea două beneficii importante: un timp mai rapid de implementare pentru sarcinile comune și integrarea mai rapidă a dezvoltatorilor noi într-o echipă existentă.
În acest articol, voi explora mai departe designul software utilizabil plecând de la ideea simplă că deși nimănui nu îi place să facă greșeli, acestea totuși apar. Deoarece designul software utilizabil înseamnă un design software care provoacă încântare când este utilizat de către dezvoltatori, ceva trebuie făcut pentru a preveni erorile. Așadar vreți ca designul software al vostru să fie lipsit de greșeli pentru a fi mai utilizabil?
Dar mai întâi, trebuie să înțelegeți un lucru important…
În 1988, un cercetător cognitiv și-a asumat sarcina de a analiza modul în care sunt concepute obiectele de uz zilnic. Profesorul Donald Norman a explorat designul centrat pe utilizator în cartea sa "Designul lucrurilor uzuale" ("The Design of Everyday Things"), pornind de la psihologie:
Un ciclu vicios începe: dacă greșești la ceva, tu crezi că este vina ta. De aceea, consideri că nu poți duce la bun sfârșit acea sarcină. Drept rezultat, următoarea dată când trebuie să realizezi sarcina respectivă, tu crezi că nu poți și de aceea nici măcar nu mai încerci. Rezultatul este că nu poți, exact cum ai crezut. Ai căzut în capcana unei profeții care se autoîmplinește.
- Donald Norman, " The Design of Everyday Things"
Iar soluția:
Este timpul să inversăm situația: să dăm vina pe aparate și pe designul lor. (…) Este de datoria aparatelor și a celor care le proiectează să înțeleagă oamenii.
- Donald Norman, " The Design of Everyday Things"
Aceasta înseamnă că...
Imaginați-vă următorul scenariu: descoperiți că Ionuț, Programatorul Junior, a făcut o greșeală când a lucrat la o sarcină. Vă dați seama că și alții au comis aceeași eroare înainte, iar soluția a fost documentată. Care este reacția voastră?
(Pun pariu că Ionuț din echipa voastră nu este chiar atât de junior!)
Îi spuneți că există documentație pentru asta și îi indicați unde să citească.
Îi explicați cum se face.
Eu obișnuiam să reacționez în modul 1 și 2 și uneori încă le mai fac asta. Vechile obiceiuri presupun efort pentru a fi schimbate. Spre deosebire de acum cinci ani, acum înțeleg că aceasta ar putea crea un cerc vicios. Iată cum:
Cum se va simți Ionuț, Programatorul Junior, dacă răspunsul vostru este 1 sau 2? Reacția psihologică involuntară este să se simtă vinovat. Repetați scenariul de câteva ori și el va înceta să pună designul la îndoială. Drept consecință, veți avea un ciclu vicios.
Ceea ce m-a învățat Domnul Norman în schimb este că ar trebui să consider această situație drept vina sistemului și nu vina dezvoltatorului. Deci următorul lucru important este să îți dai seama cum să îmbunătățești sistemul astfel încât eroarea să nu se mai repete.
Iată trei moduri de a vă îmbunătăți designul pentru a preveni erorile.
Uneori, metodele dau naștere excepțiilor atunci când sunt apelate într-o ordine diferită decât ar trebui. În anumite cazuri, este posibil să înlăturați complet excepțiile prin reproiectarea clasei. Iată un exemplu:
La diferite evenimente de măiestrie software în care exersăm tehnici de codare, am folosit TicTacToe drept o problemă. În mod tipic, sfârșim prin a avea o clasă Game, pe care cei mai mulți dezvoltatori o concep după cum urmează:
class Game{
...
moveX();
moveO();
...
}
Acest design duce la o potențială eroare: nimic nu mă împiedică să scriu următorul cod:
Game game = new Game();
game.moveO();
game.moveO();
game.moveO();
care este greșit, potrivit regulilor TicTacToe. Jucătorul X ar trebui să înceapă, apoi jocul ar trebui să continue cu mișcări alternante.
Răspunsul implicit al dezvoltatorilor care se confruntă cu această problemă este să modifice implementarea în ceva similar cu:
void moveX(){
if(currentPlayer != Player.X){
throw new NotTheTurnOfThePlayerException();
}
}
Dar aceasta tot nu mă împiedică să scriu codul de mai sus. Este un pic mai bine, deoarece mă avertizează că am făcut ceva greșit. Totuși, aș argumenta că descoperirea greșelilor mele la execuție este prea târziu. A face un software rezistent la greșeli înseamnă să concepem sistemul astfel încât să fie (aproape) imposibil să îl utilizăm greșit.
Consider că următorul design este mai rezistent la greșeli:
class Game{
Game(Player playerX, Player playerO);
move();
...
}
Acest design conduce în mod tipic la un cod similar cu:
Game game = new Game(playerX, playerO);
game.move();
Nu văd nicio cale de a utiliza acest design altfel decât ar trebui. Nu numai că este ușor de utilizat, dar este de asemenea și ușor de învățat și rezistent la greșeli.
Clasa Game poate fi utilizată numai într-un singur fel, la fel cum există un singur mod de a introduce un card de memorie în slotul lui.
O greșeală comună este să creezi un obiect fără toți parametrii obligatorii. Dacă mai târziu este apelată o metodă, apar erorile.
De exemplu, păstrând problema TicTacToe:
Game game = new Game();
game.move(); // players have not been added to the game
TicTacToe poate fi jucat numai de către doi jucători, fie ei oameni sau computer. Probabil există și jocuri TicTacToe cu mai mult de doi jucători, dar nu îmi pot imagina un TicTacToe solitar.
De aceea, este normal să exprimăm această constrângere în constructor:
Game game = new Game(firstPlayer, secondPlayer);
Chiar dacă mai târziu decidem să implementăm versiunea TicTacToe cu mai mult de doi jucători, este ușor:
game.addPlayer(thirdPlayer);
game.addPlayer(fourthPlayer);
game.move();
Indiciu: Acest obiect a fost implicat
Obsesia primitivă este un iz de cod foarte comun, pe lângă faptul că are un nume foarte sugestiv. A fost de asemenea și sursa unei pierderi de 125 milioane $ într-una dintre rarele ocazii în care se pot măsura pierderile cauzate de probleme de software.
Ward Cunningham discută despre asta:
Izul (the Smell): Obsesia primitivă utilizează tipuri de date primitive pentru a reprezenta ideile de domeniu. De exemplu, noi folosim un Șir (String) pentru a reprezenta un mesaj, un Întreg (Integer) pentru a reprezenta o sumă de bani, sau un Struct/Dicționar/Hush pentru a reprezenta un obiect specific.
Repararea: În mod tipic, noi introducem un ValueObject în locul datelor primitive, apoi privim ca la o magie cum codul din întreg sistemul indică FeatureEnvySmell și vrea să fie pe noul ValueObject. Mutăm acele metode și totul devine corect.
În cazul TicTacToe, este foarte tentant să scriem cod precum:
game.move("A1");
sau ca și acesta:
game.move(0, 0);
Există multe probleme cu acest design. Nimic nu mă împiedică să trimit coordonate greșite cum ar fi game.move(-1, 2000) sau game.move("Z9"). Pentru a evita problemele, va trebui să răspândim validări pe tot parcursul codului. În primul caz, procesarea șir va fi răspândită în jurul codului, fiind ușor să introducem erori off-by-one atunci când facem procesare în șir. Când cazurile limită (corner cases) sunt validate cu teste unitate, va trebui să repetați testele pe unitate pentru coordonate valide/ invalide în fiecare clasă pe care le utilizează.
Există o modalitate de a evita toate acestea: indiferent de cum introduceți coordonatele, convertiți-le imediat într-un obiect valoare. În cazul TicTacToe, domeniul problemei poate fi descris ușor: Panoul TicTacToe este format din 9 Locuri care au Coordonate, fiecare de la 1 la 3. Deci, de ce nu:
Place place = new Place(Coordinate.One, Coordinate.One);
game.move(place);
Utilizatorul acestui design nu mai poate apela metoda move() cu parametri greșiți.
Oamenii care utilizează un sistem cu un design prost tind să se autoînvinovățească în loc să dea vina pe sistemul pe care îl folosesc. Eu afirm că acest lucru se întâmplă atât dezvoltatorilor de software care folosesc un design software existent, cât și utilizatorilor de obiecte fizice făcute de om. Donald Norman ne indică o cale de ieșire: ca designer, el înțelege că este în mod tipic vina sistemului și proiectează sistemul având în minte toleranța față de greșeli. Am văzut trei metode de a ne îmbunătăți designul unei clase pentru a fi mai rezistentă la greșeli: eliminarea excepțiilor, trecerea argumentelor obligatorii în constructor, evitarea obsesiei primitive. Am văzut că rezultatul este mai ușor de învățat, mai ușor de utilizat și în același timp evită erorile obișnuite.
Când nu îți poți proiecta interfețele pentru a preveni greșelile, Design By Contract vine în ajutor. Vă recomand să citiți despre aceasta, drept o altă metodă de a-ți face designul să nu fie permisiv la greșeli.
Acest articol s-a concentrat pe cum să îți faci designul software rezistent la erori prin utilizarea unor elemente de design software. Realitatea este mai complexă: pentru a face designul software să nu permită greșelile este cu siguranță nevoie de un feedback rapid oferit de programarea pereche, teste automatizate, integrare continuă și sprijin IDE.
Ce tipuri de greșeli faceți voi? Ce faceți pentru a le preveni? Aștept comentariile voastre.
Vă invit pentru o discuție mai aprofundată în cadrul Workshop-ului de Usable Software Design Vineri, 29 Mai, în cadrul I T.A.K.E. Unconference 2015, la Radisson Blu Hotel, Bucuresti. Au mai rămas doar câteva bilete.
de Ovidiu Mățan
de Vlad Ciurca
de Ștefan Bălan
de Ioana Varga , Ioana Costea