Zwinne działania zaczepne
December 6th, 2006 Marcin Niebudek
Ponarzekałem sobie już sporo na skostniałe metody podejścia do projektów. Może nawet znajdziesz w tych narzekaniach trochę merytorycznych argumentów popierających przedstawiane tutaj tezy. Ale w końcu trzeba przejść do konkretów. Zaczynamy nowy projekt. Co zrobić aby wprowadzić do niego jak najwięcej praktyk agile. Po pierwsze nie należy starać się wprowadzać zbyt dużej liczby nowych elementów na raz. Wiele razy można było się spotkać z dyskusjami na temat tego, “jak bardzo agile się już jest”. Moim zdaniem dobry proces czerpiący ze zwinnych metodyk powinien przejawiać się w następujących aspektach:
- nastawienie na zmiany, jako naturalną kolej rzeczy (to jedna z cech przewodnich praktyk agile)
- iteracyjność przejawiająca się również (a może przede wszystkim) w samym wprowadzaniu agile do projektu - stosujmy te same praktyki na poziomie doskonalenia procesu, jak i na poziomie implementacji i doskonalenia samego systemu
- minimalizm w podejmowanych działaniach i maksymalizm w osiąganych efektach - najczęściej mylnie kojarzony tylko ze skracaniem czasu i obniżaniem kosztów pracy, podczas gdy chodzi także o ciągłe podwyższanie jakości i kompletności produktu oraz o doskonalenie “warsztatu”.
Wykonujemy pracę coraz szybciej i taniej nie dlatego, że robimy coraz słabsze systemy, tylko dlatego, że jesteśmy coraz lepsi, tak jak nasz lekki proces. Tak więc małymi krokami do celu. Proces wdrażania agile można zacząć od dołu, czyli od nas programistów. Nie koniecznie trzeba też od razu uświadamiać wszystkich managerów. Próbujemy wykonać mały krok. Jak się uda pochwalimy się sukcesem. Managerów od razu uspokajam… nie będę proponował samowoli, rewolucji, czy innych działań wywrotowych. Raczej nazwijmy je na razie drobnymi działaniami zaczepnymi. Pewne rzeczy można zrobić inaczej i to się opłaca. A zaczniemy od początku - czyli od szacowania i planowania.
Przed uruchomieniem projektu czas na krótki rekonesans - dochodzi do oszacowania zakresu i pracochłonności projektu (tak… trzeba klientowi podać cenę, a nie chcemy sobie strzelać w kolano). To pierwszy moment, kiedy możemy spróbować czegoś agile. Na temat zbierania i analizy wymagań napisano i powiedziano dotąd bardzo wiele. Ale mamy włożyć w to minimum środków, aby osiągnąć jak najlepsze wyniki. Potrzebujemy określić jaki system mamy zbudować.
Zebranie wymagań i zakres projektu
Zadaniem numer jeden będzie zebranie zespołu, który będzie wykonywał projekt i doprowadzenie do spotkania z klientem lub osobami, które będą określały wymagania. Każdy rzeczywisty użytkownik systemu będzie tutaj na wagę złota, ale pracujemy z tym kogo mamy. Podczas tego spotkania chcemy doprowadzić do pewnego rodzaju burzy mózgów wszystkich zebranych.
Pierwszym etapem spotkania będzie określenie, kto będzie użytkownikiem systemu. Chodzi o ustalenie ról użytkowników jacy mogą używać systemu (przerywamy wymyślanie, jeśli zauważymy, że z trudem przychodzą nam do głowy kolejne typy). Efektem może być krótka lista. Na koniec poświęćmy chwilę na ew. wyeliminowanie ról powtarzających się pod nieco inną nazwą, lub na pewną generalizację ról o podobnych zadaniach. Taka lista ułatwi nam realizację kolejnej części spotkania.
Podstawowa zasada - mamy zawsze dostarczać użytkownikom części systemu, które będą mogli użyć. Zaczynamy zastanawiać się jakie funkcjonalności ma ten system posiadać. Głównie opowiadać powinien klient, my dopytujemy o ew. szczegóły lub prosimy o wyjaśnienie niejasnych kwestii (często rzeczy oczywiste dla klienta nie są oczywiste dla programistów). Nie musimy być specjalistami w dziedzinie klienta, mamy się znać na tworzeniu systemów informatycznych. Dlatego pytajmy o wszytko czego nie rozumiemy - wyjaśnienia na tym etapie nic nie kosztują, później będzie gorzej. Sprawny kierownik czy analityk może często pomóc naprowadzać klienta na odpowiedni tor pytaniami, jeśli nie będzie on potrafił określić dobrze swoich potrzeb.
Wymagania będziemy spisywać w formie krótkich zdań objaśniających co użytkownik może lub chce zrobić z systemem (używajmy w tym celu wcześniej wyłowionych nazw ról). Np.:
“Klient sklepu internetowego może dodać do swojego koszyka produkt, którego szczegóły ogląda na stronie.”
“Pracownik BOK może zmienić na życzenie klienta zamówienie o podanym numerze. Wymaga to wcześniejszej autentykacji klienta sklepu”.
Takie krótkie opisy wymagań nazywamy historyjkami użytkownika (ang. user stories). Cała istota tych historyjek polega na tym, że powinny one być doskonale zrozumiałe dla klienta/użytkownika, a jednocześnie powinny stanowić dobry pogląd na to co system ma robić i co trzeba zaimplementować. User stories powinny być w miarę krótkie i powinny odpowiadać pojedynczym funkcjonalnościom, lub zestawom funkcjonalności, które nie mają sensu bez siebie nawzajem. Chodzi o to, żeby dało się je implementować osobno i w dowolnej kolejności. Tradycyjnie do zapisywania user stories polecane są niewielkie papierowe kartoniki (na tyle duże, aby takie user story się zmieściło, ale jednocześnie na tyle małe, żeby nie dało się na nich pisać całych opowiadań). Kartoniki są wygodne również z innego powodu - można je dowolnie mieszać, tasować, przekładać i wreszcie w dowolnej chwili taki kartonik można podrzeć.
Drugą ważną cechą user stories jest to, że zawsze powinny przedstawiać punkt widzenia użytkownika/roli. Dlatego aby zweryfikować czy dobrze myślimy o user story można zadać sobie klika pytań pomocniczych:
- Jak zaprezentuję implementację takiej pojedynczej historyjki klientowi?
- Jak użytkownik będzie mógł użyć tak przedstawionej funkcjonalności?
- Jak przetestować czy ta funkcja działa bez pisania dodatkowego kodu?
- Czy do implementacji tej funkcjonalności będę musiał ruszyć wszystkie warstwy systemu?
Znowu wyciągamy jak największą ilość historyjek od klienta/użytkownika. Kończymy kiedy zaczyna brakować pomysłów na nowe user stories. Cała taka sesja nie powinna trwać więcej niż kilka godzin. Jeśli zadanie jest wyjątkowo duże i skomplikowane, to warto podzielić je na mniejsze (np. podsystemy, moduły) i zorganizować klika sesji tematycznych. Z całą pewnością takie sesje zajmą mniej czasu niż tradycyjne wywiady analityków i tworzenie specyfikacji wymagań. Jeśli zespołowi i klientowi nie odpowiadają kartki, można użyć Excela lub innego prostego narzędzia. Chodzi o to, że nie zależy nam szczególnie na formie, tylko na treści.
To jest pewien model. Jeśli nie uda się go zrealizować za pierwszym razem, to trudno. Najważniejsze na tym etapie jest przejście na zapis wymagań odzwierciedlający punkt widzenia użytkownika, bez zbędnego narzutu technicznego. Z takim bagażem historyjek możemy przejść do szacowania…
Szacowanie czasochłonności
Jest wiele różnych technik szacowania. Najistotniejsze, aby w szacowaniu wzięli udział wszyscy członkowie zespołu, bo to oni potem będą mieli podjąć się realizacji poszczególnych zadań. Jeśli zespół jest niedoświadczony, to można do szacowania zaprosić również jednego czy dwoje bardziej doświadczonych kolegów z innych zespołów.
Szacujemy kolejno każdą z zebranych user stories. Każda osoba na własną rękę. Potem omawiamy krótko rozbieżności - ktoś mógł czegoś nie uwzględniać, albo wręcz przeciwnie - któryś z członków zespołu doskonale zna temat i wie, że pewne elementy można wykonać szybciej.
Największym problemem jest zwykle to w jakiej jednostce szacować? Tutaj pojawia się drugie miejsce gdzie można spróbować zastosować podejście najczęściej spotykane w technikach agile. Szacowanie w jednostkach “idealnych” lub wręcz wyimaginowanych (to drugie jest często wspominane ale osobiście uważam, że przynajmniej na start z agile nie bardzo się ono nadaje). Moja rada jest taka - jeśli do tej pory szacowaliśmy w firmie wszystko w osobogodzinach przechodźmy na idealne godziny programistyczne, jeśli w dniach przechodźmy na dni idealne. Jednym słowem, nie zmieniajmy radykalnie jednostki na jakieś punkty czy coś, do czego nie jesteśmy przyzwyczajeni.
Co to jest idealna godzina/dzień programistyczny? Jest to wyidealizowany czas, który poświęcilibyśmy tylko i wyłącznie na pracę nad wyznaczonym zadaniem. Czas zmierzony z zegarkiem w ręku, kiedy wszystkie nasze czynności wiążą się z wykonaniem zadania. Ĺťadnych rozmów z kolegami, żadnych odpowiedzi na rzucone w salę pytania, żadnych telefonów, kawy czy herbaty w kuchni, papierosa czy nawet wyjścia do toalety. Tylko praca w czystej formie. Każdy rozsądnie myślący człowiek powiedziałby w tym miejscu, że przecież takie dni czy godziny praktycznie się nie zdarzają. I oczywiście tak jest. Co więcej jest to zupełnie naturalne. W takim razie co to ma wspólnego z szacowaniem?
Każdy z nas udzielając odpowiedzi na pytanie “Ile potrwa zdanie X?” może jej udzielić, albo na bazie wcześniejszych doświadczeń związanych z takim samym lub podobnym zadaniem, albo starając się szybko w myślach rozłożyć zadanie na mniejsze znane mu elementy. Podświadomie porównujemy w takim momencie postawiony przed nami problem do innych, które już kiedyś rozwiązaliśmy. Chodzi o to, że myśląc o czasie, podświadomie myślimy w jednostkach idealnych. Tymczasem naszej pracy towarzyszy szereg innych czynności nie przekładających się bezpośrednio na tworzenie oprogramowania. Poza codziennymi nawykami, jak zrobienie sobie kawy, zdarza się też wiele zdarzeń nieprzewidywanych. Typowe z nich, to pytania od kolegów, odbieranie telefonów, itp. Nie jesteśmy w stanie przewidzieć ich ilości i zwykle tego nie robimy. “Zrobię to w maksymalnie 4 godziny” oznacza częściej “Zrobię to w maksymalnie 4 godziny, jak nic mi nie będzie przeszkadzać” niż “Jeśli uwzględnić 2 telefony po 5 min, jedno wyjście do toalety i może 3 pytania, którym poświęcę w sumie 10 min, to zadanie o które mnie pytasz zrobię w maksymalnie 4 godz.”
Każdy indywidualnie ma pewne tempo pracy, które można określi np. jako ilość idealnych godzin, jakie dana osoba jest w stanie faktycznie zrealizować w ciągu 8-godzinnego dnia pracy. Albo ilość idealnych dni jakie jesteśmy w stanie faktycznie zrealizować w 5-dniowym tygodniu. Ta wielkość często nosi nazwę wydajności (ang. velocity) i zależy zarówno od indywidualnych cech pracownika jak i od samego projektu czy też otaczającego nas środowiska pracy. Tradycyjne metodyki zarządzania projektami bardzo często nie uwzględniają takiego efektu. Agile natomiast zachęca do przejścia na szacowanie w jednostkach idealnych (skoro i tak okazuje się, że podświadomie właśnie tak szacujemy). Ważne będzie także ciągłe badanie velocity naszego zespołu, bo jest to narzędzie znacznie ułatwiające prognozowanie przyszłego stanu projektu.
Z każdym nowym projektem i przy w miarę stabilnym składzie zespołu powinno udać się nam zauważyć pewną średnią wartość takiej wydajności, a to już bardzo cenna informacja dla np. wycen projektu. Także w trakcie projektu należy badać wydajność, bo rozpoczynając dokonaliśmy pewnego przypuszczenia co do tego, na jakim poziomie będzie się ona kształtowała, a teraz czas to zweryfikować.
Tak więc naszą listę user stories oszacujmy w jednostkach idealnych przy zachowaniu używanej do tej pory wielkości (godzin lub dni). W dużym skrócie, aby podać klientowi przewidywaną cenę projektu, należałoby takie szacunki pomnożyć prze pewien współczynnik. Dla zespołu posiadającego już pewne doświadczenie i nie rozpraszanego zbyt często “zadaniami na boku” przyjąłbym przy pierwszym projekcie bezpiecznie taki współczynnik na poziomie 1,25 - 1,5. Wynik mnożenia da przewidywaną liczbę godzin lub dni roboczych, które już w miarę prosto da się przełożyć na koszty. Więc dalej manager może już z taką informacją przystępować do negocjacji. Niemniej na tym etapie nie robiłbym jeszcze takich mnożeń. Kolejnym krokiem będzie ubranie tak oszacowanych zadań w plan iteracji i ew, wydań, ale o tym następnym razem.
Tymczasem, jeśli kogoś zainteresowało podejście oparte na user stories i velocity (o którym też trochę więcej przy okazji planowania, bo tam będzie to użyteczna informacja), to polecam bardzo dobrą książkę Mike’a Cohna pt. “User Stories Applied” (niestety dostępna po angielsku).
Udanego pisania historyjek.
Kategorie: Agile dla programistów


8 komentarzy Dodaj komentarz
1. Jacek Rybicki | December 8th, 2006 at 14:23
Klient to brzmi dumnie. Sugerowałbym zwrócenie uwagi kto naprawdę jest odbiorcą aplikacji. Jeżeli organizacja klienta jest duża, może się okazać że kontaktujemy się z jedym działem, a produkt od niego odbierają dwa inne, mające własne wyobrażenia. Przekaz informacji okazuje się tak zanieczyszczony, że w dobrej wierze robimy nie to co jest oczekiwane. Dołóżmy do tego sztywny kontrakt i jesteśmy ugotowani.
2. Marcin Niebudek | December 8th, 2006 at 16:01
Oczywiście… zawsze kiedy piszę klient mam tutaj na myśli reprezentanta(ów) użytkowników (tzw. user proxy) lub faktycznych użytkowników. Oczywiście w takich rozmowach powinien uczestniczyć też inwestor (zwykle kto inny płaci a kto inny używa systemu, który robimy), aby mógł on ew. kontrolować zakres produktu za jaki będzie płacił, bo końcowi użytkownicy poproszą o co się da łącznie z wszelkimi możliwymi bajerami o małej wartości biznesowej. Więc ważne aby znaleźć balans między użytecznością i wartością dla przyszłego bizensu.
Pracowałem kiedyś przy implementacji systemu backoffice do zarządzania fragmentem działalności firmy. Wizję i wymagania przedstawiał w głównej mierze sam prezes i ew. jego bezpośredni podwładni (czyli kierownicy działów). Jak łatwo się domyśleć kiedy doszło do wdrożenia, w ciągu pierwszego tygodnia okazało się, że szeregowi pracownicy narzekają, bo ich praca wygląda zupełnie inaczej niż to, w jaki sposób wyobrażał to sobie prezes. Efekt był taki, że system zamiast ułatwiać początkowo utrudniał im wykonanie codziennych czynności.
Dlatego tak ważne jest dotarcie do faktycznych użytkowników systemu. Wydaje mi się też, że prezentacje produktu po iteracjach bardzo pomagają w uniknięciu takich nieporozumień. Kiedy pokażemy takiemu prezesowi jakiś element systemu do akceptacji (bo to przecież ma na celu taka prezentacja - test akceptacyjny), to zwykle żeby zabezpieczyć się przed pomyłką poprosi on o wryfikację faktycznego użytkownika. Dużo łatwiej klientowi, który nie jest użytkownikiem opowiadać o wymaganiach, niż je później zweryfikować i się pod tym podpisać. Więc po iteracjach zyskujemy naturalne punkty kontrolne, w których jest duża szansa dopadnięcia użytkowników :-)
Pozdrawiam,
Marcin
3. Jacek Rybicki | December 13th, 2006 at 14:13
Czy masz jakieś dane na temat badań tego współczynnika wydajności? Nasuwa się mi analogia z metodami mierzenia pracochłonności na podstawie UCP, gdzie uwzględnia się parametry wykonywanego zagadnienia i organizacji.
Wykonanie testów funkcjonalnych dla aplikacji trzeba szacować osobno boi możliwe że magiczny współczynnik będzie inny.
Jeżeli mamy tylko oszacowany koszt wykonania funkcjonalności (obejmującej również dokumentację), w godzinach idealnych, to można zacząć od pomnożenia tego przez dwa - koszt testu (inspekcji). Dopiero potem można zaaplikować współczynnik wydajności zespołu.
pozdrawiam,
Jacek
4. Marcin Niebudek | December 13th, 2006 at 21:20
Niestety nie spotkałem się do tej pory z badaniami na temat współczynnika wydajności wśród różnych zespołów projektowych na zasadzie porównania idealnych szacunków w stosunku do faktycznie spędzonego czasu. Sam bazuje raczej na spostrzeżeniach czy zaleceniach książkowych oraz na własnych próbach.
W przytoczonej przeze mnie książce Cohn twierdzi, że na pierwszą iterację (jeśli “zgadujemy” velocity) powinno się przyjąć wartość pomiędzy jedną trzecią a połową faktycznego czasu. Czyli dla dwóch osób w dwutygodniowej iteracji (20 dni roboczych) przyjmujemy wartość 7-10 dni idealnych. Potem można to zwiększyć.
W małych projektach (3-4 miesięcznych) dla 1-2 osób wychodziła mi wydajność na poziomie ok. 3/4, ale to dopiero pierwsze eksperymenty z mierzeniem tego współczynnika. Generalnie równie ważne jak samo uchwycenie velocity wydaje mi się wspólne szacowanie w zespole. W każdym z tych elementów gdzieś te szacunki się uśredniają i zbliżają do rzeczywistości. Samo velocity raczej traktuje jako pewien współczynnik kontrolny dla stanu “środowiska” projektu, tzn. ważniejszą informacją dla mnie są jego zaburzenia niż wartość średnia.
5. Maciej Mazur | December 22nd, 2006 at 20:45
Napisałeś, że podstawową zasadą to dostarczać klientowi część systemu którą będzie mógł użyć. Czy zawsze da się to zastosować stosując metodę user stories?
Ostatnio spisałem wymagania funkcjonalne za pomocą use case’ów (bardzo podobne do user stories). Istotnym aspektem tego projektu jest to że wymaga on stworzenia silnika, niewielkiej biblioteki, modułu który będzie obsługiwał działanie całej aplikacji. Tymczasem user stories opisują tylko to co użytkownik widzi na ekranie i może wyklikać, czyli na przykład: proces logowania się do serwisu, składania zamówienia, itp. Klient i użytkownik nie ma i nigdy nie będzie wiedział że pod spodem kryje się jakaś biblioteka.
I teraz to co stanowi problem, większość z funkcji opisanych za pomocą user stories nie może funkcjonować bez tego wewnętrzego modułu. Czyli najpierw trzeba stworzyć ten moduł, potem już pójdzie z górki. Jak zatem w początkowych iteracjach zaprezentować coś działającego klientowi?
6. Marcin Niebudek | December 22nd, 2006 at 21:14
Powszechnie przyjętą praktyką w takim przypadku jest zastosowanie iteracji zerowej. Taka iteracja stanowi pewne odstępstwo od zasad, tzn. większość jej czasu przeznacza się właśnie na implementację pewnych ogólnych mechanizmów systemu, które nie konieczne są funkcjonalnościami z punktu widzenia użytkownika. Często zdarza się także, że taka iteracja trwa trochę dłużej niż standardowa iteracja (np. trzy tygodnie zamiast dwóch). I to tyle teorii :-)
Jeśli chodzi o iterację zerową, to mam jeszcze następujące spostrzeżenia:
1. Warto w iteracji zerowej umieści chociaż jedno czy dwa user stories tak, żeby był mimo wszystko jakiś namacalny dla użytkownika efekt na koniec.
2. Nawet przy okazji implementacji silnika niekoniecznie trzeba go zbudować z najdrobniejszymi szczegółami. Aktualnie też mam taki projekt, który skoncentrowany jest wokół takiego silnika. Na iterację zerową przyjmuję kilka najistotniejszych elementów jego architektury i jak najwcześniej staram się przypuścić na niego “atak” przy pomocy funkcjonalności z user stories. Każda implementacja user story potwierdza słuszność przyjętych w silniku rozwiązań lub też szybko wskazuje na jego niedociągnięcia i braki.
3. Przy implementacji takiego silnika zakładamy, że powinien się on sprawdzić we wszystkich user stories. Tylko problem polega na tym, że wiele z user stories nie jest do końca jasnych na początku projektu. I tak jeszcze wielokrotnie taki silnik będziemy podrasowywać i refaktoryzować. Więc to też jest dla mnie powód do skracania tej wstępnej iteracji.
7. Jacek Rybicki | January 6th, 2007 at 00:50
Silniki? Biblioteki? Czasem jest to coś czego sobie klient bezpośrednio życzy - klient może mieć czasem porównywalną z członkami zespołu albo i większą świadomość tematu. Jeżeli jednak potrzeba wydzielenia warstwy wychodzi od projektantów, to można by się zastanowić nad potraktowaniem tego jako sub-projekt z własnymi user stories, opracowanymi na podstawie tego co jest potrzebne dla wyższego poziomu. Pojawia mi się teraz przed oczami wizja silnika coraz to lepszego, tuningowanego, “development driven development” w czystej postaci.
Jeżeli klient silnika nie zamówił to nie będzie go weryfikował oddzielnie i co chwilę trzeba sobie zadawać pytanie jak duży i elastyczny “silnik” jest potrzebny. Jeżeli jednak zamówił, to jest w stanie określić coś w rodzaju user stories (use-cases) z których można wyprodukować testy akceptacyjne.
Produkcja wewnętrznych bibliotek w iteracji zerowej? Naprawdę tak wcześnie jesteście w stanie coś sensownego urodzić? Ja bym się skłaniał do nazywania tego zarysami bibliotek, wstępnym grupowaniem podobnych elementów, które potem można organizować w coś reużytkowalnego.
Iterację zerową i ogólnie początek projektu lepiej wykorzystać na robienie prowizorki - prototypów, zaślepek na interfejsy, rysunków, szukanie problemów i ogónie badanie tematu.
Oczywiście wszystko zależy od sytuacji :)
8. Marcin Niebudek | January 6th, 2007 at 21:28
Produkcja wewnętrznych bibliotek w iteracji zerowej… nie nazwałem tego tak i chyba bym nie nazwał. Dla mnie silnik i wew. biblioteki to dwie zupełnie różne rzeczy i nie włożyłbym ich do jednego worka.
Masz 100% rację, że czasem takich elementów wymaga sam klient, ale wtedy przecież wszystko jest dużo łatwiejsze, bo skoro klient żąda biblioteki, to prosto jest stworzyć user stories opisujące elementy takiej biblioteki (choćby metody odpowiednich klas odpowiadające poszczególnym usługom tej biblioteki). Prezentacja takich user stories mogłaby polegać nawet na pokazaniu kodu testów jednostkowych - tam widać zarówno sposób użycia jak i poprawność działania.
A wracając do iteracji zerowej i silników, czyli w moim rozumieniu pewnych podstawowych/kluczowych elementów systemu wynikających z jego architektury, to chodzi o to, że skoro czujemy konieczność zbudowania ich na początku, to niech się to stanie w iteracji zerowej. Zaznaczam, że mam tu na myśli implementację absolutnie najpotrzebniejszych elementów, bez których nie da się iść dalej. Nie należy starać się zaimplementować od razu pełnego rozwiązania, bo nie ma to sensu. Już kolejna iteracja dowiedzie, że jednak warto coś zmienić. Osobiście nie udało mi się jeszcze w iteracji zerowej napisać czegoś co można by nazwać czymś więcej niż tylko prototypem/eksperymentem, a co podlegałoby ciągłym zmianom w kolejnych iteracjach i w momentach kiedy tylko było to potrzebne. Jednak wielokrotnie co do idei i podstawowego szkieletu, taki silnik z iteracji zerowej nie podlegał już znacznym przebudowom, bo jego wysokopoziomowa budowa wynikała z przyjętej architektury (też na ogólnym ideowym poziomie).
Także jestem tego samego zdania - iteracja zerowa, to przede wszystkim miejsce na eksperymenty i weryfikację pewnych podstawowych pomysłów. Chodzi tylko o to, żeby postarać się mimo wszystko dać na końcu także coś klientowi.
Skomentuj wpis
Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>
Trackback this post | Subscribe to the comments via RSS Feed