czwartek, 1 maja 2008

[brzydkie zapachy kodu] Jeden z Twoich najwiekszych wrogów: DRY! cz. 1

DRY (DON'T REPEAT YOURSELF), czyli: NIE POWTARZAJ SIĘ

Chodzi tu po prostu o powtórzenia kodu. KAŻDE powtórzenie prowadzi do tego, iż cały system coraz trudniej się konserwuje. Wyobraźmy sobie sytuację: mamy metodę, która wylicza składkę ubezpieczenia. Do tego zaraz tworzymy identyczną metodę (w tym samym miejscu lub gdzieś w innej klasie). Musimy pamiętać o tym, że posiadamy dwie takie same metody i jeśli chcemy coś zmienić to musimy to zrobić tu i tu. Przeważnie wystarczy usunąć 1 metodę i korzystać z jednej. To oczywiście najprostszy przypadek. W części drugiej dotyczącej DRY zajmiemy się usuwaniem powielonych części kodu przez stosowanie: metody szablonowej, zastąpieniem algorytmu, przemieszczenie pola w górę etc.

Krótka historyjka wyjaśni nam jak mogą powstawać DRY.

1. Złe zarządzanie projektem
Agnieszka: Kasia, przeglądałam wczoraj Twój kod i natchnęłam się na niemal identyczną klasę, którą w zeszłym miesiącu zrobił Tomek.
Kasia: Co? Ech, gdybym wiedziała, to użyłabym jego klasy a nie traciła czasu na coś co istnieje.

Mogło dojść do tej sytuacji, ze względu:
-na zły podział zadań w projekcie. Widocznie Kasia dostała do zrobienia coś podobnego co już robił Tomek
-nieznajomości struktury aplikacji Kasi (gdyby np. kod był tworzony w parach, to raczej by nie doszło do takiej sytuacji)
-pewność Kasi, że taka klasa nie istniała w systemie i przez to nie zapytała się członków zespołu czy już coś takiego zostało stworzone
-brak (dobrze) rozrysowanej struktury aplikacji, z której można byłoby wyczytać z jakich klas oraz relacji składa się system

2. Głupota (czy jak kto woli brak doświadczenia ;))
Kasia: Tomek, potrzebuje klasy, która służy do obliczania wynagrodzenia na koniec roku, muszę trochę zmienić jej funkcjonalność pod siebie.
Tomek: Okey, tylko nic nie zepsuj. Znajduje się ona w Wyliczenie.Wynagrodzen.
Kasia: Dzięki.
[Kasia bierze całą klasę i kopiuje ją do swojego modułu nad którym pracuje.]

Mogło dojść do tej sytuacji, ze względu:
-brak doświadczenia/zdrowego rozsądku ;)
-obawa przed wprowadzeniem błędu do klasy, co spowodowałoby katastroficzne skutki w całym systemie

To są tylko oczywiście takie dwa przypadki, które powinny Was uczulić na to co robią członkowie zespołu, w którym pracujemy. Sprawdzajmy i pilnujmy się nawzajem ;)

[brzydkie zapachy kodu] Długa metoda

Jak nie trudno się domyśleć zapach - długa metoda, polega na tym, iż metoda jest bardzo ... długa ;) Ale co to oznacza w praktyce? Otóż to, iż np. w takiej metodzie znajdziemy pętle, różnego rodzaju obliczenia, wyświetlenia treści etc. Przykład takiej metody:


class Przyklad
{
public void DlugaMetoda()
{
Console.WriteLine("Naglowek");
Console.WriteLine("---------------------------");

int wynik = 3 * 5;

int a = 0;

for (int i = 0; i < wynik; i++)
{
a += wynik;
}

if (wynik < a)
{
wynik = 123 + 123;
}
else
{
a = wynik;
}


Console.WriteLine("Wyniki:");
Console.WriteLine("a: " + a);
Console.WriteLine("wynik: " + wynik);

Console.WriteLine("---------------------------");
Console.WriteLine("Stopka");
}
}

Rozwiązanie tego problemu jest banalne. Wystarczy stosować "wydzielenie metody". Czyli tą długa metodę dzielimy na mniejsze:


class Przyklad
{
public void DlugaMetoda()
{
DrawNaglowek();

int wynik = ObliczWynik(3, 5);

int a = 0;

a = ZrobCos(wynik, a);

ZrobCosInnego(ref wynik, ref a);

DrawWyniki(wynik, a);

DrawStopka();
}

private static void ZrobCosInnego(ref int wynik, ref int a)
{
if (wynik < a)
{
wynik = 123 + 123;
}
else
{
a = wynik;
}
}

private static int ZrobCos(int wynik, int a)
{
for (int i = 0; i < wynik; i++)
{
a += wynik;
}
return a;
}

private static void DrawStopka()
{
Console.WriteLine("---------------------------");
Console.WriteLine("Stopka");
}

private static void DrawWyniki(int wynik, int a)
{
Console.WriteLine("Wyniki:");
Console.WriteLine("a: " + a);
Console.WriteLine("wynik: " + wynik);
}

private static void DrawNaglowek()
{
Console.WriteLine("Naglowek");
Console.WriteLine("---------------------------");
}

private static int ObliczWynik(int a, int b)
{
return a * b;
}
}

Gdzie mamy zysk?
-wprowadzanie zmian w takiej metodzie jest łatwiejsze
-ciało metody jest CZYTELNIEJSZE
-jesteśmy w stanie objąć całą metodę wzrokiem
-jeśli dobrze nazwiemy te inne metody powstałe przy "wydzieleniu", to nie będziemy musieli zaglądać do ich kodu, gdyż ich nazwa określi nam co robią

[refaktoryzacja] Tworzenie obiektów dla prostych typów danych

Często programiści boją się tworzyć (przynajmniej ja takich spotkałem) małe klasy do obsługi np. kodu pocztowego etc. Niepotrzebnie. Pokażemy to na przykładzie numeru telefonu. Jeśli logika jego obsługi będzie rozmieszczona w różnych klasach, będziemy mieć nie lada problem z zarządzaniem/obsługą/modyfikacją/pielęgnowaniem. Zobrazujmy ten problem:


class Osoba
{
public string NumerTelefonu
{
get
{
// zwraca numer telefonu
}
}
}

class Centrala
{
public string NumerTelefonuBezNumeruKierunkowego
{
get
{
// zwraca numer telefonu bez numeru kierunkowego
}
}
}

class Miejscowosc
{
private Osoba osoba;

public string NumerKierunkowy
{
get
{
// wyciagamy numer kierunkowy z numeru telefonu osoba.NumerTelefonu
}
}
}


Piękny syf w kodzie ;) Ciekawe gdzie teraz byśmy dodali metodę sprawdzającą poprawność numeru telefonu? Żeby uniknąć takich rzeczy wystarczy stworzyć drobną klasę zajmującą się tylko i wyłącznie obsługą tego numeru telefonu. Czyli wszystkie metody dotyczące numeru telefony przemieszczamy do klasy NumerTelefonu:


class NumerTelefonu
{
public NumerTelefonu(string numer)
{
this.numer = numer;
}

private string numer;
public string Numer
{
get
{
// zwraca numer telefonu
}
}

public string NumerTelefonuBezNumeruKierunkowego
{
get
{
// zwraca numer telefonu bez numeru kierunkowego
}
}

public string NumerKierunkowy
{
get
{
// wyciagamy numer kierunkowy z numeru telefonu osoba.NumerTelefonu
}
}

public bool CzyPoprawny
{
get
{
// sprawdzenie czy numer jest poprawny
}
}
}
Teraz mamy wszystko w jednym miejscu. Nie musimy się zastanawiać gdzie umieścić metodę walidującą numer telefonu, oczywiście w ... NumerTelefonu, ale odkrycie ;)