niedziela, 20 lutego 2011

Poziomy abstrakcji

Robert C. Martin w swej książce "Czysty kod" wspomina, iż prawidłowo skomponowane funkcje powinny być:
  • małe
  • wykonywać jedną czynność
  • zachowywać jeden poziom abstrakcji


O ile pierwsze dwa punkty łatwo sobie wyobrazić i zaimplementować, o tyle zauważyłem, że ostatni punkt jest nie do końca zrozumiały wśród niektórych programistów.

Aby unaocznić gdzie popełniany jest często błąd użyjmy prostej i każdemu znanej życiowej czynności:

Przepis na jajecznicę

  • Rozpuścić masło na małej patelni
  • Wrzucić na stopione masełko szynkę pokrojoną w małe kwadraciki oraz drobno posiekany szczypiorek
  • rozbić ostrym nożem skorupki jajek i zawartość wylewać na patelnię
  • Dodać trochę soli oraz pieprzu i mieszać, aż do momentu ścięcia się jajek

Przepis jest intuicyjny i raczej nikt nie powinien mieć problemu z jego zrozumieniem. Na tym poziomie abstrakcji wystarczy użyć kilku składników w prosty sposób i już mamy nasza potrawę gotową.

Jednakże "pod spodem" mają miejsce skomplikowane i (ze względu na nasz kulinarny model domenowy ) nieistotne zjawiska. Jak mógłby powyższy przepis wyglądać gdyby był częścią programu tworzonego przez nierozgarniętego programistę?


dodaj(maslo)

while(maslo.nieJestStopione){
  for( atom in atomyWProbceMasla){
   atom.dostarczEnergii
  }
}
dodaj(pokrojona Szynka)

while(szczypiorek.jestCaly){
  oddziaływuj nożem na sieć krystaliczną szczypiorku
}

dodaj(szczypiorek)

noż.dodajEnerigiiPotencjalnej
noz.zamieńEnergięPotencjalnąNaKinetyczną
noż.uderzW(jajko)

dodaj(zawartośćJajka)

dodaj(sól) 

for(ziarnkoPieprzu in szczyptaPipeprzu){
 dodaj(ziarnkoPieprzu)
}

i mieszać, aż do momentu ścięcia się jajek


(poprawność kwestii fizycznych może być niedokładna ale nie to jest meritum przykładu)


Tak zmieniony przepis o wiele trudniej zrozumieć i co gorsza domena kulinarna została zmieszana z domena fizyczną przez co potencjalny odbiorca przepisu musi zmierzyć się ze znacznie większą płaszczyzną problemu.

Aby zachować jeden poziom abstrakcji wracamy do korzeni:


patelnia.dodaj(maslo)
patelnia.podgrzejDoRostopieniaMasla()
patelnia.dodaj(pokroj(szynka))
patelnia.dodaj(pokroj(szczypiorek))
patelnia.dodaj(rozbij(jajko))
patelnia.dodaj(sól)
patelnia.dodaj(pieprz)
mieszajDoMomentuScieciaSieJajek(patelnia)


Poprawione źródło przepisu jest czytelniejsze i nie wymaga znajomości zagadnień wykraczających poza domenę kulinarną. Widzimy także ciekawy efekt wizualny, który może być użyty jako swoisty test czy nie wykraczamy poza dany obszar abstrakcji : mianowicie wersja poprawiona nie ma wcięć.

Jak przedstawione zjawisko mogłoby wyglądać na przykładzie bardziej zbliżonym do IT?


funkcja(obiekt){
operacjaDomenowa();
bebechy=obiekt.wyciągnijBebechy
...
kilka zagnieżdzonych pętli i try-catch
...
operacja domenowa()
znowuGrzebanieWBEbechach()
}


przykład konkretny : obsługujemy request od przeglądarki, który może zawierać obrazki i załączniki tekstowe. Każdy typ załącznika musimy zapisać do osobnego katalogu.


przetworzRequest(request){
   zapiszPlikiTekstowe(request)
   zapiszObrazki(request)
}



A można i tak :


przetworzRequest(request){
 zapiszPlikiTekstowe(request)

 strumien=OtworzStrumien
 for(bit in obrazek){
   strumien.pisz(bit)
 }
 strumien.zamknij
}



Chyba jest jasne,która wersja będzie czytelniejsza,przyjemniejsza,wygodniejsza i łatwiejsza w utrzymaniu osobie, która ujrzy ów fragment po raz pierwszy w życiu.