wtorek, 29 kwietnia 2008

Zasada Pojedynczej Odpowiedzialności / Single-Responsibility Principle

"Żadna klasa nie może być modyfikowana z więcej niż jednego powodu".

Tylko, że co to dokładnie oznacza?
Jeśli klasa odpowiada za więcej niż jeden obszar jednocześnie, wtedy może istnieć więcej powodów do zmian niż jeden. Też można się w tym trochę pogubić ;) zatem przejdźmy do przykładu, który nakreśli problem.

Mamy sobie klasę, która odpowiada za dodanie dwóch cyfr do siebie oraz wypisaniu tego na ekran.



class Matematyka
{
private int wynik;

public void Dodaj(int a, int b)
{
wynik = a + b;
}

public void Wypisz()
{
Console.WriteLine(wynik);
}
}


Tutaj jest zachwiana zasada SRP. Dlaczego? Ponieważ ta klasa odpowiada za dwie rzeczy:
1. dodawanie
2. wypisywanie/rysowanie na ekran

w myśl SRP powinna ciążyć na niej 1 odpowiedzialność, czyli albo dodawanie albo rysowanie. Poprawne rozwiązanie problemu:



interface IWypisz
{
void Wypisz(int wynik);
}

class Matematyka
{
private int wynik;
public int Wynik
{
get
{
return wynik;
}
}

public void Dodaj(int a, int b)
{
wynik = a + b;
}
}

class MatematykaUI : IWypisz
{
public void Wypisz(int wynik)
{
Console.WriteLine(wynik);
}
}

class Program
{
static void Main(string[] args)
{
Matematyka m = new Matematyka();
m.Dodaj(2,3);

IWypisz w = new MatematykaUI();
w.Wypisz(m.Wynik);
}
}


Teraz jak chcemy zmodyfikować nasz bardzo skomplikowany ;) algorytm dodawania, to mamy pewność, że nic nie schrzanimy w wyświetlaniu wyniku i odwrotnie. Oczywiście, trzeba się nauczyć stosować SRP z wyczuciem. Przyjdzie to z praktyką, lecz nie ukrywam, że jest to trudne, by na prawdę dobrze stosować SRP'a. W "przykładowej aplikacji" jaką tworzymy na pewno pojawią się dylematy z rozwiązaniem tego problemu, wtedy przypomnę o Zasadzie Pojedynczej Odpowiedzialności.

sobota, 26 kwietnia 2008

[zasada] DIP - Dependency Inversion Principle / Zasada Odwracania Zależności

1. Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji.

2. Abstrakcje nie powinny zależeć od szczegółowych rozwiązań. To szczegółowe rozwiązania powinny zależeć od abstrakcji.


O co w tym chodzi?
Posłużmy się przykładem. Każde użycie operatora new jest naruszeniem zasady DIP, lecz nie należy popadać w panikę. Wszystko trzeba robić z głową. Jeśli nasza klasa rzadko ulega zmianie, bądź przewidujemy, że nie będzie często się zmieniać, to nie powinniśmy mieć problemów wynikających ze złamania zasady DIP.
Stosowanie np. wzorca projektowego factory method , strategii, metody szablonowej powoduje odwrócenie zależności.
Skupmy się teraz na przykładzie:



class B
{
public void ZrobCos() { }
}

class A
{
public void Metoda()
{
B b = new B();

b.ZrobCos();
}
}

Jakakolwiek zmiana w klasie B może mieć wpływ na zachowanie klasy A. Poza tym, klasa A może tylko sterować zachowaniem B. Jak to zmienić? Oczywiście ABSTRAKCJĄ. Stosujemy w tym przypadku interface dla klasy B, popatrzmy:


interface IB
{
void ZrobCos() { }
}

class B : IB
{
public void ZrobCos() { }
}

class A
{
public void Metoda()
{
IB b = new B();

b.ZrobCos();
}
}


Co osiągnęliśmy?
-odwróciliśmy zależności (wyeliminowaliśmy zależność między abstrakcją a szczegółami implementacji)
-klasa A może sterować dowolnymi klasami implementującymi interface IB

Również zasada DIP przydaje się bardzo podczas tworzenia struktury projektu do wyeliminowania zależności między modułami.



Obrazek przedstawia jak bardzo moduł Presenter zależy od modułu Gateway. Poza tym każda nowa kompilacja Presentera wiąże się z ponowną kompilacją Gateway (powstaje nowe wydanie, a co za tym idzie trzeba puścić testy jednostkowe dla tych 2óch modułów). Stosując abstrakcję odwracamy zależność:



Teraz interface IGateway jest jakby "barierą" odgradzająca szczegółową implementację Gatewaya. Kompilacja Presentera w żadnym stopniu nie wpłynie na Gateway. Należy jeszcze dodać, iż IGateway również jest tutaj osobnym modułem, bądź znajduje się w module Presenter.

czwartek, 24 kwietnia 2008

[tdd] TDD. Przykładowa aplikacja cz. 4

Na razie udało nam się spławić klienta i nie będzie nam zawracał gitary, póki co oczywiście. Możemy coś sobie porobić w wolnym czasie. Zamiast chodzić po stronach i popijać piwko, ulepszymy kod aplikacji dla naszego klienta, ponieważ wiemy, że to się na OPŁACI. Na fantastycznym blogu znaleźliśmy krótkie wprowadzenie do TDD. Z wypiekami na twarzy rzucamy się na zastosowanie w praktyce tej metodyki.

Wiemy, że nasz kod nie jest jednak tak doskonały jak się nam wydawało. Czeka nas w zasadzie całkiem spora refaktoryzacja, która później zaowocuje.

Zajmijmy się najpierw uporządkowaniem naszego pliku, przeniesiemy klasy do nowych plików. Tak więc, jeśli mamy klasę piekarz, to tworzymy plik Piekarz.cs i tam przenosimy całą klasę. Robimy to zarówno dla enum, klas, interfaceów etc.



No niby jest fajnie, lecz nie do końca, jeśli będziemy pracować nad tą aplikacją i będą dochodzić kolejne klasy to zrobi nam się bałagan. Zatem zakładamy katalogi i staramy się w nich umieścić, klasy mające "coś" wspólnego ze sobą. Nasza propozycja wygląda tak:



Okey, na razie tak zostawimy.

Przed przystąpieniem do refaktoryzacji postanawiamy pokryć nasz kod testami, aby klient się nie zdziwił, że poprzednio co działało nagle przestało.

Zaczynamy od napisania testów dla obliczania wynagrodzeń. (Kto nie wie o co chodzi, zapraszam do zapoznania się z tym postem)

Utwórzmy może osobną dll'ke, która będzie zawierała testy. Nazwijmy ją PrzykladowaAplikacja.Tests. Jej struktura będzie identyczna jak struktura PrzykladowaAplikacja, czyli:



Bierzemy się za pisanie testów. Zaczniemy od napisania testów dla WynagrodzenieFactoryMethod. Tutaj będziemy sprawdzać czy metoda zwraca nam odpowiednie typy.



using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using PrzykladowaAplikacja.Global;
using PrzykladowaAplikacja.Pracownicy;

namespace PrzykladowaAplikacja.Tests.Pracownicy
{
[TestFixture]
public class TestWynagrodzenieFactoryMethod
{
[Test]
public void TestMakePracownik()
{
Assert.AreEqual(typeof(Kierownik),
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.KIEROWNIK));
}
}
}


Testujemy... i zaskoczenie, test nie przeszedł.
PrzykladowaAplikacja.Tests.Pracownicy.TestWynagrodzenieFactoryMethod.TestMakePracownik : Expected: PrzykladowaAplikacja.Pracownicy.Kierownik
But was: PrzykladowaAplikacja.Pracownicy.Kierownik


dziwny komunikat, ok to zróbmy taką "sztuczkę":



Assert.AreEqual(typeof(Kierownik).ToString(),
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.KIEROWNIK).ToString());


Testy przechodzą. No to teraz dopisujemy dla każdej klasy odpowiednie sprawdzenie. Dzięki temu klasa WynagrodzenieFactoryMethod jest już otestowana. Hmmm... ale w zasadzie poco w taki sposób napisaliśmy ten test, przecież upubliczniliśmy klasy Piekarz, Nauczyciel, Kierownik, a nie chcemy tego robić. Nie po to tworzymy obiekty przez metodę fabrykującą żeby udostępniać te klasy. Wiec jak możemy zmienić test aby te 3 klasy nie były upublicznione? Otóż bardzo prosto :). Usuńmy public w tych 3 klasach, po czym zmieńmy trochę nasz przypadek testowy:



[Test]
public void TestMakePracownik()
{
Assert.AreEqual("PrzykladowaAplikacja.Pracownicy.Kierownik",
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.KIEROWNIK).ToString());
Assert.AreEqual("PrzykladowaAplikacja.Pracownicy.Nauczyciel",
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.NAUCZYCIEL).ToString());
Assert.AreEqual("PrzykladowaAplikacja.Pracownicy.Piekarz",
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.PIEKARZ).ToString());
}


Teraz testy również przechodzą a na dodatek mamy ukryte klasy :) O to nam chodziło.

Weźmiemy się teraz za testy naszych metod obliczających. Napiszmy test dla najprostrzej metody wyliczającej wynagrodzenie. Niewątpliwie znajduje się ona w klasie Nauczyciel. Ciało testu:



[TestFixture]
public class TestNauczyciel
{
[Test]
public void TestObliczWynagrodzenie()
{

}
}


Dobra, to tak. Zawartość metody ObliczWynagrodzenie


return STAWKA_GODZINOWA * przepracowaneGodziny.Dzienne;


stawka godzinowa wynosi 21. Dajmy na to ze przepracowane godzinny dzienne wynoszą 120, 21 * 120 = 2520. Napiszmy zatem test:



[Test]
public void TestObliczWynagrodzenie()
{
PrzepracowaneGodziny pg = new PrzepracowaneGodziny(120, 0, 0);

Assert.AreEqual(2520,
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.NAUCZYCIEL).ObliczWynagrodzenie(pg));
}


świetnie, test przechodzi. Spróbujmy teraz udowodnić, że 120 * 21 != 2520 (taki przypadek również nam się przyda do testu) użyjemy do tego AreNotEqual.



Assert.AreNotEqual(2520,
WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.NAUCZYCIEL).ObliczWynagrodzenie(pg));


testy znów przechodzą, więc tą klasę mamy już pokrytą testami. Po zmianach w tej metodzie będziemy od razu wiedzieli czy coś namieszaliśmy czy też nie.

Tworząc testy dla Kierownika odkrywamy, że... metoda ObliczWynagrodzenie źle działa! No to klient się wkurzy a nam się dostanie. Gdybyśmy pierwsze napisali test a potem dopisali do niego kod, to z pewnością uniknęlibyśmy tego problemu. Gdzie znajduje się błąd? Zacznijmy od wymagań jakie były: Jeśli kierownicy przepracują więcej niż 160, to ich stawka od tej chwili rośnie 2x.
Napiszmy do tego test.



[Test]
public void TestObliczWynagrodzenie()
{
pg = new PrzepracowaneGodziny(161, 0, 0);
Assert.AreEqual(8100, WynagrodzenieFactoryMethod.MakePracownik(Enums.TypZawodu.KIEROWNIK).ObliczWynagrodzenie(pg));
}

PrzykladowaAplikacja.Tests.Pracownicy.TestKierownik.TestObliczWynagrodzenie : Expected: 8100
But was: 161.0f


rozjazd jest baardzo duży, miejmy nadzieję, że klient się zorientuje i przestanie używać naszego programu. Błąd znajduje się w metodzie ObliczGodzinyNadliczbowe



return iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA;


161 - 160 * 50 = -7839, hehe Kierownik jeszcze musi dopłacić jeśli będzie miał nadgodziny ;) Dlatego test nie przechodzi. Poprawmy metodę ObliczGodzinyNadliczbowe


return (iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY) * STAWKA_GODZINOWA * 2;


Teraz jest wszystko dobrze. Udało nam się wyłapać i wyeliminować błąd podczas pisania dla niego testu. Jeśli będziemy tworzyć test PRZED pisaniem unikniemy takich głupot.

Zadzwońmy do klienta i powiedzmy, żeby nie używał tej wersji programu, może będzie wyrozymiały... okazało się, że nie było prądu u niego w firmie więc nie zdarzyli wdrożyć nowej wersji :))) (nie liczmy w prawdziwym życiu na takie farty ;))

Podobne testy piszemy dla Piekarza. Pozostaje pytanie, co w takim razie z metodami prywatnymi, czy je też poddajemy testom. Oczywiście jak zawsze "ZALEŻY". Wg mnie, jeśli mamy jakieś drobne metody prywatne, które nie są krytyczne (w miarę proste), to nie testujemy ich, bowiem tak czy tak jak coś będzie źle, to test wykaże błąd w publicznej metodzie używającej tej prywatnej. Lecz jeśli chcemy to przetestować a poza tym MUSIMY, to zawsze można zmienić z private na public. Lepiej mieć przetestowany i sprawdzony kod. Jeszcze jedną metodą na przetestowanie metod prywatnych jest zmienienie modyfikatora dostępu na protected, wtedy test dziedziczy nasza klasę i dzięki temu mamy dostęp do metody "prywatnej". Krótki przykładzik:




public class A
{
protected void Dodaj(int a, int b)
{
return a + b;
}
}



[TestFixture]
public class TestA : A
{
[Test]
public void TestDodaj()
{
Assert.AreEqual(3, this.Dodaj(1, 2));
}
}



W następnym poście zajmiemy się refaktoryzacją w kierunku wzorca strategia.

Kod źródłowy

[tdd] TDD - Test-Driven Development / NUnit. Krótkie wprowadzenie

Gdzieś w internecie znaleźliśmy coś co nas zainteresowało. Jest to dziwny skrót TDD. Poszperajmy trochę w internecie, by dowiedzieć się co to tak na prawdę jest. Pierwsza pozycja: tdd, hmm chyba nie o to nam chodziło, druga pozycja też nie, co jest? nie ma polskich stron o tym? Przechodzimy na szukanie zagranicznych materiałów tdd od razu lepiej, aż za dobrze. Nie chce nam się czytać tego, za długie! Przelećmy wzrokiem po tym. Wiemy już co oznacza skrót - Test-Driven Development, hmm bardzo ciekawe, czyli jest to wytwarzanie oprogramowania sterowane testami, bełkot ;) Przejdźmy może do przykładu.
Acha tutaj najważniejsza rzecz: przed przystąpieniem do pisania właściwego kodu, musimy zacząć od napisania testu, czyli jak chcemy sprawdzić czy metoda Dodaj jest dobrze napisana:


class Matematyka
{
public static int Dodaj(int a, int b)
{
return a + b;
}
}


a + b = c, prawda? czyli 2 + 3 = 5.

Ok, teraz napiszmy odejmowanie wykorzystując TDD.

(Ściągamy sobie z internetu NUnita, podpinamy dll'ke: nunit.framework.dll do naszego projektu)

Tworzymy nowy plik/klasę o nazwie: TestMatematyka

Świetnie! teraz możemy przygotować naszą klasę do testów.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

namespace AplikacjaMatematyka
{
[TestFixture] // Informuje nunita, że w tej klasie znajdują się przypadki testowe
class TestMatematyka
{
[Test] // Informujemy nunita, że to jest klasa testowa
public void TestOdejmij()
{
}
}
}

No to teraz sedno: przewidujemy co dana metoda zwróci, czyli wiemy, że Odejmij będzie (uwaga!) odejmować 2 liczby od siebie ;) czyli jak mamy 3 - 2 = 1, no spoko żadne odkrycie.

Teraz musimy zapisać 3 - 2 = 1 w formie testu, ponieważ to wyrażenie będzie sprawdzać czy nasza metoda działa dobrze.

W NUnitcie, porównujemy wartości poprzez użycie metod z klasy Assert. By porównać 2 wartości posłużymy się metodą: Assert.AreEqual(); Napiszmy zatem test sprawdzający poprawność działania metody Odejmij (pamiętajmy, że jeszcze jej nie mamy zaimplementowanej! tym zajmiemy się później, trochę to dziwne na 1st rzut oka)


[Test]
public void TestOdejmij()
{
Assert.AreEqual(1, Matematyka.Odejmij(3, 2));
}

Oznacza to: oczekujemy wartości 1, odejmij 3 od 2

Skompilujmy projekt. BUM! Metoda Odejmij nie istnieje :) zatem, napiszmy ja, wiemy już z góry, ze potrzebujemy 2óch parametrów/zmiennych a i b, na wejściu mamy 3 i 2, musimy tak zrobić by wyszło 1. Nic trudnego prawda?


public static int Odejmij(int a, int b)
{
return a - b;
}

Kompilujemy. Udało się. Otwieramy NUnita, tworzymy nowy projekt, zapisujemy go KONIECZNIE tam gdzie się znajdują nasze dll'ki, bądz exe. Dołączamy exe bądź dll, gdzie znajduje się nasz test do naszego projektu i odpalamy. Wszystko jest ok, testy przeszły. Zróbmy teraz mały błąd w naszej metodzie: return a - b - 1; Kompilacja i testowanie. Test nie przeszedł. Co to oznacza? Oczywiście, to że wprowadziliśmy błąd do wyliczania odejmowania. Błąd był świadomy i wiemy o nim, lecz co jeśli błąd zrobimy NIEŚWIADOMIE? W jakimś miejscu w projekcie? Nie będziemy wiedzieli na dodatek w którym miejscu. Tutaj testy nam przychodzą z pomocą. Jeśli mamy otestowane metody w projekcie wtedy możemy mieć pewność, że wydanie nowej wersji nie będzie zawierało błędów. Brzmi fantastycznie. Lecz przecież wykonanie testu wiąże się z CZASEM. Hehe, ok, ale nie prościej napisać raz test a potem wywoływać go już ile razy chcemy i kiedy chcemy? bez ingerencji "ręcznego" testowania przez użytkownika? "Chyba" testowanie ręczne nam więcej zajmie czasu niż napisanie testu. Zatem mamy zysk czasu a nie stratę.

Ale poco pisaliśmy test na samym początku? Przecież można było zrobić to po napisaniu metody. Otóż właśnie że nie, w tym jest sęk. Jeśli ustalimy co mamy na wejściu a co chcemy mieć na wyjściu, to już będziemy wiedzieć jak sie zabrać do napisania interesującej nas metody. Przypadek z odejmowaniem był banalny, lecz co jeśli będziemy wyliczać jakieś skomplikowane biznesowe wskaźniki czy kto wie co jeszcze. Mamy dzięki temu wszystko sprecyzowane. Do tego tworzymy przy okazji bardzo dobrą dokumentację.

Korzyści z TDD:
-jest duża szansa, że kod będzie poprawny, mimo iż będziemy wprowadzać modyfikacje do kodu
-nie pozwalamy na powstanie CODE LEGACY
-bardzo szybko jesteśmy w stanie zdiagnozować błąd

Koszty... czy w ogóle są?
Ja korzystam dość długo już z TDD i cóż mogę powiedzieć o kosztach. "Jedynie" tyle, że one również muszą być poddawane refaktoryzacji, zmianom wraz gdy zmieniają się wymagania co dana metoda ma robić, trzeba je również konserwować i dbać o nie jak o normalny kod. Tym bardziej, że rozmiar kodu testowego będzie tak długi jak "normalny" kod.

Pytanie: Czy wszystkie metody muszę testować?

i tak i nie, zależy jak leży ;) A tak na serio to ZALEŻY od pewnych czynników, ale to już temat na inny post. Wg statystyk, jakie gdzieś wyczytałem, to jeśli kod jest pokryty testami w 60-80% to jest dobrze, ale nie daje głowy, że dobrze pamiętam. Z praktyki mogę powiedzieć jedno: im więcej tym szczelniej ale myślę, że to żadne odkrycie.

Więcej na ten temat w tutaj.

Kod źródłowy

środa, 23 kwietnia 2008

[refaktoryzacja] Parametryzowanie metod. - Przykładowa aplikacja cz. 3

Ostatnio przyszedł do nas klient i nagle zmienił nam wymagania. Chciał żeby wynagrodzenie piekarza było obliczane na podstawie 2óch parametrów, które by reprezentowały stawki godzinowe nocne ale również i dzienne. Do stawek nocnych doliczany jest bonus w wysokości 5 zł. Pierwsze co nam przyszło do głowy to dodanie nowej zmiennej do metody ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzinDziennych, UInt16 iloscPrzepracowanychGodzinNocnych);w interface IPracownik. Jeślibyśmy to zastosowali, to zmiany w kodzie byłyby baaardzo duże. Trzeba by było zmienić wszystkie klasy implementujące IPracownik, lecz nie tylko, każde dodanie nowej zmiennej kończyłoby się ze zmianami w całej strukturze klas a na dodatek inne klasy np Nauczyciel nie wykorzystują tych zmiennych.

Czyżby nie było dobrego rozwiązania tej metody? Przecież musi istnieć. Może po prostu powinniśmy zrobić po prostu "paczkę" zmiennych. Utworzylibyśmy klasę PrzepracowaneGodziny, która by się składała z 2óch zmiennych, dzienne i nocne. Zróbmy to:


class PrzepracowaneGodziny
{
private UInt16 dzienne;
public UInt16 Dzienne
{
get { return dzienne; }
}

private UInt16 nocne;
public UInt16 Nocne
{
get { return nocne; }
}

public PrzepracowaneGodziny(UInt16 dzienne, UInt16 nocne)
{
this.dzienne = dzienne;
this.nocne = nocne;
}
}

Dobrze skoro mamy już klasę "paczkę", to podepnijmy ją do naszego systemu.

Musimy zmienić IPracownik.ObliczWynagrodzenie, teraz nie będzie przyjmować zmiennej iloscPrzepracowanychGodzin, lecz naszą klasę PrzepracowaneGodziny



interface IPracownik
{
float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny);
}


no to skoro zdecydowaliśmy się na taki krok, to musimy pozmieniać we wszystkich klasach implementujących interface IPracownik.



class Nauczyciel : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 21;

public float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny)
{
return STAWKA_GODZINOWA * przepracowaneGodziny.Dzienne;
}
}


wygląda nieźle, ale zobaczymy dalej czy to się sprawdzi.


class Kierownik : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 50;

private const UInt16 PODSTAWOWY_CZAS_PRACY = 160;

private static bool CzyPodstawowyCzasPracyZostalPrzekroczony(UInt16 iloscPrzepracowanychGodzin)
{
return iloscPrzepracowanychGodzin > PODSTAWOWY_CZAS_PRACY;
}

private static int ObliczGodzinyNadliczbowe(UInt16 iloscPrzepracowanychGodzin)
{
return iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA;
}

private static int ObliczPodstawoweWynagrodzenie()
{
return STAWKA_GODZINOWA * PODSTAWOWY_CZAS_PRACY;
}

public float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny)
{
if (CzyPodstawowyCzasPracyZostalPrzekroczony(przepracowaneGodziny.Dzienne))
{
return ObliczPodstawoweWynagrodzenie() +
ObliczGodzinyNadliczbowe(przepracowaneGodziny.Dzienne);
}

return STAWKA_GODZINOWA * przepracowaneGodziny.Dzienne;
}
}

tutaj też niczego sobie. Okey, to brnijmy w to dalej. Teraz na warsztat bierzemy klasę Piekarz:



public float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny)
{
return ((STAWKA_GODZINOWA + BONUS_ZA_PRACE_W_NOCY) * przepracowaneGodziny.Nocne) +
(STAWKA_GODZINOWA * przepracowaneGodziny.Dzienne);
}


i znów miłe zaskoczenie, gładko, rzekłbym zbyt łatwo ;). Uporządkujmy ciało tej metody, znów tak jakoś skomplikowanie jest. Zastosujmy wydzielenie metod poznane w 2 części



class Piekarz : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 10;

private const UInt16 BONUS_ZA_PRACE_W_NOCY = 5;

private static int ObliczWynagrodzenieNocne(UInt16 przepracowaneGodzinyNocne)
{
return (STAWKA_GODZINOWA + BONUS_ZA_PRACE_W_NOCY) * przepracowaneGodzinyNocne;
}

private static int ObliczWynagrodzenieDzienne(UInt16 przepracowaneGodzinyDzienne)
{
return STAWKA_GODZINOWA * przepracowaneGodzinyDzienne;
}

public float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny)
{
return ObliczWynagrodzenieNocne(przepracowaneGodziny.Nocne) +
ObliczWynagrodzenieDzienne(przepracowaneGodziny.Dzienne);
}
}


wow i w sadzie zakończyliśmy zlecenie.

Wciągnięci pracą nie słyszeliśmy jak dzwonił do nas klient. Nagrał się na automatyczną sekretarkę:
Właśnie jadę do was by obejrzeć gotowy produkt. Mam nadzieję że pamiętaliście o tym, że piekarz w święta za przepracowane godziny otrzymuje podwójną stawkę.


Cholera! Zapomnieliśmy o tym. Ale zaraz! Przecież nasza aplikacja jest wspaniale napisania. Drobna zmiana powinna załatwić sprawę. Pomyślmy, potrzebujemy jeszcze zmienną: swiateczne, w klasie PrzepracowaneGodziny:


class PrzepracowaneGodziny
{
private UInt16 dzienne;
public UInt16 Dzienne
{
get { return dzienne; }
}

private UInt16 nocne;
public UInt16 Nocne
{
get { return nocne; }
}

private UInt16 swiateczne;
public UInt16 Swiateczne
{
get { return swiateczne; }
}

public PrzepracowaneGodziny(UInt16 dzienne, UInt16 nocne, UInt16 swiateczne)
{
this.dzienne = dzienne;
this.nocne = nocne;
this.swiateczne = swiateczne;
}
}

teraz musimy zmienić Piekarz.ObliczWynagrodzenie


class Piekarz : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 10;

private const UInt16 BONUS_ZA_PRACE_W_NOCY = 5;

private const UInt16 MNOZNIK_STAWKI_SWIATECZNEJ = 2;

private static int ObliczWynagrodzenieNocne(UInt16 przepracowaneGodzinyNocne)
{
return (STAWKA_GODZINOWA + BONUS_ZA_PRACE_W_NOCY) * przepracowaneGodzinyNocne;
}

private static int ObliczWynagrodzenieDzienne(UInt16 przepracowaneGodzinyDzienne)
{
return STAWKA_GODZINOWA * przepracowaneGodzinyDzienne;
}

private static int ObliczWynagrodzenieZaSwieta(UInt16 przepracowaneGodzinySwiateczne)
{
return STAWKA_GODZINOWA * MNOZNIK_STAWKI_SWIATECZNEJ * przepracowaneGodzinySwiateczne;
}

public float ObliczWynagrodzenie(PrzepracowaneGodziny przepracowaneGodziny)
{
return ObliczWynagrodzenieNocne(przepracowaneGodziny.Nocne) +
ObliczWynagrodzenieDzienne(przepracowaneGodziny.Dzienne) +
ObliczWynagrodzenieZaSwieta(przepracowaneGodziny.Swiateczne);
}
}


TYLE! W sam raz, bo właśnie przyszedł klient. Ufff udało nam się, tym razem, lecz coś nam chodzi po głowie, że ten kod, który stworzyliśmy wcale nie jest taki dobry... cdn.

Kod źródłowy

wtorek, 22 kwietnia 2008

[refaktoryzacja] Tworzenie małych pomocniczych metod. Przykładowa aplikacja cz. 2

W poprzedniej części naszej aplikacji poradziliśmy sobie z problemem narażenia kodu na awarie związaną z dodaniem nowych pracowników, teraz zajmiemy się jego dalsza rozbudową.

Przychodzi do nas klient:
Potrzebujemy utworzyć małą aplikację, która na podstawie wybranego zawodu wyliczy nam miesięczne wynagrodzenie. Do tego wynagrodzenie zależy od ilości przepracowanych godzin oraz stawki. Jeśli kierownicy przepracują więcej niż 160, to ich stawka od tej chwili rośnie 2x.

Pierwsze co musimy zrobić, to otworzyć sobie kod źródłowy z naszego poprzedniego przykładu.
Weźmy się może za dodanie nowej funkcjonalności dla kierownika, czyli jeśli jego ilość godzin będzie większa od 160, to jego stawka sie podwaja:



public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
if (iloscPrzepracowanychGodzin > 160)
{
return (STAWKA_GODZINOWA * 160) + (iloscPrzepracowanychGodzin - 160 * STAWKA_GODZINOWA);
}

return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}

Dość szybko nam poszło, lecz hola amigo!

Nagle przychodzi klient:
Witam, rozmyśliliśmy się co do podstawowego wymiaru pracy kierownika, nie chcemy by wynosił 160, lecz 170.

Extra, teraz musimy dokonać zmian w 3 miejscach, a na dodatek jeśli zrobi to obca osoba, to może nawet się nie zorientować co oznacza cyfra 160. Nie możemy tak tego zostawić. Musimy poddać kod drobnej refaktoryzacji. Tworzymy stałą PODSTAWOWY_WYMIAR_PRACY i przypisujemy jej wartość 160, następnie wszędzie w klasie 160 zamieniamy na tą stała. Proszę!


class Kierownik : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 50;

private const UInt16 PODSTAWOWY_CZAS_PRACY = 160;

public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
if (iloscPrzepracowanychGodzin > PODSTAWOWY_CZAS_PRACY)
{
return (STAWKA_GODZINOWA * PODSTAWOWY_CZAS_PRACY) + (iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA);
}

return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}
}

Teraz kod jest czytelniejszy i bardziej elastyczny. Jak przyjdzie klient i powie, że jednak podstawowy czas pracy dla kierownika ma wynosić 180, to nikt nie będzie miał problemów z tą zmianą. Lecz metoda ObliczWynagrodzenie wydaje sie bardzo skomplikowana/rozciągnięta, musimy ją jakoś uprościć.

warunek:
if (iloscPrzepracowanychGodzin > PODSTAWOWY_CZAS_PRACY)

zamienimy na statyczna metodę:


private static bool CzyPodstawowyCzasPracyZostalPrzekroczony(UInt16 iloscPrzepracowanychGodzin)
{
return iloscPrzepracowanychGodzin > PODSTAWOWY_CZAS_PRACY;
}

dzięki temu po przeczytaniu warunku od razu się zorientujemy co ulega sprawdzeniu.

ta linijka jest jeszcze trochę zagmatwana:


return (STAWKA_GODZINOWA * PODSTAWOWY_CZAS_PRACY) + (iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA);


stwórzmy z niej 2 metody:

wyrażenie:
(STAWKA_GODZINOWA * PODSTAWOWY_CZAS_PRACY)

zamienimy na:


private static int ObliczPodstawoweWynagrodzenie()
{
return STAWKA_GODZINOWA * PODSTAWOWY_CZAS_PRACY;
}

natomiast:
(iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA)

na:


private static int ObliczGodzinyNadliczbowe(UInt16 iloscPrzepracowanychGodzin)
{
return iloscPrzepracowanychGodzin - PODSTAWOWY_CZAS_PRACY * STAWKA_GODZINOWA;
}


Proszę, z linijki niemal tak długiej jak "spaghetti" udało nam się utworzyć całkiem zgrabny kod:



public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
if (CzyPodstawowyCzasPracyZostalPrzekroczony(iloscPrzepracowanychGodzin))
{
return ObliczPodstawoweWynagrodzenie() +
ObliczGodzinyNadliczbowe(iloscPrzepracowanychGodzin);
}

return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}

Jak widać refaktoryzacja działa cuda.

Uff uporaliśmy sie z wymogiem naszego klienta, jesteśmy bardzo z siebie zadowoleni i już wiemy, że nic nam dobrego humoru nie popsuje.
Nagle dzwoni klient:

Witam! Zapomniałem powiedzieć, ze piekarze muszą mieć inaczej liczoną stawkę w nocy.

Hmm co by tu wykombinować? Żeby to zrobić to dla piekarzy gdzieś trzeba byłoby utworzyć zmienne, które by reprezentowały ilość przepracowanych godzin w dzień oraz w nocy. Jeśli tak zrobimy, musielibyśmy naruszyć nasza metodę ObliczWynagrodzenie przyjmującą wyłączenie 1 zmienną a nie 2! Jeśli to zrobimy będziemy musieli dokonać zmian we wszystkich klasach implementujących IPracownik. Nie możemy na to sobie pozwolić!

Telefon od klienta:
Witam, jeszcze o jednej zmianie zapomniałem wam powiedzieć, jeśli piekarz wyrobi w święta jakieś godziny to musimy mu je pomnożyć przez 2.

istny horror... cdn.

a tym czasem tutaj znajdują się źródła

[refaktoryzacja] Zasada. Dbajmy o nazwy klas i zmiennych

Niezwykle ważną sprawą przy wytwarzaniu DOBREJ jakości kodu są poprawnie dobrane nazwy: metod, klas, zmiennych etc. Może się oczywiście tak zdarzyć, iż w pewnym momencie podczas pracy nad projektem, dojdziemy do wniosku, że taka a taka zmienna powinna sie tak i tak nazywać. Więc przystępujemy do refaktoryzacji. Nawet stosowanie tak prostej zasady przyczynia się do tworzenia na prawdę dobrego i przejrzystego kodu. Może się to na pierwszy rzut oka wydawać śmieszne, lecz czy nie zdarzyło się Wam popatrzeć do kodu i rozmyślać o tym co dana zmienna robi? A gdyby dobrze była nazwana, to już po tym byśmy wiedzieli do czego służy.

Korzyści:
-czytelny kod
-zaoszczędzony CZAS

[wzorce projektowe] Factory Method (Metoda Wytwórcza/Fabrykująca). Przykładowa aplikacja cz. 1

Dziś zajmiemy się zrozumieniem wzorca projektowego factory method [metody fabrykującej/wytwórczej].

Do czego może się nam przydać:
-do zastąpienia warunkowych wyrażeń
-do uzyskania kodu otwartego na rozbudowę a zamkniętego na modyfikację (o zasadzie OCP będzie przeznaczony osobny post, ale już tutaj postaram się pokazać to na przykładzie)
-zgodne zasadą DIP (ale o tym też później)

Gdzie są koszty?:
-stosowanie wzorca factory method zawsze niesie za sobą większą złożoność kodu
-tworzenie za każdym razem obiektu co może wpłynąć nieznacznie na wydajność, lecz tym bym się (póki co) nie przejmował.

Przychodzi klient:
Potrzebujemy utworzyć małą aplikację, która na podstawie wybranego zawodu wyliczy nam miesięczne wynagrodzenie. Do tego wynagrodzenie zależy od ilości przepracowanych godzin oraz stawki.

Wydaję mi się, że część osób stworzyłaby podobną klase jak tutaj ja zrobiłem:



class Wynagrodzenie
{
private const UInt16 STAWKA_GODZINOWA_PIEKARZA = 10;
private const UInt16 STAWKA_GODZINOWA_NAUCZYCIELA = 21;
private const UInt16 STAWKA_GODZINOWA_KIEROWNIKA = 50;

public static float ObliczWynagrodzenia(TypZawodu zawod, UInt16 iloscPrzepracowanychGodzin)
{
switch (zawod)
{
case TypZawodu.PIEKARZ:
return STAWKA_GODZINOWA_PIEKARZA * iloscPrzepracowanychGodzin;
case TypZawodu.NAUCZYCIEL:
return STAWKA_GODZINOWA_NAUCZYCIELA * iloscPrzepracowanychGodzin;
case TypZawodu.KIEROWNIK:
return STAWKA_GODZINOWA_KIEROWNIKA * iloscPrzepracowanychGodzin;
default:
return 0;
}
}
}


użycie:



UInt16 wyplata = Wynagrodzenie.ObliczWynagrodzenia(TypZawodu.NAUCZYCIEL, 120);

Niby wszystko jest ok, klasa będzie dobrze działać, lecz co z tego jeśli dodanie nowego stanowiska wiąże się z poważną zmianą klasy Wynagrodzenie. Nieumyślnie możemy zrobić jakiś błąd podczas modyfikacji i wszystko będzie się źle liczyło. Oczywiście tutaj, to nie jest AŻ tak dobrze widoczne, lecz wyobraźcie sobie rozbudowaną klasę napisaną w taki sposób, która oblicza jakieś poważne wartości, 1 pomyłka i leżymy.
I tutaj przychodzi nam z pomocą wzorzec Factory Method (oczywiście nie jest to jedyny sposób na dobre rozwiązanie problemu).

zaczynamy od utworzenia interface'u:


interface IPracownik
{
float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin);
}

teraz każdy pracownik musi implementować metodę ObliczWynagrodzenie


class Piekarz : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 10;

public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}
}

class Nauczyciel : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 21;

public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}
}

class Kierownik : IPracownik
{
private const UInt16 STAWKA_GODZINOWA = 50;

public float ObliczWynagrodzenie(UInt16 iloscPrzepracowanychGodzin)
{
return STAWKA_GODZINOWA * iloscPrzepracowanychGodzin;
}
}


użycie:


IPracownik pracownik = WynagrodzenieFactoryMethod.MakePracownik(TypZawodu.NAUCZYCIEL);

pracownik.ObliczWynagrodzenie(120);


Okey, i teraz korzyści ze stosowania factory, jeśli będziemy chcieli dodać kolejny zawód, to musimy tylko utworzyć nowa klasę, która implementuje interface IPracownik oraz dodać do metody MakePracownik kolejnego case'a. Tyle. Dzięki temu nie narażamy inne klasy na modyfikację, a w szczególności algorytm wyliczający wynagrodzenie (który akurat tutaj jest jednakowy i niezwykle prosty). W kolejnych postach postaram się przedstawić zastosowanie innych wzorców na tym przykładzie, lecz będą dochodziły coraz to nowe wymagania "klienta" czyli mnie ;)


I taka porada dnia: nie nadużywajmy tego wzorca, pomyślmy czy pierwsze nad problemem, czy nie da się go rozwiązać w inny sposób, trzeba pamiętać, że wzorce nie należy upychać gdzie się da. Wg mnie powinniśmy dążyć do wzorca projetowego a nie odrazu do jego implementacji. Niech to sie dzieje ewolucyjnie a nie rewolucyjnie.


Zapraszam do ściągnięcia źródeł przykładu

poniedziałek, 21 kwietnia 2008

[c#] nowa linia w string - Environment.NewLine

Może temat i błahy, lecz nie każdy wie jak dobrze robić nową linie.

Całą sprawę załatwia nam: Environment.NewLine

zamiast pisać:

string s = "1linia\n2linia";

string s = @"1linia
2linia";


można po prostu użyć:

string s = "1linia" + Environment.NewLine + "2linia";


jaki z tego zysk? Przede wszystkim - Przenośność, jeśli używamy Environment.NewLine, to spod widnowsa/linuxa/PDA będziemy mieć ten sam efekt. Nie musimy pamietać czy na linuxie/windzie jest \n\r czy \r\n czy \n?

[ASP.NET] Literal Multiline, ale bez tagow HTML'owych

Dajac uzytkownikom dostep np. do pisania postow na stronie, nieraz nie chcemy zeby byly interpretowane przez przegladarke tagi html'owe [z np. wzgledow bezpieczenstwa]. Bierzemy sobie kontrolke literal, badz label i przypisujemy do wlasciwosci Text naszego string'a:

Literal1.Text = "ala ma\nkota";


Wgrywamy strone, patrzymy a tu literal wyglada tak:

ala ma kota


Co jest? Nie ma "entera"? Wlasnie i tu jest problem, ktory zostaje rozwiazany przez ten jakze magiczny ;) kod:

Literal1.Text = Server.HtmlEncode("ala ma\nkota).Replace("\n", "<br />");

Teraz nasz literal wyglada tak jak powinien, czyli:
ala ma
kota

[c#] Operator string'a + (konkatenacja) a StringBuilder

Dobrym sposobem na przyspieszenie budowania string'ow jest uzycie do tego StringBuilder'a. Jest on o wiele szybszy od operatora +. Roznice czasowa najlepiej widac w jakiejs petli, wiec jesli budujemy jakiegos bardzo duzego string'a oraz jego dlugosc moze byc dynamiczna, to polecam uzywanie StringBuilder'a.
Dlaczego tak sie dzieje? Poniewaz StringBuilder w czasie tworzenia przydziela w pamieci obszar roborczy, do ktorego jest wpisany nasz string, po czym StringBuilder dokonuje bezposrednio operacji wlasnie na tym obszarze.

Zamiast:
string s = "ala" + " ma " + "kota";
Console.WriteLine(s);

Mozemy:

StringBuilder sb = new StringBuilder();
sb.Append("ala");
sb.Append(" ma ");
sb.Append("kota");
Console.WriteLine(sb); // sb.ToString(), by wypisac cala jego zawartosc




Wg mnie jedna z najlepszych ksiazek do c# i .net'a

[c#] Sprawdzanie czy string jest pusty

Niby taka blahostka, lecz moze sie przydac. Ja zawsze stawiam na wydajnosc nawet w takich za przeproszeniem "pierdolkach" ;) Zamiast przeprowadzac sprawdzenia takie jak:
string text = "test";

if ( text == null ) ;
if ( text == "" ) ;
if ( text.Equals("") ) ;
if ( text.CompareTo("") == 0 ) ;
etc.

mozna uzyc:
if ( string.IsNullOrEmpty(text) ) ;

co jest szybsze od wyzej wymienionych sprawdzen.

[c#] String.Compare vs String.CompareOrdinal

Jesli zalezy nam na szybkosci dzialania aplikacji, to raczej powinnismy korzystac z metody String.CompareOrdinal zamiast String.Compare. Natomiast trzeba pamietac, ze String.CompareOrdinal jest czuly na roznice "kultury" oraz wielkosc liter.

przyklad uzycia:

if ( string.Compare("a", "a") ) ; //true

if ( string.CompareOrdinal("a", "a") == 0 ) ; //true

[VS >= 2005] Jak zmusic debuggera do ominiecia wybranej linii

Podczas pracy z debuggerem, chcemy mu dac do zrozumienia, zeby nie wlazil nam niepotrzebnie do jakichs czesci kodu, problem rozwiazuje nam wlasciwosc: [System.Diagnostics.DebuggerStepThrough]

public string Test
{
private int test;

[System.Diagnostics.DebuggerStepThrough]
get { return test; }

[System.Diagnostics.DebuggerStepThrough]
set { test = value; }
}

[c#] jak łączyć ścieżki plików w .netcie

Nie wiem jak Wy, ale ja zawsze mialem klopoty ze ścieżkami plików nie jeden raz mi uciekł jakiś slash "\" ten problem nam doskonale rozwiazuje .NET

Zamiast:


public string GetFullPath(string fileName)
{
string folder = "katalog1";

return folder + fileName;
}


Mozna:


public string GetFullPath(string filename)
{
string folder = "katalog1";

return Path.Combine(folder, filename); //tutaj
}

I juz nie musimy sie martwic o "\".