Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00083 010783 10754280 na godz. na dobę w sumie
Praca z zastanym kodem. Najlepsze techniki - książka
Praca z zastanym kodem. Najlepsze techniki - książka
Autor: Liczba stron: 440
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-3692-6 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> inne - programowanie
Porównaj ceny (książka, ebook, audiobook).

Naucz się pracować na gotowych projektach!

Programiści uwielbiają brać udział w nowych projektach, być świadkami ewolucji kodu, mieć wpływ na wybór narzędzi i projektować ich architekturę. Niestety, w ogromnej większości przypadków muszą pracować z kodem mającym wiele lat i pisanym przez wiele osób. Jak sobie poradzić w takim środowisku? Jak dobrać techniki pracy do gotowego kodu? Na te i wiele innych podobnych pytań odpowiada ten wyjątkowy podręcznik.

Dzięki niemu dowiesz się, jak wprowadzać zmiany w zastanym kodzie, tworzyć testy automatyczne oraz modyfikować architekturę rozwiązania. Ponadto poznasz najlepsze techniki pracy z projektami niezorientowanymi obiektowo oraz przekonasz się, że można skutecznie poradzić sobie z przerośniętymi klasami i metodami. Ostatnia część książki została poświęcona technikom usuwania zależności. Ten podręcznik to lektura obowiązkowa każdego programisty. Dzięki niemu Twoja praca z zastanym kodem nabierze nowego sensu. Musisz ją mieć!

Dzięki tej książce:

Praca z zastanym kodem nie musi być nużąca!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: Working Effectively with Legacy Code Tłumaczenie: Ireneusz Jakóbik Projekt okładki: Studio Gravite/Olsztyn Obarek, Pokoński, Pazdrijowski, Zaprucki ISBN: 978-83-246-8317-8 Authorized translation from the English language edition, entitled: WORKING EFFECTIVELY WITH LEGACY CODE; ISBN 0131177052; by Michael C. Feathers; published by Pearson Education, Inc, publishing as Prentice Hall. Copyright © 2005 by Pearson Education, Inc. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Polish language edition published by HELION S.A., Copyright © 2014. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie bierze jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Wydawnictwo HELION nie ponosi również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Materiały graficzne na okładce zostały wykorzystane za zgodą Shutterstock Images LLC. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/prazak.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/prazak Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis treści Słowo wstępne ........................................................................................................................... 9 Przedmowa ............................................................................................................................. 11 Wstęp ...................................................................................................................................... 17 Część I: Mechanika zmian ........................................................................................ 19 Rozdział 1. Zmiany w oprogramowaniu ................................................................................ 21 Cztery powody wprowadzania zmian w oprogramowaniu .............................................21 Ryzykowna zmiana ................................................................................................................25 Rozdział 2. Praca z informacją zwrotną ................................................................................ 27 Co to jest testowanie jednostkowe? .....................................................................................30 Testy wyższego poziomu .......................................................................................................32 Pokrycie testami .....................................................................................................................33 Algorytm dokonywania zmian w cudzym kodzie .............................................................36 Rozdział 3. Rozpoznanie i separowanie ................................................................................. 39 Fałszywi współpracownicy ....................................................................................................41 Rozdział 4. Model spoinowy ................................................................................................... 47 Ogromny arkusz z tekstem ...................................................................................................47 Spoiny ......................................................................................................................................48 Rodzaje spoin ..........................................................................................................................51 Rozdział 5. Narzędzia ............................................................................................................. 63 Narzędzia do automatycznej refaktoryzacji .......................................................................63 Obiekty pozorowane ..............................................................................................................65 Jarzmo testowania jednostkowego ......................................................................................66 Ogólne jarzmo testowe ..........................................................................................................71 Część II: Zmiany w oprogramowaniu ...................................................................... 73 Rozdział 6. Nie mam zbyt wiele czasu, a muszę to zmienić .................................................. 75 Kiełkowanie metody ..............................................................................................................77 Kiełkowanie klasy ...................................................................................................................80 Opakowywanie metody .........................................................................................................85 Opakowywanie klasy .............................................................................................................88 Podsumowanie .......................................................................................................................93 Poleć książkęKup książkę 6 SPIS TREŚCI Rozdział 7. Dokonanie zmiany trwa całą wieczność ............................................................. 95 Zrozumienie ............................................................................................................................95 Opóźnienie ..............................................................................................................................96 Usuwanie zależności ..............................................................................................................97 Podsumowanie .................................................................................................................... 102 Rozdział 8. Jak mogę dodać nową funkcjonalność? ............................................................ 103 Programowanie sterowane testami ................................................................................... 104 Programowanie różnicowe ................................................................................................ 110 Podsumowanie .................................................................................................................... 119 Rozdział 9. Nie mogę umieścić tej klasy w jarzmie testowym ............................................. 121 Przypadek irytującego parametru ..................................................................................... 121 Przypadek ukrytej zależności ............................................................................................. 128 Przypadek konstrukcyjnego kłębowiska .......................................................................... 131 Przypadek irytującej zależności globalnej ........................................................................ 133 Przypadek straszliwych zależności dyrektyw include .................................................... 141 Przypadek cebulowego parametru .................................................................................... 144 Przypadek zaliasowanego parametru ............................................................................... 147 Rozdział 10. Nie mogę uruchomić tej metody w jarzmie testowym ................................... 151 Przypadek ukrytej metody ................................................................................................. 152 Przypadek „pomocnych” funkcji języka .......................................................................... 155 Przypadek niewykrywalnych skutków ubocznych ......................................................... 158 Rozdział 11. Muszę dokonać zmian. Które metody powinienem przetestować? ............... 165 Myślenie o skutkach ............................................................................................................ 166 Śledzenie w przód ................................................................................................................ 171 Propagacja skutków ............................................................................................................ 176 Narzędzia do wyszukiwania skutków ............................................................................... 177 Wyciąganie wniosków z analizy skutków ........................................................................ 179 Upraszczanie schematów skutków ................................................................................... 180 Rozdział 12. Muszę dokonać wielu zmian w jednym miejscu. Czy powinienem pousuwać zależności we wszystkich klasach, których te zmiany dotyczą? ................. 183 Punkty przechwycenia ........................................................................................................ 184 Ocena projektu z punktami zwężenia .............................................................................. 191 Pułapki w punktach zwężenia ........................................................................................... 192 Rozdział 13. Muszę dokonać zmian, ale nie wiem, jakie testy napisać ............................... 195 Testy charakteryzujące ....................................................................................................... 196 Charakteryzowanie klas ..................................................................................................... 199 Testowanie ukierunkowane ............................................................................................... 200 Heurystyka pisania testów charakteryzujących .............................................................. 205 Rozdział 14. Dobijają mnie zależności biblioteczne ........................................................... 207 Rozdział 15. Cała moja aplikacja to wywołania API ........................................................... 209 Poleć książkęKup książkę SPIS TREŚCI 7 Rozdział 16. Nie rozumiem wystarczająco dobrze kodu, żeby go zmienić ........................ 219 Notatki i rysunki .................................................................................................................. 220 Adnotowanie listingów ...................................................................................................... 221 Szybka refaktoryzacja ......................................................................................................... 222 Usuwanie nieużywanego kodu .......................................................................................... 223 Rozdział 17. Moja aplikacja nie ma struktury ..................................................................... 225 Opowiadanie historii systemu ........................................................................................... 226 Puste karty CRC .................................................................................................................. 230 Analiza rozmowy ................................................................................................................ 232 Rozdział 18. Przeszkadza mi mój testowy kod .................................................................... 235 Konwencje nazewnicze klas ............................................................................................... 235 Lokalizacja testu .................................................................................................................. 236 Rozdział 19. Mój projekt nie jest zorientowany obiektowo. Jak mogę bezpiecznie wprowadzać zmiany? ...................................................................................................... 239 Prosty przypadek ................................................................................................................. 240 Przypadek trudny ................................................................................................................ 241 Dodawanie nowego zachowania ....................................................................................... 244 Korzystanie z przewagi zorientowania obiektowego ..................................................... 247 Wszystko jest zorientowane obiektowo ........................................................................... 250 Rozdział 20. Ta klasa jest za duża, a ja nie chcę, żeby stała się jeszcze większa .................. 253 Dostrzeganie odpowiedzialności ...................................................................................... 257 Inne techniki ........................................................................................................................ 269 Posuwanie się naprzód ....................................................................................................... 270 Po wyodrębnieniu klasy ..................................................................................................... 273 Rozdział 21. Wszędzie zmieniam ten sam kod .................................................................... 275 Pierwsze kroki ...................................................................................................................... 278 Rozdział 22. Muszę zmienić monstrualną metodę, lecz nie mogę napisać do niej testów .... 293 Rodzaje monstrów .............................................................................................................. 294 Stawianie czoła monstrom przy wsparciu automatycznej refaktoryzacji ................... 297 Wyzwanie ręcznej refaktoryzacji ...................................................................................... 300 Strategia ................................................................................................................................ 307 Rozdział 23. Skąd mam wiedzieć, czy czegoś nie psuję? ...................................................... 311 Superświadome edytowanie .............................................................................................. 312 Edytowanie jednego elementu naraz ................................................................................ 313 Zachowywanie sygnatur ..................................................................................................... 314 Wsparcie kompilatora ........................................................................................................ 317 Programowanie w parach .................................................................................................. 318 Rozdział 24. Czujemy się przytłoczeni. Czy nie będzie chociaż trochę lepiej? ................... 321 Poleć książkęKup książkę 8 SPIS TREŚCI Część III: Techniki usuwania zależności ............................................................... 325 Rozdział 25. Techniki usuwania zależności ......................................................................... 327 Adaptacja parametru .......................................................................................................... 328 Wyłonienie obiektu metody .............................................................................................. 332 Uzupełnianie definicji ........................................................................................................ 338 Hermetyzacja referencji globalnej .................................................................................... 340 Upublicznienie metody statycznej .................................................................................... 346 Wyodrębnienie i przesłonięcie wywołania ...................................................................... 349 Wyodrębnienie i przesłonięcie metody wytwórczej ...................................................... 351 Wyodrębnienie i przesłonięcie gettera ............................................................................. 353 Wyodrębnienie implementera .......................................................................................... 356 Wyodrębnienie interfejsu .................................................................................................... 361 Wprowadzenie delegatora instancji ................................................................................. 367 Wprowadzenie statycznego settera ................................................................................... 370 Zastępowanie biblioteki ..................................................................................................... 375 Parametryzacja konstruktora ............................................................................................ 377 Parametryzacja metody ...................................................................................................... 381 Uproszczenie parametru .................................................................................................... 383 Przesunięcie funkcjonalności w górę hierarchii ............................................................. 386 Przesunięcie zależności w dół hierarchii .......................................................................... 390 Zastąpienie funkcji wskaźnikiem do funkcji ................................................................... 393 Zastąpienie referencji globalnej getterem ........................................................................ 396 Utworzenie podklasy i przesłonięcie metody .................................................................. 398 Zastąpienie zmiennej instancji .......................................................................................... 401 Przedefiniowanie szablonu ................................................................................................ 405 Przedefiniowanie tekstu ..................................................................................................... 409 Dodatek: Refaktoryzacja ...................................................................................................... 411 Wyodrębnianie metody ...................................................................................................... 411 Słownik .................................................................................................................................. 415 Skorowidz ............................................................................................................................. 417 Poleć książkęKup książkę Rozdział 9. Nie mogę umieścić tej klasy w jarzmie testowym Będzie ciężko. Gdyby utworzenie instancji klasy w jarzmie testowym zawsze było łatwe, książka ta byłaby o wiele krótsza. Niestety, często zadanie to jest trudne. Oto cztery najczęściej występujące problemy, które napotykamy: 1. Nie da się w prosty sposób utworzyć obiektów klasy. 2. Nie da się w prosty sposób przeprowadzić procesu budowy jarzma testowego z umieszczoną w nim klasą. 3. Korzystanie z konstruktora, którego potrzebujemy użyć, wywołuje skutki uboczne. 4. Konstruktor wykonuje sporo pracy, a my musimy ją rozpoznać. W rozdziale tym zajmiemy się serią przykładów, które skupiają się na tych problemach, z uwzględnieniem różnych języków. Istnieje więcej niż tylko jeden sposób poradzenia sobie z każdym z tych problemów. Zaznajomienie się z tymi przykładami jest jednak niezłą metodą na poznanie całego arsenału technik usuwania zależności i nauczenia się, które z nich wybrać i jak je stosować w określonych sytuacjach. Przypadek irytującego parametru Kiedy muszę wprowadzić zmianę w cudzym systemie, zwykle początkowo jestem nasta- wiony bardzo optymistycznie. Nie mam pojęcia dlaczego. Na ile tylko mogę, próbuję być realistą, ale optymizm zawsze się przebija. „Hej”, mówię do siebie (albo do kolegi), „wy- gląda na to, że będzie łatwo. Musimy tylko co(cid:258)tam trochę stentegowa(cid:202), i po robocie”. Wszystko to brzmi prosto, kiedy się o tym mówi, aż bierzemy się do klasy Co(cid:258)Tam (czym- kolwiek by ona była) i się jej przyglądamy. „No dobra. Musimy więc dodać metodę tutaj, zmienić inną metodę tam i oczywiście wrzucić całość do jarzma testowego”. W tym Poleć książkęKup książkę 122 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM momencie zaczynam nieco wątpić. „Motyla noga! Wygląda na to, że najprostszy kon- struktor w tej klasie przyjmuje trzy parametry, ale — dodaję optymistycznie — być może utworzenie obiektu wcale nie będzie takie trudne”. Zajmijmy się przykładem i zobaczmy, czy mój optymizm ma podstawy, czy jest tylko W kodzie systemu realizującego płatności znajduje się nieprzetestowana klasa Javy mechanizmem obronnym. o nazwie CreditValidator. public class CreditValidator { public CreditValidator(RGHConnection connection, CreditMaster master, String validatorID) { ... } Certificate validateCustomer(Customer customer) throws InvalidCredit { ... } ... } Jedną z wielu funkcji tej klasy jest informowanie nas, czy klienci mają otwarty kredyt. Jeśli tak, otrzymujemy certyfikat informujący o wysokości kredytu. Jeśli nie, klasa zgłasza wyjątek. Nasza misja, o ile tylko ją przyjmiemy, polega na dodaniu do tej klasy nowej metody. Metoda będzie nosić nazwę getValidationPercent, a jej zadaniem będzie informowanie nas o odsetku udanych wywołań metody validateCustomer w czasie działania walidatora. Od czego zaczniemy? Kiedy musimy utworzyć obiekt w jarzmie testowym, często najlepszym podejściem jest po prostu próba jego utworzenia. Moglibyśmy przeprowadzić rozległą analizę, aby dowie- dzieć się, dlaczego będzie (albo też nie będzie) to łatwe bądź trudne, ale równie proste będzie utworzenie klasy testowej jUnit, wprowadzenie do niej kodu i skompilowanie go. public void testCreate() { CreditValidator validator = new CreditValidator(); } Najlepszym sposobem na stwierdzenie, czy będziesz mieć kłopoty podczas tworzenia in- stancji klasy w jarzmie testowym, jest po prostu próba jej utworzenia. Napisz przypadek testowy i spróbuj w jego ramach utworzyć obiekt. Kompilator powie Ci, czego potrzebu- jesz, aby odnieść sukces. To jest test konstrukcyjny. Testy konstrukcyjne wyglądają trochę dziwnie. Kiedy piszę taki test, zwykle nie umieszczam w nim asercji. Po prostu próbuję skonstruować obiekt. Później, gdy mam już możliwość tworzenia obiektów w jarzmie testowym, zwykle pozby- wam się takiego testu albo zmieniam jego nazwę, żebym mógł go wykorzystać do spraw- dzenia czegoś ważniejszego. Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEGO PARAMETRU 123 Wróćmy jednak do naszego przykładu. Do konstruktora nie dodaliśmy jeszcze żadnych argumentów, więc kompilator narzeka. Mówi nam, że dla klasy CreditValidator nie ma domyślnego konstruktora. Szperając w kodzie, odkrywamy, że potrzebujemy klasy RGHConnection, klasy CreditMaster oraz hasła. Klasy te mają tylko po jednym konstruktorze i wyglądają następująco: public class RGHConnection { public RGHConnection(int port, String Name, string passwd) throws IOException { ... } } public class CreditMaster { public CreditMaster(String filename, boolean isLocal) { ... } } Kiedy konstruowany jest obiekt klasy RGHConnection, łączy się on z serwerem. Podczas połączenia pobierane są z serwera wszystkie dane potrzebne do zweryfikowania kredytu klienta. Druga klasa, CreditMaster, przekazuje nam informacje polityczne, z których korzy- stamy, podejmując decyzje kredytowe. Podczas konstruowania obiekt klasy CreditMaster wczytuje informacje z pliku i zapisuje je w pamięci, abyśmy mogli z nich skorzystać. Wygląda więc na to, że umieszczenie tej klasy w jarzmie testowym będzie dość łatwe, prawda? Nie tak szybko. Możemy napisać test, ale czy damy radę z nim pracować? public void testCreate() throws Exception { RGHConnection connection = new RGHConnection(DEFAULT_PORT, admin , rii8ii9s ); CreditMaster master = new CreditMaster( crm2.mas , true); CreditValidator validator = new CreditValidator( connection, master, a ); } Okazuje się, że nawiązywanie przez metodę RGHConnection połączenia z serwerem w czasie testu nie jest dobrym pomysłem. Zabiera to sporo czasu, a serwer nie zawsze odpowiada. Z kolei klasa CreditMaster nie stwarza problemów. Kiedy tworzymy jej in- stancję, plik jest wczytywany szybko, poza tym jest on tylko do odczytu, w związku z czym nie musimy się obawiać, że testy go uszkodzą. Kiedy chcemy utworzyć walidator, prawdziwą przeszkodę stanowi klasa RGHConnection — jest ona irytującym parametrem. Gdybyśmy mogli utworzyć jakiś rodzaj fałszywego obiektu tej klasy i sprawić, że CreditValidator uwierzy, iż komunikuje się z autentycznym obiektem, moglibyśmy uniknąć całej masy problemów związanych z połączeniem. Spójrzmy na metody, które udostępnia klasa RGHConnection (rysunek 9.1). Poleć książkęKup książkę 124 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM Rysunek 9.1. Klasa RGHConnection Wygląda na to, że klasa RGHConnection zawiera zbiór metod, które obsługują mecha- nizm nawiązywania połączenia: connect, disconnect i retry, a także kilka metod biz- nesowych, takich jak RFDIReportFor i ACTIOReportFor. Pisząc naszą nową metodę dla klasy CreditValidator, będziemy musieli wywołać metodę RFDIReportFor, aby uzyskać wszyst- kie potrzebne nam informacje. Zazwyczaj dane te pochodzą z serwera, ale ponieważ chcemy uniknąć nawiązywania rzeczywistego połączenia, będziemy musieli znaleźć jakiś sposób na ich udostępnienie przez nas. W tym przypadku najlepszą metodą na utworzenie fałszywego obiektu będzie wyod- rębnienie interfejsu (361) w odniesieniu do klasy RGHConnection. Jeśli dysponujesz narzę- dziem, które wspiera refaktoryzację, prawdopodobnie udostępnia ono wyodrębnianie interfejsu. Jeżeli Twoje środowisko programistyczne nie wspiera tej techniki, pamiętaj, że wyodrębnianie interfejsu jest na tyle proste, że można je wykonać ręcznie. Po wyodrębnieniu interfejsu (361) otrzymujemy taką strukturę, jak pokazano na rysunku 9.2. Rysunek 9.2. Klasa RGHConnection po wyodrębnieniu interfejsu Możemy przystąpić do pisania testów, tworząc niedużą fałszywą klasę, która udo- stępnia potrzebne nam raporty: public class FakeConnection implements IRGHConnection { Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEGO PARAMETRU 125 public RFDIReport report; public void connect() {} public void disconnect() {} public RFDIReport RFDIReportFor(int id) { return report; } public ACTIOReport ACTIOReportFor(int customerID) { return null; } } Mając tę klasę, możemy rozpocząć tworzenie takich testów: void testNoSuccess() throws Exception { CreditMaster master = new CreditMaster( crm2.mas , true); IRGHConnection connection = new FakeConnection(); CreditValidator validator = new CreditValidator( connection, master, a ); connection.report = new RFDIReport(...); Certificate result = validator.validateCustomer(new Customer(...)); assertEquals(Certificate.VALID, result.getStatus()); } Klasa FakeConnection jest trochę dziwna. Jak często w ogóle piszemy metody, które nie mają żadnego ciała i tylko zwracają pustą wartość? Co gorsza, ma ona publiczną zmienną, której każdy może nadać taką wartość, jaką tylko zechce. Wydaje się, że klasa ta narusza wszelkie obowiązujące reguły. Otóż w rzeczywistości ich wcale nie narusza. Re- guły dla klas umożliwiających przeprowadzanie testów są inne. Kod klasy FakeConnection nie jest kodem produkcyjnym. Nigdy nie zostanie uruchomiony w naszej pełnej aplikacji — będzie działać wyłącznie w jarzmie testowym. Teraz, kiedy możemy już utworzyć walidator, mamy możliwość napisania metody getValidationPercent. Oto test, który ją weryfikuje: void testAllPassed100Percent() throws Exception { CreditMaster master = new CreditMaster( crm2.mas , true); IRGHConnection connection = new FakeConnection( admin , rii8ii9s ); CreditValidator validator = new CreditValidator( connection, master, a ); connection.report = new RFDIReport(...); Certificate result = validator.validateCustomer(new Customer(...)); assertEquals(100.0, validator.getValidationPercent(), THRESHOLD); } Kod testowy a kod produkcyjny Kod testowy nie musi spełniać tych samych standardów co kod produkcyjny. Zazwyczaj nie mam nic przeciwko naruszaniu zasady hermetyzacji poprzez tworzenie zmiennych pu- blicznych, jeśli uprości to pisanie testów. Kod testowy powinien być jednak przejrzysty; powinien być łatwy do zrozumienia i prosty w modyfikowaniu. Spójrz na testy testNoSuccess i testAllPassed100Percent w tym przykładzie. Czy zawierają one powielony kod? Tak. Powtórzone są trzy pierwsze wiersze. Powinny one zostać wyod- rębnione i umieszczone w jednym miejscu — metodzie setUp() tej klasy testowej. Poleć książkęKup książkę 126 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM Test ten sprawdza, czy procent uwierzytelnienia wynosi mniej więcej 100.0, gdy otrzymujemy pojedynczy, ważny certyfikat kredytu. Test działa poprawnie, ale pisząc kod metody getValidationPercent, zauważamy coś interesującego. Okazuje się, że nie będzie ona w ogóle używać metody CreditMaster. Dlaczego więc ją piszemy i przekazujemy do obiektu klasy CreditValidator? Może wcale nie musimy tego robić? Instancję klasy CreditValidator moglibyśmy utworzyć w naszym teście następująco: CreditValidator validator = new CreditValidator(connection, null, a ); Czy jeszcze się nie pogubiłeś? Sposób, w jaki ludzie reagują na tego typu kod, sporo mówi o systemach, z którymi oni pracują. Jeżeli widząc ten kod, powiedziałeś: „O, świetnie. Do konstruktora przekazywana jest wartość null. W naszym systemie ciągle tak robimy”, prawdopodobnie zajmujesz się dość paskudnym systemem. Zapewne wszędzie masz w nim porozrzucane instrukcje sprawdzające występowanie pustej wartości i pełno kodu warunkowego określającego, co można, a co trzeba z nią zrobić. Z drugiej jednak strony, jeśli spojrzałeś na powyższy kod i stwierdziłeś: „Z tym facetem chyba jest coś nie tak! Przekazywanie w systemie wartości null? Czy on w ogóle ma o czymkolwiek pojęcie?” — otóż tym z Was z tej drugiej grupy (a przynajmniej tym, którzy nadal to czytają i nie zamknęli z rozmachem tej książki w księgarni) — chciałbym powiedzieć coś takiego: pamiętajcie, że robimy to tylko w testach. Najgorsze, co się może stać, to to, że jakiś fragment kodu spróbuje skorzystać z tej zmiennej. W naszym przypadku środowisko uruchomieniowe Javy zgłosi wyjątek. Ponieważ jarzmo wyłapuje wszystkie wyjątki zgłaszane podczas testów, dość szybko dowiemy się, czy jakiś parametr jest w ogóle używany. Przekazywanie pustej wartości Kiedy piszesz testy, a pewien obiekt wymaga parametru, który jest trudny do utworzenia, rozważ przekazanie po prostu pustej wartości. Jeżeli parametr ten zostanie użyty podczas wykonywania się testu, kod zgłosi wyjątek, który zostanie wychwycony przez jarzmo testowe. Jeśli musisz mieć zachowanie, które istotnie wymaga obiektu, to będziesz mógł go skonstru- ować i przekazać jako parametr. Przekazywanie pustej wartości jest w niektórych językach bardzo wygodną techniką. Dobrze się ona sprawdza w Javie i C# oraz niemal każdym języku, który zgłasza wyjątek, gdy w czasie działania programu zostanie użyta pusta referencja. Z tego wynika, że przekazywanie pustej wartości nie jest dobrym pomysłem w przypadku C i C++, chyba że masz pewność, iż program uruchomieniowy wykryje błędy związane z pustymi wskaźnikami. W przeciwnym razie bę- dziesz mieć do czynienia z testami, które się tajemniczo wysypują — o ile będziesz mieć szczęście. Jeśli zabraknie Ci szczęścia, Twoje testy będą po prostu bezobjawowo i beznadziejnie złe. W czasie działania będą niszczyć pamięć, a Ty nigdy się o tym nie dowiesz. Kiedy pracuję w Javie, często zaczynam od następującego testu, a parametry uzupeł- niam w miarę potrzeb. Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEGO PARAMETRU 127 public void testCreate() { CreditValidator validator = new CreditValidator(null, null, a ); } Najważniejsze do zapamiętania jest to, aby nie przekazywać pustej wartości w kodzie produkcyjnym, chyba że nie masz innego wyboru. Wiem, że niektóre biblioteki tego od Ciebie oczekują, ale kiedy piszesz nowy kod, masz lepszą alternatywę. Jeśli kusi Cię użycie pustej wartości w kodzie produkcyjnym, znajdź miejsca, w których są one zwracane i po- bierane, po czym weź pod uwagę inne rozwiązanie. Zastanów się nad użyciem wzorca pusty obiekt. Wzorzec pusty obiekt Wzorzec pusty obiekt to sposób na uniknięcie użycia pustych wartości w programach. Mamy na przykład metodę, która zwraca dane pracownika po otrzymaniu jego numeru identyfikacyj- nego. Co powinno zostać zwrócone, jeśli nie ma pracownika o podanym numerze? for(Iterator it = idList.iterator(); it.hasNext(); ) { EmployeeID id = (EmployeeID)it.next(); Employee e = finder.getEmployeeForID(id); e.pay(); } Mamy kilka możliwości. Możemy podjąć decyzję o zgłaszaniu wyjątków, dzięki czemu nie będzie trzeba niczego zwracać, ale takie rozwiązanie zmusiłoby klienty do jawnej obsługi błędów. Mo- glibyśmy także zwracać pustą wartość, lecz wówczas klienty musieliby jawnie sprawdzać, czy jej nie otrzymują. Jest jeszcze trzecie rozwiązanie. Czy powyższy kod tak naprawdę sprawdza, czy istnieje pra- cownik, któremu należy zapłacić? Czy musi to robić? A gdybyśmy tak mieli klasę o nazwie NullEmployee? Instancja tej klasy nie ma nazwiska ani adresu, a kiedy polecisz jej dokonanie wypłaty, po prostu nic nie zrobi. W takich kontekstach puste obiekty mogą być przydatne — pomagają one chronić klienty przed jawną obsługą błędów. Chociaż puste obiekty są pomocne, powinieneś zachować ostrożność, gdy z nich korzystasz. Oto przykład niewłaściwego sposobu liczenia pracowników, którym wy- płacono wynagrodzenie: int employeesPaid = 0; for(Iterator it = idList.iterator(); it.hasNext(); ) { EmployeeID id = (EmployeeID)it.next(); Employee e = finder.getEmployeeForID(id); e.pay(); mployeesPaid++; // b(cid:225)(cid:261)d! } Jeśli któryś ze zwróconych pracowników jest pracownikiem pustym, to zliczenie będzie błędne. Puste obiekty są przydatne zwłaszcza wtedy, gdy klient nie musi sprawdzać, czy dana operacja się powiodła. W wielu przypadkach możemy tak dopracować nasz projekt, abyśmy mieli do czy- nienia z takim właśnie rozwiązaniem. Poleć książkęKup książkę 128 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM Przekazanie pustej wartości i wyodrębnienie interfejsu (361) to dwa sposoby na poradzenie sobie z irytującymi parametrami. Czasami można jednak skorzystać z jeszcze innej możliwości. Jeżeli sprawiająca problemy zależność w parametrze nie jest bezpo- średnio zakodowana w swoim konstruktorze, w celu pozbycia się jej możemy skorzystać z techniki tworzenia podklasy i przesłaniania metody (398). W tym przypadku rozwią- zanie takie byłoby możliwe. Jeśli konstruktor klasy RGHConnection używa metody connect w celu nawiązania połączenia, moglibyśmy pozbyć się zależności, przesłaniając wywołanie connect() w testowanej podklasie. Tworzenie podklasy i przesłanianie metody (398) może być bardzo przydatnym sposobem usuwania zależności, ale musimy mieć pewność, że nie zmieniamy zachowania, które chcemy przetestować, gdy z niego korzystamy. Przypadek ukrytej zależności Niektóre klasy bywają podstępne. Patrzymy na nie, znajdujemy konstruktor, którego chcemy użyć, i próbujemy go wywołać. Wtedy trach! Napotykamy przeszkodę. Jedną z najczęściej występujących przeszkód jest ukryta zależność — konstruktor korzysta z za- sobów, do których nie mamy łatwego dostępu w naszym jarzmie testowym. Zobaczymy taką sytuację w następnym przykładzie; źle zaprojektowaną klasę w C++, która obsługuje listę mailingową: class mailing_list_dispatcher { public: mailing_list_dispatcher (); virtual ~mailing_list_dispatcher; void send_message(const std::string message); void add_recipient(const mail_txm_id id, const mail_address address); ... private: mail_service *service; int status; }; Oto fragment konstruktora tej klasy. Alokuje ona obiekt mail_service za pomocą in- strukcji new na liście inicjatora konstruktora. To kiepski styl, ale potem jest jeszcze gorzej. Konstruktor wykonuje sporo szczegółowej pracy z tym obiektem; korzysta też z magicznej liczby 12. Co ma oznaczać to 12? mailing_list_dispatcher::mailing_list_dispatcher() : service(new mail_service), status(MAIL_OKAY) { const int client_type = 12; service- connect(); if (service- get_status() == MS_AVAILABLE) { Poleć książkęKup książkę PRZYPADEK UKRYTEJ ZALEŻNOŚCI 129 service- register(this, client_type, MARK_MESSAGES_OFF); service- set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF); } else status = MAIL_OFFLINE; ... } Podczas testu możemy utworzyć instancję tej klasy, ale chyba nie przyniesie nam ona większych korzyści. Przede wszystkim musimy połączyć się z bibliotekami pocztowymi i skonfigurować system pocztowy, aby obsługiwał rejestrowanie. Jeśli w trakcie testów użyjemy funkcji send_message, to naprawdę wyślemy maile do ludzi. Automatyczne te- stowanie tej funkcjonalności będzie trudne, chyba że skonfigurujemy specjalną skrzynkę pocztową i będziemy się z nią regularnie łączyć, czekając na nadejście wiadomości. Takie rozwiązanie byłoby dobre podczas całościowych testów systemu, ale wszystko, co chcemy teraz zrobić, to tylko dodać do klasy kilka przetestowanych funkcjonalności, więc sposób ten byłby lekką przesadą. Jak moglibyśmy utworzyć prosty obiekt w celu dodania jakiejś nowej funkcjonalności? Podstawowy problem w tym przypadku polega na tym, że zależność od obiektu mail_service jest ukryta w konstruktorze mailing_list_dispatcher. Gdyby istniał jakiś sposób na zastąpienie tego obiektu fałszywką, moglibyśmy dokonać rozpoznania, posługując się fałszywym obiektem i uzyskać informację zwrotną podczas modyfikowania klasy. Jedną z technik, które możemy wykorzystać, jest parametryzacja konstruktora (377). Przy jej pomocy wyciągamy na zewnątrz zależność istniejącą w konstruktorze, przekazując ją do konstruktora. Oto jak wygląda kod konstruktora po sparametryzowaniu konstruktora (377): mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service) : status(MAIL_OKAY) { const int client_type = 12; service- connect(); if (service- get_status() == MS_AVAILABLE) { service- register(this, client_type, MARK_MESSAGES_OFF); service- set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF); } else status = MAIL_OFFLINE; ... } Jedyna różnica tak naprawdę sprowadza się do tego, że obiekt mail_service jest two- rzony poza klasą i do niej przekazywany. Być może nie wygląda to na znaczne usprawnie- nie, ale teraz mamy ogromne możliwości. Możemy skorzystać z wyodrębniania interfejsu (361), aby uzyskać interfejs dla mail_service. Jeden z implementerów tego interfejsu może być klasą produkcyjną, która faktycznie wysyła maile. Inny może być fałszywą klasą, rozpo- znającą to, co robimy podczas testów, oraz informującą nas, że istotnie to się stało. Poleć książkęKup książkę 130 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM Parametryzacja konstruktora (377) jest bardzo wygodnym sposobem na przesu- nięcie zależności z konstruktora na zewnątrz, jednak programiści nie biorą go zbyt często pod uwagę. Jedną z przeszkód stanowi fakt, że wiele osób sądzi, iż w celu przekazania nowego parametru konieczna będzie zmiana wszystkich klientów klasy, co jednak nie jest prawdą. Możemy sobie z tym poradzić następująco. Najpierw wyodrębniamy ciało konstruktora i tworzymy z niego nową metodę o nazwie initialize. W przeciwieństwie do większości innych sposobów wyodrębniania metod możemy to całkiem bezpiecznie zrobić bez testów, gdyż w trakcie tej czynności zachowujemy sygnatury (314). void mailing_list_dispatcher::initialize(mail_service *service) { status = MAIL_OKAY; const int client_type = 12; service.connect(); if (service- get_status() == MS_AVAILABLE) { service- register(this, client_type, MARK_MESSAGES_OFF); service- set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF); } else status = MAIL_OFFLINE; ... } mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service) { initialize(service); } Teraz możemy udostępnić konstruktor, który ma oryginalną sygnaturę. Testy mogą wywoływać konstruktor sparametryzowany przez mail_service, natomiast klienty mogą wywoływać go w poniższy sposób; nie muszą wiedzieć, że coś uległo zmianie. mailing_list_dispatcher::mailing_list_dispatcher() { initialize(new mail_service); } Tego typu refaktoryzacja jest jeszcze łatwiejsza w takich językach jak C# i Java, po- nieważ możemy w nich wywoływać konstruktory z poziomu innych konstruktorów. Gdybyśmy na przykład robili coś podobnego w C#, wynikowy kod mógłby wyglądać następująco: public class MailingListDispatcher { public MailingListDispatcher() : this(new MailService()) {} public MailingListDispatcher(MailService service) { ... } } Poleć książkęKup książkę PRZYPADEK KONSTRUKCYJNEGO KŁĘBOWISKA 131 Z zależnościami ukrytymi w konstruktorach można sobie radzić za pomocą wielu technik. Zwykle możemy zastosować wyodrębnianie i przesłanianie gettera (353), wyod- rębnianie i przesłanianie metody wytwórczej (351) oraz zastępowanie zmiennej instan- cji (401), najbardziej jednak lubię korzystać z parametryzacji konstruktora (377). Kiedy w konstruktorze tworzony jest obiekt i nie ma on żadnych zależności konstrukcyj- nych, łatwą do zastosowania techniką będzie właśnie parametryzacja konstruktora. Przypadek konstrukcyjnego kłębowiska Parametryzacja konstruktora (377) jest jedną z najprostszych technik, z których mo- żemy skorzystać w celu usunięcia zależności ukrytych w konstruktorze. Jest też techniką, po którą często sięgam w pierwszej kolejności. Niestety, nie zawsze jest ona najlepszym wyborem. Jeśli konstruktor tworzy sporą liczbę obiektów lub ma dostęp do wielu zmien- nych globalnych, możemy w rezultacie uzyskać bardzo długą listę parametrów. W gorszych sytuacjach konstruktor tworzy kilka obiektów, po czym używa ich do utworzenia kolej- nych obiektów, tak jak poniżej: class WatercolorPane { public: WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop) { ... anteriorPanel = new Panel(border); anteriorPanel- setBorderColor(brush- getForeColor()); backgroundPanel = new Panel(border, backdrop); cursor = new FocusWidget(brush, backgroundPanel); ... } ... } Gdybyśmy chcieli zrobić rozpoznanie, posługując się obiektem cursor, mielibyśmy problem. Obiekt ten jest osadzony w kłębowisku tworzonych obiektów. Moglibyśmy spró- bować przenieść poza klasę cały kod użyty do tworzenia kursora. W dalszej kolejności klient mógłby utworzyć kursor i przekazać go jako argument. Nie będzie to jednak bez- pieczne, jeśli nie mamy na miejscu testów, poza tym rozwiązanie takie byłoby sporym utrudnieniem dla klientów tej klasy. Jeśli dysponujemy narzędziem refaktoryzującym, które bezpiecznie wyodrębnia metody, możemy skorzystać z techniki wyodrębniania i przesłaniania metody wytwórczej (351) w odniesieniu do kodu konstruktora, co jednak nie sprawdzi się we wszystkich językach. Możemy tak postąpić w Javie i C#, ale już C++ nie pozwala wywoływać funkcji wirtu- alnych w konstruktorach w celu odwołania się do wirtualnych funkcji zdefiniowanych w klasach pochodnych. Poza tym tak w ogóle to nie jest dobry pomysł. Funkcje w klasach pochodnych często zakładają, że mogą korzystać ze zmiennych w ich klasie bazowej. Poleć książkęKup książkę 132 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM Dopóki konstruktor klasy bazowej nie zakończy w pełni swojej pracy, istnieje ryzyko, że przesłonięta funkcja, która go wywołuje, może uzyskać dostęp do niezainicjalizowanej zmiennej. Inną opcją jest zastępowanie zmiennej instancji (401). Piszemy setter dla klasy, który umożliwi nam podstawienie innej instancji po skonstruowaniu obiektu. class WatercolorPane { public: WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop) { ... anteriorPanel = new Panel(border); anteriorPanel- setBorderColor(brush- getForeColor()); backgroundPanel = new Panel(border, backdrop); cursor = new FocusWidget(brush, backgroundPanel); ... } void supersedeCursor(FocusWidget *newCursor) { delete cursor; cursor = newCursor; } } Musimy być bardzo ostrożni, gdy stosujemy tę technikę refaktoryzacji w C++. Kiedy zastępujemy obiekt, powinniśmy pozbyć się jego starej instancji. Często oznacza to, że musimy skorzystać z operatora delete w celu wywołania destruktora i zniszczenia pamięci obiektu. Kiedy to robimy, musimy wiedzieć, co robi destruktor i czy niszczy on coś, co zo- stało przekazane konstruktorowi obiektu. Jeśli nie będziemy ostrożni podczas czyszczenia pamięci, możemy spowodować pewne subtelne błędy. W większości innych języków zastępowanie zmiennej instancji (401) jest dość proste w użyciu. Oto wynik zastosowania tej techniki zapisany w Javie. Nie musimy robić nic szczególnego, aby pozbyć się obiektu, do którego odwołuje się cursor — proces odśmie- cania pamięci i tak w końcu się go pozbędzie. Powinniśmy jednak być szczególnie uważni, aby nie korzystać z tej metody w kodzie produkcyjnym. Jeżeli obiekty, które zastępujemy, zarządzają innymi zasobami, możemy spowodować całkiem poważne problemy związane z dostępem do zasobów. void supersedeCursor(FocusWidget newCursor) { cursor = newCursor; } Teraz, gdy mamy już zastępczą metodę, możemy podjąć się próby utworzenia instancji FocusWidget poza klasą i przekazać go do obiektu po jego skonstruowaniu. Ponieważ potrzebujemy przeprowadzić rozpoznanie, w odniesieniu do klasy FocusWidget możemy skorzystać z techniki wyodrębniania interfejsu (361) albo wyodrębniania implementera Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 133 (356) i utworzyć fałszywy obiekt do przekazania. Z pewnością będzie to łatwiejsze niż two- rzenie obiektu FocusWidget w konstruktorze. TEST(renderBorder, WatercolorPane) { ... TestingFocusWidget *widget = new TestingFocusWidget; WatercolorPane pane(form, border, backdrop); pane.supersedeCursor(widget); LONGS_EQUAL(0, pane.getComponentCount()); } Nie lubię korzystać z techniki zastępowania zmiennej instancji (401). Używam jej, kiedy nie mam już innego wyjścia. Prawdopodobieństwo pojawienia się problemów z za- rządzaniem zasobami jest zbyt duże. Mimo to korzystam z niej od czasu do czasu w C++. Często wolałbym zastosować wyodrębnianie i przesłanianie metody wytwórczej (351), co jednak jest niemożliwe w przypadku konstruktorów w języku C++. Z tego też powodu rzadko uciekam się do zastępowania zmiennej instancji (401). Przypadek irytującej zależności globalnej Od lat ludzie w branży programistycznej narzekają, że nie ma na rynku większej liczby komponentów wielokrotnego użytku. Wraz z upływem czasu sytuacja polepszyła się — istnieje wiele komercyjnych oraz otwartych platform, ale w zasadzie z wielu z nich tak naprawdę nie korzystamy; to raczej one używają naszego kodu. Platformy te często zarzą- dzają cyklem życia aplikacji, a my piszemy kod wypełniający puste miejsca. Możemy to zaobserwować we wszystkich rodzajach platform, od ASP.NET aż do Java Struts. W taki sposób działa nawet xUnit — piszemy klasy testowe, a on je wywołuje i wyświetla wyniki ich działania. Platformy rozwiązują wiele problemów i nadają nam impet, gdy rozpoczynamy nowe projekty, ale to nie takiego rodzaju wielokrotnego wykorzystania oczekiwano we wcze- snych latach rozwoju oprogramowania. Wielokrotne użycie w starym stylu ma miejsce wtedy, gdy znajdujemy jakąś klasę lub zbiór klas, których chcemy użyć w naszej aplikacji, i po prostu to robimy. Dodajemy je do naszego projektu i z nich korzystamy. Byłoby miło, gdybyśmy mogli tak robić rutynowo, ale szczerze mówiąc — myślę, że sami siebie oszuku- jemy nawet wtedy, gdy tylko myślimy o takim rodzaju wielokrotnego użycia, skoro nie potrafimy wyciągnąć z pierwszej lepszej aplikacji dowolnej klasy i oddzielnie jej skompi- lować w jarzmie testowym bez konieczności wykonania całej masy pracy (ale zrzędzę). Wiele różnych rodzajów zależności może utrudniać tworzenie i używanie klas na platformach testowych, a jedną z najgorszych, z jakimi możemy mieć do czynienia, jest użycie zmiennych globalnych. W prostszych przypadkach w celu ominięcia takich zależ- ności możemy posłużyć się parametryzacją konstruktora (377), parametryzacją metody Poleć książkęKup książkę 134 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM (381) oraz wyodrębnianiem i przesłanianiem wywołania (349), ale czasami zależności globalne są tak wszechobecne, że prościej będzie rozprawić się z nimi u źródła. Z taką sy- tuacją spotkamy się w następnym przykładzie, którym jest aplikacja w Javie, rejestrująca pozwolenia na budowę w pewnej agencji rządowej. Oto jedna z jej głównych klas: public class Facility { private Permit basePermit; public Facility(int facilityCode, String owner, PermitNotice notice) throws PermitViolation { Permit associatedPermit = PermitRepository.getInstance().findAssociatedPermit(notice); if (associatedPermit.isValid() !notice.isValid()) { basePermit = associatedPermit; } else if (!notice.isValid()) { Permit permit = new Permit(notice); permit.validate(); basePermit = permit; } else throw new PermitViolation(permit); } ... } Chcielibyśmy utworzyć klasę Facility w jarzmie testowym, w związku z czym za- czynamy od próby utworzenia jej obiektu: public void testCreate() { PermitNotice notice = new PermitNotice(0, a ); Facility facility = new Facility(Facility.RESIDENCE, b , notice); } Test kompiluje się prawidłowo, ale kiedy zaczynamy pisać kolejne testy, zauważamy pewien problem. Konstruktor korzysta z klasy o nazwie PermitRepository i aby nasze testy zostały poprawnie skonfigurowane, musi zostać zainicjalizowany określonym zestawem pozwoleń. Przebiegłe, co? Oto ta problematyczna instrukcja w konstruktorze: Permit associatedPermit = PermitRepository.getInstance().findAssociatedPermit(notice); Moglibyśmy ominąć tę przeszkodę, parametryzując konstruktor, ale w tej aplikacji nie jest to odosobniony przypadek. Istnieje jeszcze 10 innych klas, które zawierają mniej więcej taki sam wiersz kodu. Tkwi on w konstruktorach, metodach zwykłych i statycznych. Możemy tylko wyobrazić sobie, ile czasu byśmy poświęcili, walcząc z tym problemem w bazie kodu. Jeżeli uczyłeś się kiedyś o wzorcach projektowych, być może rozpoznałeś w tym przy- kładzie wzorzec projektowy singleton (370). Metoda getInstance klasy PermitRepository Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 135 jest metodą statyczną, której zadaniem jest zwrócenie jedynej instancji klasy Permit (cid:180)Repository, która może istnieć w naszej aplikacji. Pole przechowujące tę instancję także jest statyczne i znajduje się w tej klasie. W Javie wzorzec singleton jest jednym z mechanizmów używanych do tworzenia zmiennych globalnych. Zazwyczaj wykorzystywanie zmiennych globalnych jest złym pomysłem z kilku powodów. Jednym z nich jest nieprzejrzystość. Kiedy oglądamy frag- ment kodu, dobrze byłoby wiedzieć, na co może on wpływać. Na przykład w Javie — jeśli chcemy zrozumieć, jaki wpływ wywiera na różne elementy poniższy kod — musimy zajrzeć do kilku zaledwie miejsc. Account example = new Account(); example.deposit(1); int balance = example.getBalance(); Wiemy, że obiekt klasy Account może mieć wpływ na elementy, które przekazujemy do konstruktora Account, ale my niczego nie przekazujemy. Obiekty klasy Account mogą też wpływać na obiekty, które przekazujemy do metody jako parametry, ale w tym przy- padku nie przekazujemy nic, co mogłoby ulec zmianie, a jedynie liczbę całkowitą. W miej- scu tym przypisujemy zwracaną wartość getBalance zmiennej i tak naprawdę jest to jedyny element, który może być zmieniany przez powyższe instrukcje. Kiedy korzystamy ze zmiennych globalnych, sytuacja zostaje postawiona na głowie. Możemy patrzeć na użycie takich klas jak Account i nie mieć pojęcia, czy ma ona dostęp i modyfikuje zmienne zadeklarowane w jakimś innym miejscu programu. Nie trzeba nadmieniać, że z tego powodu programy mogą być trudniejsze do zrozumienia. Trudnym zadaniem w naszej sytuacji jest konieczność określenia, które zmienne globalne zostały użyte w klasie, i nadanie im odpowiednich wartości na potrzeby testu. Do tego musimy to robić przed każdym testem, jeśli konfiguracja testów ma się zmieniać. Zadanie to jest dość żmudne; musiałem je wykonywać w odniesieniu do całej masy syste- mów, aby można było je poddać testom, i wraz z upływem czasu nie stało się ono nawet na jotę bardziej ekscytujące. Powróćmy jednak do naszego przykładu. PermitRepository jest singletonem. Z tego względu jest on szczególnie trudny do sfałszowania. Cała koncepcja kryjąca się za wzorcem singletona polega na uniemożli- wieniu tworzenia więcej niż jednej jego instancji w danej aplikacji. Takie rozwiązanie może sprawdzać się w kodzie produkcyjnym, ale w przypadku testowania każdy test w ze- stawie powinien być w pewnym sensie miniaplikacją — powinien być całkowicie odizolo- wany od pozostałych testów. Aby zatem uruchomić w jarzmie testowym kod zawierający singletony, musimy osłabić własność singletona. Oto jak to zrobimy. Najpierw do klasy singletona dodamy nową metodę statyczną. Pozwoli nam ona za- stąpić statyczną instancję w singletonie. Nazwiemy ją setTestingInstance. public class PermitRepository { private static PermitRepository instance = null; Poleć książkęKup książkę 136 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM private PermitRepository() {} public static void setTestingInstance(PermitRepository newInstance) { instance = newInstance; } public static PermitRepository getInstance() { if (instance == null) { instance = new PermitRepository(); } return instance; } public Permit findAssociatedPermit(PermitNotice notice) { ... } ... } Teraz, gdy mamy już setter, możemy utworzyć testową instancję klasy PermitRepository i nadać jej wartość. W naszej testowej metodzie setUp chcielibyśmy dodać następujący kod: public void setUp() { PermitRepository repository = new PermitRepository(); ... // w tym miejscu dodaj zezwolenia do repozytorium ... PermitRepository.setTestingInstance(repository); } Wprowadzanie statycznego settera (370) nie jest jedynym sposobem na poradzenie sobie z taką sytuacją. Oto inne podejście. Do metody resetForTesting() możemy dodać singleton, który wygląda następująco: public class PermitRepository { ... public void resetForTesting() { instance = null; } ... } Jeśli wywołamy tę metodę z naszej metody testowej setUp (a dobrym pomysłem będzie wywołanie jej także z metody tearDown), będziemy tworzyć świeże singletony w każdym te- ście. Za każdym razem singleton będzie inicjalizował się od nowa. Schemat ten dobrze się sprawdza, kiedy metody publiczne w singletonie pozwalają konfigurować jego stan w do- wolny sposób, jaki tylko będzie potrzebny podczas testowania. Jeśli singleton nie ma takich publicznych metod albo korzysta z zewnętrznych zasobów, które wpływają na jego stan, lepszym wyborem będzie wprowadzenie statycznego settera (370). Dzięki temu będziesz mógł tworzyć podklasy singletona, przesłaniać metody, usuwać zależności i dodawać me- tody publiczne do podklas, aby poprawnie konfigurować ich stan. Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 137 Czy to zadziała? Jeszcze nie. Kiedy programiści korzystają ze wzorca projektowego singleton (370), często ich konstruktor klasy singletona jest prywatny, i mają ku temu dobry powód. Jest to najbardziej przejrzysty sposób zagwarantowania, że nikt spoza tej klasy nie będzie mógł utworzyć kolejnej instancji singletona. W tym momencie pojawia się konflikt między dwoma założeniami naszego projektu. Chcemy mieć pewność, że w systemie istnieje tylko jedna instancja klasy PermitRepository, ale chcemy też dysponować systemem, w którym klasy można testować niezależnie od sie- bie. Czy uda nam się osiągnąć jednocześnie oba te cele? Cofnijmy się na chwilę. Dlaczego chcemy mieć w systemie tylko jedną instancję klasy? Odpowiedź będzie się różnić w zależności od systemu, ale oto kilka najczęściej spotykanych powodów: 1. Modelujemy rzeczywisty świat, a w rzeczywistym świecie istnieje tylko jedna taka rzecz. Właśnie takie są niektóre systemy kontrolujące sprzęt. Programiści tworzą klasę dla każdego urządzenia, które musi być kontrolowane. Wychodzą z założenia, że jeśli istnieje tylko po jednym urządzeniu, każde z nich powinno być singletonem. Podobnie sprawy się mają w przypadku baz danych. W naszej agencji istnieje tylko jeden zbiór pozwoleń, a zatem element zapewniający do nich dostęp powinien być singletonem. 2. Jeśli utworzymy dwie takie rzeczy, możemy znaleźć się w poważnych opałach. Sytuacja taka również często ma miejsce w dziedzinie sterowania urządzeniami. Wyobraź sobie przypadkowe utworzenie dwóch kontrolerów prętów uranowych i umożliwienie dwóm różnym częściom programu sterowanie nimi w tym samym czasie bez wzajemnej wiedzy o sobie. 3. Jeśli ktoś utworzy dwie takie rzeczy, będziemy zużywać zbyt wiele zasobów. To zdarza się często. Zasoby mogą być obiektami fizycznymi, takimi jak miejsce na dysku albo zużycie pamięci, ale mogą być także abstrakcyjne, jak na przykład liczba licencji na oprogramowanie. Takie są powody, dla których wymusza się istnienie pojedynczych instancji, ale nie są to główne powody, dla których singletony są używane. Programiści często tworzą single- tony, ponieważ chcą mieć zmienne globalne. Uważają, że przekazywanie zmiennych do miejsc, w których będą one potrzebne, jest zbyt kłopotliwe. Jeśli mamy singletona z tej drugiej przyczyny, to naprawdę nie ma powodu do zacho- wania jego własności. Nasz konstruktor może mieć zakres chroniony, publiczny albo pakietu, a przy tym nadal będziemy dysponować przyzwoitym, możliwym do testowania systemem. W innym przypadku i tak warto poszukać alternatywy. Jeśli zajdzie taka potrzeba, wprowadzimy inny rodzaj ochrony. Moglibyśmy dodać w naszym systemie kompilującym sprawdzanie we wszystkich plikach źródłowych, czy metoda setTestingInstance nie jest wywoływana przez kod nietestujący. Tak samo możemy postąpić w odniesieniu do kontroli wykonywanej w czasie działania programu. Jeśli metoda setTestingInstance zostanie wywołana podczas działania aplikacji, możemy podnieść alarm albo zawiesić system i poczekać na działanie ze strony użytkownika. Faktem jest, że chociaż wymuszenie Poleć książkęKup książkę 138 ROZDZIAŁ 9. NIE MOGĘ UMIEŚCIĆ TEJ KLASY W JARZMIE TESTOWYM „singletonowości” nie było możliwe w wielu językach przed pojawieniem się zorientowa- nia obiektowego, to jednak programiści zdołali utworzyć wiele bezpiecznych syste- mów. W końcu wszystko sprowadza się do odpowiedzialnego projektu i kodowania. Jeżeli naruszenie własności singletona nie stanowi większego problemu, możemy zdać się na regułę stosowaną przez zespół. Przykładowo każdy w zespole powinien zrozumieć, że w aplikacji mamy jedną instancję bazy danych i że nie powinniśmy mieć kolejnej. Aby osłabić właściwość singletona w klasie PermitRepository, możemy przekształcić konstruktor w publiczny. Takie rozwiązanie będzie nas satysfakcjonować, dopóki publiczne metody tej klasy pozwalają nam robić wszystko, czego potrzebujemy w celu skonfigurowa- nia naszego repozytorium na potrzeby testów. Jeśli na przykład w klasie PermitRepository znajduje się metoda o nazwie addPermit, umożliwiająca dodawanie pozwoleń, które będą nam potrzebne w testach, być może wystarczy po prostu, że umożliwimy sobie tworzenie repozytoriów i użyjemy ich w naszych testach. W innym przypadku możemy nie mieć potrzebnego nam dostępu lub — co gorsza — singleton może robić rzeczy, co do których nie chcielibyśmy, aby się działy w jarzmie testowym, takie jak komunikowanie się w tle z bazą danych. W takich okolicznościach możemy utworzyć podklasę i przesłonić metodę (398), po czym utworzyć klasy pochodne, które ułatwią nam testowanie. Oto przykład z naszego systemu pozwoleń. Oprócz metod i zmiennych, które sprawiają, że PermitRepository jest singletonem, mamy także następującą metodę: public class PermitRepository { ... public Permit findAssociatedPermit(PermitNotice notice) { // otwórz baz(cid:266) danych z pozwoleniami ... // wybierz na podstawie warto(cid:286)ci w powiadomieniu ... // sprawd(cid:296), czy mamy tylko jedno pasuj(cid:261)ce pozwolenie; je(cid:286)li nie, zg(cid:225)o(cid:286) b(cid:225)(cid:261)d ... // zwró(cid:252) pasuj(cid:261)ce pozwolenie ... } } Jeśli chcemy uniknąć komunikacji z bazą danych, możemy utworzyć następującą podklasę klasy PermitRepository: public class TestingPermitRepository extends PermitRepository { private Map permits = new HashMap(); public void addAssociatedPermit(PermitNotice notice, permit) { permits.put(notice, permit); } public Permit findAssociatedPermit(PermitNotice notice) { Poleć książkęKup książkę PRZYPADEK IRYTUJĄCEJ ZALEŻNOŚCI GLOBALNEJ 139 return (Permit)permits.get(notice); } } Kiedy tak zrobimy, będziemy mogli zachować część własności singletona. Ponieważ korzystamy z podklasy klasy PermitRepository, sprawimy, że
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Praca z zastanym kodem. Najlepsze techniki
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: