Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 6.
Do tej pory udało nam się wyodrębnić kod do refaktororyzacji do oddzielnego pliku i przygotować automatyczne testy, które będą zabezpieczeniem przed naruszeniem funkcjonalności kodu. W toku tych działań umknęła jednak jedna kwestia. Otóż po wyizolowaniu kodu do oddzielnego pliku, wszelkie kolejne działania – w tym zmiany przystosowujące go do testów, odbywały się już na kopii tego pliku, a nie na oryginale, który pozostał w pierwotnym projekcie. Po zakończeniu refaktoryzacji, powstały kod trzeba będzie dołączyć do pierwotnego projektu dostosowując wywołania w metodach zdarzeń. Oznacza to, że użyty wzorzec Obiektu Metody przestanie istnieć. Z tego też względu nie siliłem się na razie specjalnie, aby dobrze nazwać klasę – ma ona po prostu nazwę implementowanego wzorca. Jeżeli z jakichś względów okaże się, że warto ją pozostawić – wówczas nadana zostanie jej stosowana nazwa.
Jak refaktoryzować? Nie ma tutaj z góry ustalonego, szczegółowego scenariusza – są ogólne zalecenia. Na pewno można jednak stwierdzić, jak nie refaktoryzować – od razu do właściwej postaci. Jest to po prostu niemożliwe. Owszem w niektórych przypadkach, kiedy kod będzie niewielki, niezbyt złożony, da się od razu określić, że ostatecznie powinien wyglądać w taki, a nie inny sposób. W tej ocenie pomaga w dużej mierze i wiedza, i doświadczenie. Jednak w większości przypadków uzyskanie właściwego kodu od razu nie jest możliwe. Tak samo zresztą, jak w przypadku pisania kodu od nowa. Tu też po jego napisaniu i odłożeniu na jakiś czas, po powrocie do niego widać, że można było sporo kwestii zrealizować inaczej.
Pamiętam, że kiedyś miałem ambicje, aby tworzyć kod jak najlepszy, żeby nie tracić czasu na niepotrzebne próby. Gorzkim owocem tego myślenia było … tracenie o wiele więcej czasu, niż gdybym owe próby podjął. Siedziałem i zastanawiałem się nad tym jak kod powinien wyglądać, każdy pomysł, rozwiązanie rozpatrywałem na wiele sposobów. Rzecz w tym, że kod nie powstawał. Chciałem się de facto teleportować do właściwego rozwiązania ;). Kiedy już miałem wrażenie (plus jakieś notatki), że to jest to, przystępowałem do pisania kodu, po czym okazywało się, że to jednak nie to. I koniec końców wychodziło coś innego, często niezadowalającego, bo z braku czasu (przeznaczonego na przemyślenia) nie można już było kodu doprowadzić do właściwej postaci.
Dlaczego tak robiłem? Bardzo często spotykałem się z opinią, że programista nie siada i nie piszę kodu bezmyślnie. Z tych – jakże popularnych w środowisku programistów opinii, wynikało, że najpierw trzeba przemyśleć, zaplanować, rozważyć, itd., itp. Jaki jednak jest sens w rozpisaniu funkcjonalności jako zwykłego tekstu, skoro można po prostu napisać kawałek kodu, który tę funkcjonalność równie dobrze – jeśli nie lepiej – opisze? Owszem, potrzebne jest planowanie, ale nie szczegółowe, nie obejmujące cały proces, bo nie wiadomo czy on naprawdę ma wyglądać tak, jak nam się obecnie wydaje. To ma być tu i teraz. Plan ma obejmować kilka punktów, które w chwili obecnej są wiadome i wydają się – także z punktu widzenia teraz – właściwe do wykonania. Tu jedna ważna uwaga – nie należy się do tego planu przywiązywać. Jeśli po realizacji pierwszych punktów okaże się, że plan rozmija się z właśnie uzyskaną rzeczywistością – po prostu zmienić plan. Tak samo, jak zmienia się kod. Najlepiej, choć nie – wróć – bo to nie znaczy, że to co wydaje mi się najlepsze będzie też takie dla innych. Na swój użytek stosuję coś na kształt mantry, która mi bardzo pomaga: „pisze ten kod (plan, cokolwiek) po to, aby mógł się zmienić”. Jest to w sumie rozwinięcie zasady, którą można napotkać w większości literatury – szczególnie tej dotyczącej programowania zwinnego – która mówi, że są dwie niezmienne rzeczy: podatki i to, że kod się zmieni ;).
Ale nie uciekajmy w zbyt dużo dygresji :). Pora przejść do meritum i przedstawić, jak taki plan mógłby wyglądać. Co wiemy tu i teraz o kodzie, który mamy zmienić? Wiemy, że kod się powtarza. A co dokładnie się w nim powtarza? Funkcjonalności. Jakie z nich są praktycznie niezmienne (czyli wykonuje się to samo, a to co się ewentualnie zmienia, to tylko otoczenie tego wykonania – np. są inne parametry)?
- wybieranie pliku,
- sprawdzenie czy plik wybrano,
- odczytywanie pliku,
- pokazywanie postępu przetwarzania,
- generowanie xml,
- sprawdzenie czy xml dał się wygenerować,
- przekazywanie xml gdzieś dalej,
- dwie metody mają ten sam sposób interpretacji danych (ImportCSV i ImportTabSeparated).
No proszę! Pomijając ostatni punkt mamy de facto opisany algorytm działania importu pliku. Nie, nie będziemy go obecnie zmieniać, choć może niektórym wydaje się, że jest niedoskonały. Na razie taki pozostanie, bo na razie chodzi o to, aby sprowadzić wszystkie cztery metody do wspólnego mianownika, jakim jest powyższy scenariusz. Czyli naszym celem jest uzyskanie jednej metody, która będzie go realizowała, uwzględniając oczywiście specyfikę wszystkich czterech formatów plików z danymi.
Od czego zacząć? Nie ma w powyższym scenariuszu nic, co wskazywałoby na to, że choć jeden z punktów ma wyższy priorytet niż inne. Więc właściwie można zacząć od początku. Czyli od wybierania pliku. No dobrze, ale jak wybierać plik, przecież już jest wybierany – za każdym razem niemal identycznie – podawana jest inna wartość filtra i to załatwia sprawę. Właściwie tak, ale jest tu jeden dość nieciekawy aspekt – zależność od konkretnego obiektu umożliwiającego wybór pliku. Czy to źle, ktoś spyta? Przecież właściwie do tego ten obiekt jest – czego innego używać? Obecnie, w tym konkretnym aspekcie – owszem, ale kto zapewni, że kod po naszej refaktoryzacji nie okaże się tak rewelacyjny, że zostanie zastosowany w innym projekcie, a ten będzie wyposażony w interfejs WWW? Albo użyty zostanie WPF? Otóż to. Wtedy trzeba będzie dostosować kod i siłą rzeczy albo znowu się go powieli (bo trzeba będzie użyć obiektu innej klasy), albo trzeba będzie używać jakichś warunków kompilacji czy innych niepotrzebnych sztuczek. A nawet jeśli kod nie zostanie wykorzystany nigdzie indziej, to co z testami? Pamiętacie, co trzeba było zrobić, aby obecną postać kodu dostosować do testów automatycznych?
Podsumowując: jak widać zależność od konkretnego obiektu jest jednak niepożądana. Jak ją wyeliminować? Stosując ostatnią z reguł SOLID (o nich na pewno napisze jeszcze na tym blogu – na razie musi wystarczyć Wikipedia) tj. Dependency Inversion Principle czyli Regułę Odwracania Zależności. Mówi ona (za Agile. Programowanie zwinne: zasady, wzorce i praktyki zwinnego wytwarzania oprogramowania w C#):
A. Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji.
B. Abstrakcje nie powinny zależeć od szczegółowych rozwiązań. To szczegółowe rozwiązania powinny zależeć od abstrakcji.
Czym jest abstrakcja używana w treści reguły. To klasa abstrakcyjna lub – częściej – interfejs. Czyli wybór pliku powinien być realizowany poprzez interfejs. Wtedy zawsze można napisać klasę, która ten interfejs implementuje i uzyskać oczekiwany efekt – wybór pliku – jak nie byłby on realizowany. Klasa ta nie musi wcale implementować owego wyboru osobiście. Może delegować to zadanie do innej klasy, której instancją będzie zarządzać.
Nabywszy tę wiedzę w kolejnej części cyklu przystąpimy do zdefiniowania i implementacji takiego interfejsu. Patrząc zaś na powstałą powyżej listę oraz mając w pamięci przystosowywanie kodu do testów, możemy od razu stwierdzić, że taki sam interfejs potrzebny będzie dla mechanizmu prezentowania postępu przetwarzania. Czy będzie też potrzebny interfejs dla informowania o niepomyślnym wygenerowaniu XML-a? Dla tego przypadku istnieje o wiele lepszy mechanizm, ale nie uprzedzajmy faktów ;).