czwartek, 29 grudnia 2011

Clean code - rules & tools

1. Wprowadzenie

Dokument przedstawia zasady tworzenia kodu, który obok realizowania założone funkcji, spełnia dodatkowe kryteria, takie jak czytelność, możliwość rozwijania, powtórnego wykorzystania, łatwość testowania, odporność na błędy (readability, extensability, reusablity, testabilitiy, robustness).

Oczywiście temat ten nie jest w tym dokumencie zamykany. O zasadach tworzenia czystego kodu powstało wiele opracowań (najbardziej godne polecenia to (4) Fowlera). Zagadnienia, które zostaną opisane można najogólniej zawrzeć w 4 punktach: konwencje nazewnicze, komentarze i dokumentowanie kodu, przekształcenia refaktoryzacyjne i narzędzia do zautomatyzowanej analizy i poprawy kodu.

W "Refactoring: Improving the Design of Existing Code" (4) w pierwszym rozdziale znajduje się zdanie: "Każdy idiota napisze kawałek kodu zrozumiały dla kompilatora. Dobry programista napisze coś zrozumiałego dla drugiego człowieka". Może to posłużyć jako motto całego artykułu.

2. How does it smell?

"Code smells" to termin wprowadzony przez Kenta Becka. Oznacza konstrukcje w kodzie, które nie spełniają założeń opisanych w pierwszym punkcie. Innym określeniem tego samego zjawiska są "złe praktyki". Tworząc kod unikamy takich konstrukcji, a także staramy się poświęć czas na refaktoryzację stworzonego rozwiązania.

Konkretne przykłady zostały zaczerpnięte z wykładów z (3), które stanowią uporządkowaną i usystematyzowany zbiór przykładów do tez zawartych w (4).

Zanim przytoczę wszystkie przykłady warto zwrócić uwagę na istotną kwestię. Celem tworzenia kodu jest realizacja pewnej funkcjonalności. Przekształcenia refaktoryzacyjne nie mogą w żadnych stopniu wpływać na jej poprawność. Czystość kodu jest ważna, ale nie jest priorytetem - wkład w jej zachowanie nie może przesłonić podstawowego celu pisania.

a. Symptomy złego kodu

i. Powielenie kodu - Identyczny lub podobny kod znajduje się w wielu miejscach systemu.
Rozwiązanie - wyłączenie metody, przeniesienie kodu do klasy nadrzędnej, delegacja wywołania metody do innej klasy

ii. Długa metoda - Funkcja ma realizować jedną i tylko jedną funkcję. Każda funkcja, która realizuje wiele czynności jest "dluga". Problem może wynikać ze złej architektury systemu - braku podzialu na warstwy
Rozwiązanie - wyłączenie metody, usuwanie zmiennych lokalnych i zmniejszanie listy parameterów poprzez wprowadzanie nowych klas

iii. Duża klasa - Opisana w artykule (5) zasada dotycząca klas wyraźnie narzuca ograniczenie pojedynczej klasy do pojedynczej odpowiedzialności.
Rozwiązanie - wyróżnienie poszczególnych odpowiedzialności i stworzenie adekwatnych klas, następnie odwoływanie się do tych klas za pomocą referencji, dziedziczenia lub polimorfizmu

iv. Długa lista parametrów - Często związana z sytuacją zbyt dużej metody. Funkcja otrzymuje zbyt wiele danych.
Rozwiązanie - wprowadzenie klasy zawierające zbiór parametrów przekazywany do funkcji, rozważenie podziału funkcji

v. Nadmiar komentarzy - Jeżeli czujesz potrzebę wyjaśniania działania swojej metody, zastanów się najpierw, czy nie istnieje łatwiejsze rozwiązanie problemu, które nie będzie wymagać wyjaśnień. W radykalnym pojdejściu do tej zasady kod w ogóle nie powinien zawierać komentarzy - dzięki jego przejrzystości i przestrzeganiu zasad powinien "komentować się sam".

vi. Skomplikowane instrukcje warunkowe - Metoda zawiera złożoną, wielopoziomową instrukcję if lub switch. Ogólnie zbyt zagnieżdżona instrukcja zawsze powoduje problem z czytelnością, należy zatem takich konstrukcji unikać

vii. Łańcuchy wywołania metod - Naruszenie "prawa Demeter" (6). Klasa w swoich metodach powinna korzytać tylko z metod do których posiada referencje. Innymi słowy należy ufać tylko swojemu bezpośredniemu sąsiadowi.
Rozwiązanie - wyodrębnienie metod w klasie sąsiedniej i ukrycie w nich nadmiarowych wywołań

viii. Pojemnik na dane - Klasa, której jedyną funkcją jest przechowywanie danych.

ix. Zbitki danych - Zbiory danych występujących wspólnie i mających logiczne powiązanie (np. "Autor", "Tytuł", "Wydawnictwo")

x. Odrzucony spadek - Klasa nie wykorzystuje metod i pól, które zawarte są w nadklasie

xi. Niewłaściwa hermetyzacja - Klasa odwołuje się do wewnętrznych pól innej klasy
Rozwiązanie - Przesunięcie metody do właściwej klasy

xii. Bezużyteczna klasa - Klasa nie realizująca żadnej odpowiedzialności. Przykładem jest "Pojemnik na dane".
Rozwiązanie - rozdzielić pola pomiędzy istniejące klasy, usunąć klasę

xiii. Zazdrość o funkcje - Metoda zdecydowanie częściej odwołuje się do funkcji spoza swojej klasy macierzystej. Sytuacja analogiczna do "Niewłaściwej hermetyzacji". Oba problemy są związane z łamaniem ogólniejszej zasady pojedynczej odpowiedzialności. Metody odwołujące się do "obcych" pól czy metod powinny należeć do innych klas
Rozwiązanie - przeniesienie metody do właściwej klasy

xiv. Zmiana z wielu przyczyn - Stan klasy jest modyfikowany z pod wpływem wielu czynników. Ponownie występuje problem z zasadą pojedynczej odpowiedzilaności.

xv. Spekulacyjne uogólnienie - Tworzenie abstrakcji nie mającej uzasadnienia w obecnych założeniach biznesowych.

b. Przekształcenia refaktoryzacyjne

Pełna lista przekształceń znajduje się w (4). Plecam eksperymetowanie z poszczególnymi przekształceniami za pomocą np. ReSharpera (menu ReSharper -> Refactor). Przykładowe przekształcenia to m.in. "Extract method", "Add parameter", "Move method", "Replace conditional with polymorphism". W następnym punkcie przedstawiony zostanie ciąg przekształceń dla kodu realizującego funkcjonalność wypożyczalni filmów. Przykład został zaczerpnięty z (4).

3. Scenariusz refaktoryzacyjny.

Rozważmy funkcjonalność składającą się z trzech klas - Movie, Rental i Client. Pozwalają one na obliczanie opłaty za korzystanie z wypożyczalni filmów. Klasa Client zawiera następującą metodę obliczającą wspomniany koszt:


public String statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements())
{
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement();
//determine amounts for each line
switch (each.getMovie().getPriceCode())
{
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}


Funkcja oblicza koszt dla każdego zamówienia i korzysta z danych klasy Rental, a nie Client. Pierwsza informacja sugeruje wyodrębnienie obliczania kosztu zamówienia (ExtractMethod - tworzymy metodę getCharge). Wyodrębnioną metodę należy przenieść do odpowiedniej klasy, czyli do Rental. Po tych przekształceniach kod wygląda następująco:


public String statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements())
{
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement();
thisAmount = each.getCharge();
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}


W metodzie getCharge() wciąż mamy do czynienia ze źle wyglądającym kodem. Użyta została konstrukcja "switch - case" względem typu filmu. Taką konstrukcję można zastąpić polimorfizmem, co sprowadzi metodę w klasie Rental do następującej, prostej postaci:


double getCharge()
{
return movie.getCharge(daysRented);
}


Zmienna "movie" reprezentować będzie ogólny typ, za który podstawiane będą konkretne implementacje, takie jak RegularMovie, ChildrenMovie, NewReleaseMovie.

Dalsze przekształcenia są oczywiście możliwe, ale celem powyższego przykładu było przedstawienie kilu kroków refaktoryzacji i sposobu analizowania kodu, pod kątem wyszukiwania obszarów wymagających poprawy.

4. Narzędzia

Przedstawienie kompletnego opracowania na temat aplikacji wspierających programistę byłoby zadaniem bardzo pracochłonnym i jak sądzę pozbawionym sensu. Celem dokumentu nie jest stworzenie rankingu takich programów, ale wykazanie, że przestrzeganie zasad czystości kodu ma bardzo duże znaczenia dla skutecznej pracy w zespołowym projekcie. Przedstawię przegląd rozwiązań, które mogą ułatwić realizację tego celu. Opisy mają charakter subiektywnych wrażeń i spostrzeżeń z użytkowania tych narzędzi. Zachęcam każdego, do podjęcia samodzielnej decyzji o wyborze konkretnego produktu oraz do dzielenia się wrażeniami i spostrzeżeniami z jego stosowania.

a. ReSharper

- Refaktoryzacja z poziomu struktury projektu



Na powyższym przykładzie, przenoszę interfejs do nowego folderu. Wykonując taką operację za pomocą przekształcenia "Move" automatycznie uaktualniam nazwy namcespace, odnośniki w innych klasach i unikam dzięki temu konieczności dokonywania zmian w kodzie.

Inne możliwe przekształcenia, widoczne są w menu kontekstowym na screenie.

- Refaktoryzacja kodu



Powyższy screen przedstawia zbiór przekształceń dostępnych w menu ReSharpera. Aplikacja automatycznie wybiera te operacje, które mają sens, dla danego fragmentu kodu.

- Testy jednostkowe

Ta opcja ReSharpera nie jest bezpośrednio związana z refaktoryzacją, ale należy pamiętać o konieczności używania testów do weryfikowania kodu po refaktoryzacji. Mamy możliwość tworzenia dowolnych podzbiorów testów i grupowania ich w sesje.



Bibliografia

1. http://msdn.microsoft.com/en-us/library/ms229002.aspx
2. Scott Bellware, C# Code Style Guide. [Online] http://www.sourceformat.com/pdf/cs- coding-standard-bellware.pdf
3. dr inż. Bartosz Walter, Zaawansowane projektowanie obiektowe, wykłady [Online]
http://wazniak.mimuw.edu.pl/images/d/d8/Zpo-8-wyk.pdf
http://wazniak.mimuw.edu.pl/images/3/33/Zpo-9-wyk.pdf
http://wazniak.mimuw.edu.pl/images/e/ee/Zpo-10-wyk.pdf
http://wazniak.mimuw.edu.pl/images/b/b8/Zpo-11-wyk.pdf
4. Fowler Martin, Refactoring: Improving the Design of Existing Code

5. http://premium-hands.blogspot.com/2011/12/techniki-pracy-z-kodem-zarzadzanie.html

6. http://www.ccs.neu.edu/research/demeter/papers/law-of-demeter/oopsla88-law-of- demeter.pdf

1 komentarze:

Anonimowy pisze...

Świetny artykuł, dziękuję za usystematyzowanie wiedzy na ten temat. Dziękuję również za podanie ciekawej literatury do której niewątpliwie zajrzę :)

Prześlij komentarz

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management