JavaRush /Blog Java /Random-PL /Przewodnik po ogólnym stylu programowania
pandaFromMinsk
Poziom 39
Минск

Przewodnik po ogólnym stylu programowania

Opublikowano w grupie Random-PL
Ten artykuł jest częścią kursu akademickiego „Zaawansowana Java”. Kurs ten ma pomóc Ci nauczyć się efektywnie korzystać z funkcji Java. Materiał obejmuje tematy „zaawansowane”, takie jak tworzenie obiektów, rywalizacja, serializacja, refleksja itp. Kurs nauczy Cię, jak skutecznie opanować techniki Java. Szczegóły tutaj .
Treść
1. Wprowadzenie 2. Zakres zmiennej 3. Pola klas i zmienne lokalne 4. Argumenty metod i zmienne lokalne 5. Pakowanie i rozpakowywanie 6. Interfejsy 7. Ciągi znaków 8. Konwencje nazewnictwa 9. Biblioteki standardowe 10. Niezmienność 11. Testowanie 12. Dalej. .. 13. Pobierz kod źródłowy
1. Wstęp
W tej części tutoriala będziemy kontynuować dyskusję na temat ogólnych zasad dobrego stylu programowania i responsywnego projektowania w Javie. Niektóre z tych zasad widzieliśmy już w poprzednich rozdziałach przewodnika, ale zostanie podanych wiele praktycznych wskazówek mających na celu doskonalenie umiejętności programisty Java.
2. Zakres zmienny
W części trzeciej („Jak projektować klasy i interfejsy”) omówiliśmy, w jaki sposób można zastosować widoczność i dostępność do elementów klas i interfejsów, biorąc pod uwagę ograniczenia zakresu. Jednakże nie omówiliśmy jeszcze zmiennych lokalnych używanych w implementacjach metod. W języku Java każda zmienna lokalna po zadeklarowaniu ma zasięg. Zmienna ta staje się widoczna od miejsca jej zadeklarowania do momentu zakończenia wykonywania metody (lub bloku kodu). Ogólnie rzecz biorąc, jedyną zasadą, której należy przestrzegać, jest deklarowanie zmiennej lokalnej jak najbliżej miejsca, w którym będzie ona używana. Spójrzmy na typowy przykład: for( final Locale locale: Locale.getAvailableLocales() ) { // блок kodа } try( final InputStream in = new FileInputStream( "file.txt" ) ) { // блока kodа } w obu fragmentach kodu zakres zmiennych jest ograniczony do bloków wykonawczych, w których te zmienne są zadeklarowane. Po zakończeniu bloku zakres się kończy, a zmienna staje się niewidoczna. Wydaje się to jaśniejsze, ale wraz z wydaniem Java 8 i wprowadzeniem lambd wiele dobrze znanych idiomów tego języka wykorzystujących zmienne lokalne staje się przestarzałych. Podam przykład z poprzedniego przykładu z użyciem lambd zamiast pętli: Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> { // блок kodа } ); Jak widać, zmienna lokalna stała się argumentem funkcji, która z kolei jest przekazywana jako argument do metody forEach .
3. Pola klasowe i zmienne lokalne
Każda metoda w Javie należy do określonej klasy (lub, w przypadku Java8, do interfejsu, w którym metoda jest zadeklarowana jako metoda domyślna). Pomiędzy zmiennymi lokalnymi będącymi polami klasy lub metodami zastosowanymi w implementacji istnieje możliwość wystąpienia konfliktu nazw. Kompilator Java wie, jak wybrać właściwą zmienną spośród dostępnych, nawet jeśli więcej niż jeden programista zamierza użyć tej zmiennej. Nowoczesne środowiska IDE języka Java świetnie radzą sobie z informowaniem programisty o zbliżającym się wystąpieniu takich konfliktów za pomocą ostrzeżeń kompilatora i podświetlania zmiennych. Ale nadal lepiej jest myśleć o takich rzeczach podczas pisania kodu. Sugeruję spojrzenie na przykład: public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += value; return value; } } Przykład wygląda dość łatwo, ale jest pułapką. MetodakalkulujValue wprowadza wartość zmiennej lokalnej i operując na niej ukrywa pole klasy o tej samej nazwie . Linia powinna być sumą wartości pola klasy i zmiennej lokalnej, ale zamiast tego wykonywane jest coś innego. Prawidłowa implementacja wyglądałaby tak (przy użyciu słowa kluczowego this): Chociaż ten przykład jest pod pewnymi względami naiwny, pokazuje ważną kwestię, której debugowanie i naprawianie w niektórych przypadkach może zająć wiele godzin. value += value; public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += this.value; return value; } }
4. Argumenty metod i zmienne lokalne
Inną pułapką, w jaką często wpadają niedoświadczeni programiści Java, jest używanie argumentów metod jako zmiennych lokalnych. Java pozwala na ponowne przypisanie wartości do niestałych argumentów (jednak nie ma to wpływu na pierwotną wartość): public String sanitize( String str ) { if( !str.isEmpty() ) { str = str.trim(); } str = str.toLowerCase(); return str; } Powyższy fragment kodu nie jest elegancki, ale dobrze radzi sobie z odkrywaniem problemu: str ma przypisany inny wartość (i jest zasadniczo używana jako zmienna lokalna). We wszystkich przypadkach (bez żadnego wyjątku) można i należy obejść się bez tego przykładu (na przykład deklarując argumenty jako stałe). Na przykład: public String sanitize( final String str ) { String sanitized = str; if( !str.isEmpty() ) { sanitized = str.trim(); } sanitized = sanitized.toLowerCase(); return sanitized; } Stosując się do tej prostej zasady, łatwiej jest prześledzić podany kod i znaleźć źródło problemu, nawet jeśli wprowadza się zmienne lokalne.
5. Pakowanie i rozpakowywanie
Pakowanie i rozpakowywanie to nazwa techniki używanej w Javie do konwertowania typów pierwotnych ( int, long, double itp. ) na odpowiadające im opakowania typu ( Integer, Long, Double itp.). W części 4 samouczka Jak i kiedy używać typów ogólnych widziałeś już to w akcji, gdy mówiłem o zawijaniu typów pierwotnych jako parametrów typu typów ogólnych. Chociaż kompilator Java stara się ukryć takie konwersje, wykonując autoboxing, czasami jest to mniej niż oczekiwano i daje nieoczekiwane rezultaty. Spójrzmy na przykład: public static void calculate( final long value ) { // блок kodа } final Long value = null; calculate( value ); powyższy fragment kodu kompiluje się prawidłowo. Jednakże zgłosi wyjątek NullPointerException w linii // блок , w której następuje konwersja między Long i long . Rada w takim przypadku jest taka, że ​​wskazane jest stosowanie typów pierwotnych (jednak wiemy już, że nie zawsze jest to możliwe).
6. Interfejsy
W trzeciej części samouczka, „Jak projektować klasy i interfejsy”, omówiliśmy interfejsy i programowanie kontraktowe, podkreślając, że jeśli to możliwe, interfejsy powinny być preferowane zamiast konkretnych klas. Celem tej sekcji jest zachęcenie Cię do rozważenia najpierw interfejsów poprzez zademonstrowanie tego na przykładach z życia. Interfejsy nie są powiązane z konkretną implementacją (z wyjątkiem metod domyślnych). Są to jedynie umowy i przykładowo dają dużą swobodę i elastyczność w sposobie realizacji umów. Elastyczność ta staje się jeszcze ważniejsza, gdy wdrożenie obejmuje zewnętrzne systemy lub usługi. Spójrzmy na przykład prostego interfejsu i jego możliwej implementacji: public interface TimezoneService { TimeZone getTimeZone( final double lat, final double lon ) throws IOException; } public class TimezoneServiceImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { final URL url = new URL( String.format( "http://api.geonames.org/timezone?lat=%.2f&lng=%.2f&username=demo", lat, lon ) ); final HttpURLConnection connection = ( HttpURLConnection )url.openConnection(); connection.setRequestMethod( "GET" ); connection.setConnectTimeout( 1000 ); connection.setReadTimeout( 1000 ); connection.connect(); int status = connection.getResponseCode(); if (status == 200) { // Do something here } return TimeZone.getDefault(); } } Powyższy fragment kodu przedstawia typowy wzorzec interfejsu i jego implementację. Ta implementacja korzysta z zewnętrznej usługi HTTP ( http://api.geonames.org/ ) w celu pobrania strefy czasowej określonej lokalizacji. Jednakże, ponieważ umowa zależy od interfejsu, bardzo łatwo jest wprowadzić kolejną implementację interfejsu, wykorzystując np. bazę danych lub nawet zwykły plik płaski. Dzięki nim interfejsy są bardzo pomocne w projektowaniu testowalnego kodu. Na przykład wywoływanie usług zewnętrznych przy każdym teście nie zawsze jest praktyczne, dlatego warto zamiast tego wdrożyć alternatywną, najprostszą implementację (taką jak kod pośredniczący): public class TimezoneServiceTestImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { return TimeZone.getDefault(); } } tej implementacji można używać wszędzie tam, gdzie wymagany jest interfejs TimezoneService , izolując skrypt testowy z zależności od komponentów zewnętrznych. Wiele doskonałych przykładów efektywnego wykorzystania takich interfejsów jest zawartych w standardowej bibliotece Java. Kolekcje, listy, zestawy — te interfejsy mają wiele implementacji, które można płynnie wymieniać i wymieniać, gdy umowy skorzystają. Na przykład: public static< T > void print( final Collection< T > collection ) { for( final T element: collection ) { System.out.println( element ); } } print( new HashSet< Object >( /* ... */ ) ); print( new ArrayList< Integer >( /* ... */ ) ); print( new TreeSet< String >( /* ... */ ) );
7. Struny
Ciągi są jednym z najczęściej używanych typów zarówno w Javie, jak i innych językach programowania. Język Java upraszcza wiele rutynowych manipulacji ciągami znaków, obsługując operacje łączenia i porównywania od razu po wyjęciu z pudełka. Ponadto biblioteka standardowa zawiera wiele klas, dzięki którym operacje na łańcuchach są wydajne. Właśnie to omówimy w tej sekcji. W Javie ciągi znaków są niezmiennymi obiektami reprezentowanymi w kodowaniu UTF-16. Za każdym razem, gdy łączysz ciągi znaków (lub wykonujesz jakąkolwiek operację modyfikującą oryginalny ciąg), tworzona jest nowa instancja klasy String . Z tego powodu operacja łączenia może stać się bardzo nieefektywna, powodując utworzenie wielu pośrednich instancji klasy String (ogólnie tworząc śmieci). Jednak standardowa biblioteka Java zawiera dwie bardzo przydatne klasy, których celem jest ułatwienie manipulowania ciągami znaków. Są to StringBuilder i StringBuffer (jedyna różnica między nimi polega na tym, że StringBuffer jest bezpieczny dla wątków, podczas gdy StringBuilder jest odwrotnie). Przyjrzyjmy się kilku przykładom użycia jednej z tych klas: final StringBuilder sb = new StringBuilder(); for( int i = 1; i <= 10; ++i ) { sb.append( " " ); sb.append( i ); } sb.deleteCharAt( 0 ); sb.insert( 0, "[" ); sb.replace( sb.length() - 3, sb.length(), "]" ); Chociaż użycie StringBuilder/StringBuffer jest zalecanym sposobem manipulowania ciągami, może wyglądać na przesadę w najprostszym scenariuszu łączenia dwóch lub trzech ciągów, tak że normalny operator dodawania ( ( „+”), na przykład: String userId = "user:" + new Random().nextInt( 100 ); Często najlepszą alternatywą uproszczenia łączenia jest użycie formatowania ciągów oraz standardowej biblioteki Java, aby zapewnić statyczną metodę pomocniczą String.format . Obsługuje bogaty zestaw specyfikatorów formatu, w tym liczby, symbole, datę/godzinę itp. (Więcej szczegółów można znaleźć w dokumentacji referencyjnej). Metoda String.format( "%04d", 1 ); -> 0001 String.format( "%.2f", 12.324234d ); -> 12.32 String.format( "%tR", new Date() ); -> 21:11 String.format( "%tF", new Date() ); -> 2014-11-11 String.format( "%d%%", 12 ); -> 12% String.format zapewnia przejrzyste i lekkie podejście do generowania ciągów z różnych typów danych. Warto zauważyć, że nowoczesne środowiska IDE Java potrafią analizować specyfikację formatu na podstawie argumentów przekazanych do metody String.format i ostrzegać programistów w przypadku wykrycia jakichkolwiek niezgodności.
8. Konwencje nazewnictwa
Java jest językiem, który nie zmusza programistów do ścisłego przestrzegania jakiejkolwiek konwencji nazewnictwa, ale społeczność opracowała zestaw prostych zasad, które sprawiają, że kod Java wygląda spójnie zarówno w standardowej bibliotece, jak i we wszystkich innych projektach Java:
  • nazwy pakietów pisane są małymi literami: org.junit, com.fasterxml.jackson, javax.json
  • nazwy klas, wyliczeń, interfejsów, adnotacji pisane są wielką literą: StringBuilder, Runnable, @Override
  • nazwy pól lub metod (z wyjątkiem static final ) podawane są w notacji wielbłądziej: isEmpty, format, addAll
  • Nazwy statycznych pól końcowych lub stałych wyliczeniowych są pisane wielkimi literami i oddzielane podkreśleniami („_”): LOG, MIN_RADIX, INSTANCE.
  • zmienne lokalne lub argumenty metod są wpisywane w notacji wielbłądziej: str, newLength, minimumCapacity
  • nazwy typów parametrów dla typów ogólnych są reprezentowane przez pojedynczą wielką literę: T, U, E
Przestrzegając tych prostych konwencji, napisany kod będzie wyglądał zwięźle i nie do odróżnienia stylem od innej biblioteki lub frameworka, a także będzie sprawiał wrażenie, jakby został napisany przez tę samą osobę (jeden z tych rzadkich przypadków, gdy konwencje faktycznie działają).
9. Biblioteki standardowe
Bez względu na rodzaj projektu Java, nad którym pracujesz, standardowe biblioteki Java są Twoimi najlepszymi przyjaciółmi. Tak, trudno się nie zgodzić, że mają pewne ostre krawędzie i dziwne decyzje projektowe, jednak w 99% przypadków jest to kod wysokiej jakości napisany przez ekspertów. Warto to odkryć. Każde wydanie Java wprowadza wiele nowych funkcji do istniejących bibliotek (z pewnymi możliwymi problemami związanymi ze starymi funkcjami), a także dodaje wiele nowych bibliotek. Java 5 wprowadziła nową bibliotekę współbieżności jako część pakietu java.util.concurrent . W Javie 6 wprowadzono (mniej znaną) obsługę skryptów ( pakiet javax.script ) i API kompilatora Java (jako część pakietu javax.tools ). Java 7 wniosła wiele ulepszeń do java.util.concurrent , wprowadzając nową bibliotekę I/O w pakiecie java.nio.file i obsługę języków dynamicznych w java.lang.invoke . I wreszcie Java 8 dodała długo oczekiwaną datę/godzinę w pakiecie java.time . Java jako platforma ewoluuje i bardzo ważne jest, aby rozwijała się wraz z powyższymi zmianami. Ilekroć rozważasz włączenie do swojego projektu biblioteki lub frameworku strony trzeciej, upewnij się, że wymagana funkcjonalność nie jest już zawarta w standardowych bibliotekach Java (oczywiście istnieje wiele wyspecjalizowanych i wysokowydajnych implementacji algorytmów, które wyprzedzają algorytmy w bibliotekach standardowych, ale w większości przypadków tak naprawdę nie są one potrzebne).
10. Niezmienność
Niezmienność w całym przewodniku i w tej części pozostaje dla przypomnienia: proszę traktować to poważnie. Jeśli zaprojektowana przez Ciebie klasa lub zaimplementowana metoda mogą zapewnić gwarancję niezmienności, w większości przypadków można jej używać wszędzie bez obawy, że zostanie zmodyfikowana w tym samym czasie. Dzięki temu Twoje życie jako programisty (i, miejmy nadzieję, życie członków Twojego zespołu) stanie się łatwiejsze.
11. Testowanie
Praktyka programowania sterowanego testami (TDD) jest niezwykle popularna w społeczności Java i podnosi poprzeczkę w zakresie jakości kodu. Biorąc pod uwagę wszystkie korzyści, jakie zapewnia TDD, smutnym jest to, że dzisiejsza standardowa biblioteka Java nie zawiera żadnego frameworka testowego ani narzędzi pomocniczych. Jednakże testowanie stało się niezbędną częścią współczesnego programowania w języku Java i w tej sekcji przyjrzymy się kilku podstawowym technikom wykorzystującym framework JUnit . Zasadniczo w JUnit każdy test jest zbiorem instrukcji dotyczących oczekiwanego stanu lub zachowania obiektu. Sekret pisania świetnych testów polega na tym, aby były proste i krótkie, testując jedną rzecz na raz. W ramach ćwiczenia napiszmy zestaw testów sprawdzających, czy String.format jest funkcją z sekcji string, która zwraca pożądany wynik. package com.javacodegeeks.advanced.generic; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.Test; public class StringFormatTestCase { @Test public void testNumberFormattingWithLeadingZeros() { final String formatted = String.format( "%04d", 1 ); assertThat( formatted, equalTo( "0001" ) ); } @Test public void testDoubleFormattingWithTwoDecimalPoints() { final String formatted = String.format( "%.2f", 12.324234d ); assertThat( formatted, equalTo( "12.32" ) ); } } Obydwa testy wyglądają bardzo czytelnie, a ich wykonanie jest instancją. Obecnie przeciętny projekt Java zawiera setki przypadków testowych, dzięki czemu programista może szybko uzyskać informację zwrotną na temat regresji lub funkcji w trakcie procesu tworzenia oprogramowania.
12. Dalej
Ta część poradnika kończy szereg dyskusji związanych z praktyką programowania w Javie oraz podręcznikami do tego języka programowania. Następnym razem powrócimy do możliwości języka, eksplorując świat Javy pod kątem wyjątków, ich typów, tego jak i kiedy z nich korzystać.
13. Pobierz kod źródłowy
To była lekcja ogólnych zasad programowania z kursu Advanced Java. Kod źródłowy lekcji można pobrać tutaj .
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION