wtorek, 22 kwietnia 2008

[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

9 komentarzy:

Anonimowy pisze...

Generalnie nic nie zrozumiałem z tego co napisałeś w poście.

Używanie UInt16 zaoszczędza pamięć ale zmiesza wydajność. W porównaniu z UInt32 To można potraktować jako ciekawostkę

Laziers pisze...

w sumie jesli chcemy sie przyczepic do wydajnosci az tak to nie UInt32 a Int32 jest najbardziej wydajne

Anonimowy pisze...

A gdzie jest deklarowane WynagrodzenieFactoryMethod i metoda MakePracownik?

Laziers pisze...

@Anonimowy - mozna zobaczyc w kodzie zrodlowym: http://laziers.googlepages.com/FactoryMethodPrzykladWynagrodzenie.cs
class WynagrodzenieFactoryMethod
{
public static IPracownik MakePracownik(TypZawodu zawod)
{
switch (zawod)
{
case TypZawodu.PIEKARZ:
return new Piekarz();
case TypZawodu.NAUCZYCIEL:
return new Nauczyciel();
case TypZawodu.KIEROWNIK:
return new Kierownik();
default:
throw new Exception("Niewlasciwy typ pracownika");
}
}
}

no1 pisze...

Nonses. Algorytm wyliczający wynogrodzenie masz nadal powielony w każdej klasie. Algorytm powinien być zdefiniowany w jednym miejscu (np. w klasie nadrzędnej) a każdy Pracownik implementował by tylko podajStawkę() - tu przy okazji wzorzec metoda szablonowa się kłania, ale przede wszystkim podstawy programowania obiektowego.

Laziers pisze...
Ten komentarz został usunięty przez autora.
Laziers pisze...

@no1 jest to tylko przyklad, ktory ma WYLACZNIE obrazowac uzycie tego wzorca a nie wybor najlepszego wzorca dla danego problemu. po co zaraz takie ostre slowa?

Vaxquis pisze...

no1 ma racje. przykład jest debilny, bo a) jest sztuczny i syntetyczny, b) nie pokazuje żadnego usprawnienia kodu wynikającego z zastosowania factory method pattern, c) jest zakodowany nieelegancko i mało wydajnie, d) odstrasza od Twojego bloga Laziers, i to dosyć skutecznie, pokazując albo brak wiedzy, albo umiejętności, albo zaangażowania

Marcin pisze...

Bardzo dobry wpis, choć... przykład mógłby być nieco lepszy ;)