JavaRush /Blog Java /Random-PL /Zasady pisania kodu: od stworzenia systemu po pracę z obi...

Zasady pisania kodu: od stworzenia systemu po pracę z obiektami

Opublikowano w grupie Random-PL
Dzień dobry wszystkim: dzisiaj chciałbym z wami porozmawiać na temat poprawnego pisania kodu. Kiedy zaczynałem programować, nigdzie nie było jasno napisane, że możesz tak pisać, a jeśli będziesz tak pisać, znajdę cię i…. W rezultacie w mojej głowie pojawiło się wiele pytań: jak poprawnie pisać, jakich zasad należy przestrzegać w tej czy innej części programu itp. Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 1No cóż, nie każdy ma ochotę od razu zagłębiać się w książki typu Clean Code, bo dużo jest w nich napisane, ale na początku niewiele jest jasne. A zanim skończysz czytać, możesz zniechęcić się do programowania. Zatem bazując na powyższym, dzisiaj chcę przedstawić Wam mały poradnik (zestaw małych rekomendacji) dotyczący pisania kodu wyższego poziomu. W tym artykule omówimy podstawowe zasady i koncepcje związane z tworzeniem systemu i pracą z interfejsami, klasami i obiektami. Przeczytanie tego materiału nie zajmie Ci dużo czasu i, mam nadzieję, nie pozwoli Ci się nudzić. Przejdę od góry do dołu, czyli od ogólnej struktury aplikacji do bardziej szczegółowych szczegółów. Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 2

System

Ogólnie pożądane cechy systemu to:
  • minimalna złożoność - należy unikać zbyt skomplikowanych projektów. Najważniejsze jest prostota i przejrzystość (najlepiej = prosto);
  • łatwość utrzymania – tworząc aplikację musisz pamiętać, że będzie wymagała wsparcia (nawet jeśli to nie Ty), więc kod powinien być przejrzysty i oczywisty;
  • słabe połączenie to minimalna liczba połączeń pomiędzy różnymi częściami programu (maksymalne wykorzystanie zasad OOP);
  • reusability – zaprojektowanie systemu z możliwością ponownego wykorzystania jego fragmentów w innych zastosowaniach;
  • przenośność – system musi być łatwo przystosowany do innego środowiska;
  • jednolity styl – projektowanie systemu w jednym stylu w różnych jego fragmentach;
  • rozszerzalność (skalowalność) - ulepszanie systemu bez naruszania jego podstawowej struktury (jeśli dodasz lub zmienisz jakiś fragment, nie powinno to mieć wpływu na resztę).
Praktycznie niemożliwe jest zbudowanie aplikacji, która nie wymagałaby modyfikacji, bez dodawania funkcjonalności. Ciągle będziemy musieli wprowadzać nowe elementy, aby nasz pomysł mógł nadążać za duchem czasu. I tu właśnie pojawia się skalowalność . Skalowalność to w zasadzie rozbudowa aplikacji, dodanie nowych funkcjonalności, praca z większą ilością zasobów (czyli, innymi słowy, z większym obciążeniem). Oznacza to, że musimy przestrzegać pewnych zasad, takich jak ograniczenie sprzężenia systemu poprzez zwiększenie modułowości, aby łatwiej było dodać nową logikę.

Etapy projektowania systemu

  1. System oprogramowania - projektowanie aplikacji w formie ogólnej.
  2. Podział na podsystemy/pakiety - zdefiniowanie logicznie oddzielnych części i określenie zasad współdziałania pomiędzy nimi.
  3. Podział podsystemów na klasy – podział części systemu na określone klasy i interfejsy, a także zdefiniowanie interakcji pomiędzy nimi.
  4. Podział klas na metody to pełna definicja niezbędnych metod dla klasy, oparta na zadaniu tej klasy. Projektowanie metod - szczegółowe określenie funkcjonalności poszczególnych metod.
Zazwyczaj za projekt odpowiadają zwykli programiści, a za elementy opisane powyżej odpowiedzialny jest architekt aplikacji.

Główne zasady i koncepcje projektowania systemów

Idiom leniwej inicjalizacji Aplikacja nie traci czasu na tworzenie obiektu, dopóki nie zostanie on użyty, co przyspiesza proces inicjalizacji i zmniejsza obciążenie modułu zbierającego elementy bezużyteczne. Ale nie powinieneś posuwać się z tym za daleko, ponieważ może to prowadzić do naruszenia modułowości. Może warto przenieść wszystkie etapy projektowania do konkretnej części, na przykład głównej, lub do klasy, która działa jak fabryka . Jednym z aspektów dobrego kodu jest brak często powtarzanego, szablonowego kodu. Z reguły taki kod jest umieszczany w osobnej klasie, aby można go było wywołać w odpowiednim momencie. AOP Osobno chciałbym wspomnieć o programowaniu aspektowym . Jest to programowanie poprzez wprowadzenie logiki typu end-to-end, czyli powtarzający się kod umieszczany jest w klasach – aspektach i wywoływany po spełnieniu określonych warunków. Na przykład podczas uzyskiwania dostępu do metody o określonej nazwie lub uzyskiwania dostępu do zmiennej określonego typu. Czasami pewne aspekty mogą być mylące, ponieważ nie jest od razu jasne, skąd wywoływany jest kod, niemniej jednak jest to bardzo przydatna funkcjonalność. W szczególności podczas buforowania lub logowania: dodajemy tę funkcjonalność bez dodawania dodatkowej logiki do zwykłych klas. Więcej o OAP możesz przeczytać tutaj . 4 zasady projektowania prostej architektury według Kenta Becka
  1. Ekspresyjność – potrzebę jasno wyrażonego celu zajęć, osiąga się poprzez prawidłowe nazewnictwo, niewielką wielkość i trzymanie się zasady pojedynczej odpowiedzialności (szerzej przyjrzymy się temu poniżej).
  2. Minimum klas i metod - chcąc podzielić klasy na możliwie małe i jednokierunkowe, można posunąć się za daleko (antywzorzec - shotgunning). Zasada ta wymaga, aby system był kompaktowy i nie posuwał się za daleko, tworząc klasę na każde kichnięcie.
  3. Brak powielania - dodatkowy kod, który myli jest oznaką złego projektu systemu i jest przenoszony w osobne miejsce.
  4. Wykonanie wszystkich testów - kontrolowany jest system, który przeszedł wszystkie testy, gdyż każda zmiana może skutkować niepowodzeniem testów, co może nam pokazać, że zmiana w wewnętrznej logice metody doprowadziła również do zmiany oczekiwanego zachowania .
SOLID Projektując system warto uwzględnić dobrze znane zasady SOLID: S – pojedyncza odpowiedzialność – zasada pojedynczej odpowiedzialności; O - otwarte-zamknięte - zasada otwartości/bliskości; L - podstawienie Liskowa - zasada podstawienia Barbary Liskov; I - segregacja interfejsów - zasada separacji interfejsów; D - inwersja zależności - zasada inwersji zależności; Nie będziemy rozwodzić się nad każdą zasadą z osobna (wykracza to nieco poza zakres tego artykułu, ale więcej możesz dowiedzieć się tutaj )

Interfejs

Być może jednym z najważniejszych etapów tworzenia odpowiedniej klasy jest stworzenie odpowiedniego interfejsu, który będzie reprezentował dobrą abstrakcję ukrywającą szczegóły implementacyjne klasy, a jednocześnie będzie reprezentował grupę wyraźnie spójnych ze sobą metod . Przyjrzyjmy się bliżej jednej z zasad SOLID-a - segregacji interfejsów : klienci (klasy) nie powinni implementować zbędnych metod, których nie będą używać. Czyli jeśli mówimy o budowaniu interfejsów z minimalną liczbą metod, które mają na celu wykonanie jedynego zadania tego interfejsu (jak dla mnie jest to bardzo podobne do pojedynczej odpowiedzialności ), to lepiej stworzyć kilka mniejszych zamiast jednego nadętego interfejsu. Na szczęście klasa może implementować więcej niż jeden interfejs, tak jak ma to miejsce w przypadku dziedziczenia. Trzeba także pamiętać o prawidłowym nazewnictwie interfejsów: nazwa powinna możliwie najdokładniej odzwierciedlać swoje zadanie. I oczywiście im krótszy, tym mniej zamieszania spowoduje. To właśnie na poziomie interfejsu najczęściej pisane są komentarze do dokumentacji , które z kolei pomagają nam szczegółowo opisać, co dana metoda powinna robić, jakie przyjmuje argumenty i co zwróci.

Klasa

Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 3Przyjrzyjmy się wewnętrznej organizacji zajęć. A raczej pewne poglądy i zasady, którymi należy się kierować przy konstruowaniu klas. Zazwyczaj klasa powinna zaczynać się od listy zmiennych ułożonych w określonej kolejności:
  1. publiczne stałe statyczne;
  2. prywatne stałe statyczne;
  3. zmienne instancji prywatnej.
Następnie znajdują się różne konstruktory w kolejności od mniejszej do większej liczby argumentów. Po nich następują metody od bardziej otwartego dostępu po te najbardziej zamknięte: z reguły na samym dole znajdują się metody prywatne, które ukrywają realizację jakiejś funkcjonalności, którą chcemy ograniczyć.

Rozmiar klasy

Teraz chciałbym porozmawiać o liczebności klas. Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 4Pamiętajmy o jednej z zasad SOLID – pojedyncza odpowiedzialność . Pojedyncza odpowiedzialność – zasada pojedynczej odpowiedzialności. Stwierdza, że ​​każdy obiekt ma tylko jeden cel (odpowiedzialność), a logika wszystkich jego metod ma na celu jego zapewnienie. Czyli na tej podstawie powinniśmy unikać dużych, rozdętych klas (które ze swej natury są antywzorcem – „boskim obiektem”), a jeśli mamy w klasie dużo metod o zróżnicowanej, heterogenicznej logice, to musimy pomyśleć o podziale go na kilka logicznych części (klas). To z kolei poprawi czytelność kodu, gdyż nie potrzebujemy dużo czasu na zrozumienie celu metody, jeśli znamy przybliżony cel danej klasy. Musisz także zwrócić uwagę na nazwę klasy : powinna ona odzwierciedlać zawartą w niej logikę. Powiedzmy, że jeśli mamy klasę, której nazwa zawiera ponad 20 słów, musimy pomyśleć o refaktoryzacji. Każda szanująca się klasa nie powinna mieć tak dużej liczby zmiennych wewnętrznych. Tak naprawdę każda metoda współpracuje z jedną lub kilkoma metodami, co powoduje większe sprzężenie w obrębie klasy (i tak właśnie powinno być, gdyż klasa powinna stanowić jedną całość). W rezultacie zwiększenie spójności klasy prowadzi do zmniejszenia jej jako takiej i oczywiście zwiększa się liczba naszych klas. Dla niektórych jest to denerwujące; muszą częściej chodzić na zajęcia, aby zobaczyć, jak działa określone, duże zadanie. Między innymi każda klasa to mały moduł, który powinien być minimalnie powiązany z pozostałymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić podczas dodawania dodatkowej logiki do klasy.

Obiekty

Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 5

Kapsułkowanie

Tutaj porozmawiamy przede wszystkim o jednej z zasad enkapsulacji OOP . Ukrywanie implementacji nie sprowadza się zatem do tworzenia warstwy metod pomiędzy zmiennymi (bezmyślne ograniczanie dostępu poprzez pojedyncze metody, metody pobierające i ustawiające, co nie jest dobre, ponieważ traci się cały sens enkapsulacji). Ukrywanie dostępu ma na celu tworzenie abstrakcji, czyli klasa udostępnia wspólne, konkretne metody, dzięki którym pracujemy z naszymi danymi. Ale użytkownik nie musi dokładnie wiedzieć, jak pracujemy z tymi danymi – to działa i to jest w porządku.

Prawo Demeter

Możesz także rozważyć Prawo Demetera: jest to niewielki zestaw reguł, który pomaga zarządzać złożonością na poziomie klasy i metody. Załóżmy więc, że mamy obiekt Cari ma on metodę - move(Object arg1, Object arg2). Zgodnie z prawem Demeter metoda ta ogranicza się do wywołania:
  • metody samego obiektu Car(innymi słowy to);
  • metody obiektów tworzonych w move;
  • metody przekazywanych obiektów jako argumenty - arg1, arg2;
  • metody obiektów wewnętrznych Car(to samo).
Innymi słowy, prawo Demeter to coś w rodzaju dziecięcej zasady – z przyjaciółmi można rozmawiać, ale nie z nieznajomymi .

Struktura danych

Struktura danych to zbiór powiązanych ze sobą elementów. Rozpatrując obiekt jako strukturę danych, jest to zbiór elementów danych przetwarzanych za pomocą metod, których istnienie jest domniemane w sposób dorozumiany. Oznacza to, że jest to obiekt, którego celem jest przechowywanie i obsługa (przetwarzanie) przechowywanych danych. Kluczowa różnica w stosunku do zwykłego obiektu polega na tym, że obiekt jest zestawem metod operujących na elementach danych, których istnienie jest sugerowane. Czy rozumiesz? W zwykłym obiekcie najważniejsze są metody, a zmienne wewnętrzne mają na celu ich prawidłowe działanie, natomiast w strukturze danych jest odwrotnie: metody wspierają i pomagają w pracy z przechowywanymi elementami, a to one są tutaj najważniejsze. Jednym z typów struktur danych jest obiekt transferu danych (DTO) . Jest to klasa ze zmiennymi publicznymi i bez metod (lub tylko metody do odczytu/zapisu), które przekazują dane podczas pracy z bazami danych, pracują z parsowaniem komunikatów z gniazd itp. Zazwyczaj dane w takich obiektach nie są przechowywane przez długi czas i są niemal natychmiast przekształcony w podmiot, z którym współpracuje nasza aplikacja. Podmiot z kolei to także struktura danych, jednak jej celem jest uczestnictwo w logice biznesowej na różnych poziomach aplikacji, natomiast DTO ma za zadanie transportować dane do/z aplikacji. Przykład DTO:
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Wszystko wydaje się jasne, ale tutaj dowiadujemy się o istnieniu hybryd. Hybrydy to obiekty zawierające metody do obsługi ważnych logiki i przechowywania elementów wewnętrznych oraz metod dostępu do nich (get/set). Takie obiekty powodują bałagan i utrudniają dodawanie nowych metod. Nie powinieneś ich używać, ponieważ nie jest jasne, do czego są przeznaczone - do przechowywania elementów lub wykonywania jakiejś logiki. O możliwych typach obiektów możesz przeczytać tutaj .

Zasady tworzenia zmiennych

Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 6Pomyślmy trochę o zmiennych, a raczej o tym, jakie mogłyby być zasady ich tworzenia:
  1. Idealnie byłoby zadeklarować i zainicjować zmienną bezpośrednio przed jej użyciem (zamiast ją tworzyć i zapominać o niej).
  2. Jeśli to możliwe, deklaruj zmienne jako ostateczne, aby zapobiec zmianie ich wartości po inicjalizacji.
  3. Nie zapomnij o zmiennych licznikowych (zwykle używamy ich w jakiejś pętli for, czyli nie możemy zapomnieć o ich zresetowaniu, w przeciwnym razie może to zepsuć całą naszą logikę).
  4. Powinieneś spróbować zainicjować zmienne w konstruktorze.
  5. Jeśli istnieje wybór między użyciem obiektu z referencją lub bez ( new SomeObject()), wybierz bez ( ), ponieważ ten obiekt, gdy zostanie użyty, zostanie usunięty podczas następnego wyrzucania elementów bezużytecznych i nie będzie marnował zasobów.
  6. Staraj się, aby czas życia zmiennych był jak najkrótszy (odległość pomiędzy utworzeniem zmiennej a ostatnim dostępem).
  7. Inicjuj zmienne używane w pętli bezpośrednio przed pętlą, a nie na początku metody zawierającej pętlę.
  8. Zawsze zaczynaj od najbardziej ograniczonego zakresu i rozszerzaj go tylko w razie potrzeby (powinieneś starać się, aby zmienna była jak najbardziej lokalna).
  9. Używaj każdej zmiennej tylko do jednego celu.
  10. Unikaj zmiennych o ukrytym znaczeniu (zmienna jest rozdarta pomiędzy dwoma zadaniami, co oznacza, że ​​jej typ nie nadaje się do rozwiązania jednego z nich).
Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 7

Metody

Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 8Przejdźmy bezpośrednio do realizacji naszej logiki, czyli do metod.
  1. Pierwszą zasadą jest zwartość. Idealnie, jedna metoda nie powinna przekraczać 20 linii, więc jeśli, powiedzmy, metoda publiczna znacznie „puchnie”, trzeba pomyśleć o przeniesieniu wydzielonej logiki do metod prywatnych.

  2. Druga zasada jest taka, że ​​bloki w poleceniach , ifitd . nie powinny być silnie zagnieżdżone:elsewhile znacznie zmniejsza to czytelność kodu. W idealnym przypadku zagnieżdżenie nie powinno przekraczać dwóch bloków {}.

    Wskazane jest również, aby kod w tych blokach był zwarty i prosty.

  3. Trzecia zasada mówi, że metoda musi wykonywać tylko jedną operację. Oznacza to, że jeśli metoda realizuje złożoną, zróżnicowaną logikę, dzielimy ją na podmetody. W rezultacie sama metoda będzie fasadą, której celem jest wywołanie wszystkich pozostałych operacji w odpowiedniej kolejności.

    Ale co, jeśli operacja wydaje się zbyt prosta, aby utworzyć osobną metodę? Tak, czasami może się to wydawać jak strzelanie do wróbli z armaty, ale małe metody zapewniają szereg korzyści:

    • łatwiejsze czytanie kodu;
    • metody stają się coraz bardziej złożone w trakcie rozwoju i jeśli metoda była początkowo prosta, komplikowanie jej funkcjonalności będzie nieco łatwiejsze;
    • ukrywanie szczegółów implementacji;
    • ułatwianie ponownego wykorzystania kodu;
    • większa niezawodność kodu.
  4. Zasada skierowana w dół jest taka, że ​​kod należy czytać od góry do dołu: im niżej, tym większa głębia logiki i odwrotnie, im wyżej, tym bardziej abstrakcyjne są metody. Na przykład polecenia przełączania są dość niekompaktowe i niepożądane, ale jeśli nie możesz obejść się bez użycia przełącznika, powinieneś spróbować przenieść go jak najniżej, do metod najniższego poziomu.

  5. Argumenty metod - ile jest idealnych? Idealnie byłoby, gdyby ich w ogóle nie było)) Ale czy tak się naprawdę dzieje? Należy jednak starać się mieć ich jak najmniej, bo im mniej, tym łatwiej zastosować tę metodę i tym łatwiej ją przetestować. W razie wątpliwości spróbuj odgadnąć wszystkie scenariusze użycia metody z dużą liczbą argumentów wejściowych.

  6. Osobno chciałbym wyróżnić metody, które mają flagę boolowską jako argument wejściowy , ponieważ naturalnie oznacza to, że metoda ta implementuje więcej niż jedną operację (jeśli prawda, to jedną, fałsz - drugą). Jak pisałem powyżej, nie jest to dobre i należy tego unikać, jeśli to możliwe.

  7. Jeśli metoda ma dużą liczbę przychodzących argumentów (ekstremalna wartość to 7, ale warto pomyśleć o tym po 2-3), należy zgrupować niektóre argumenty w osobnym obiekcie.

  8. Jeśli istnieje kilka podobnych metod (przeciążonych) , to podobne parametry należy przekazać w tej samej kolejności: zwiększa to czytelność i użyteczność.

  9. Kiedy przekazujesz parametry do metody, musisz mieć pewność, że wszystkie zostaną użyte, w przeciwnym razie po co ten argument? Wytnij to z interfejsu i tyle.

  10. try/catchZe swojej natury nie wygląda to zbyt ładnie, więc dobrym posunięciem byłoby przeniesienie go do pośredniej, osobnej metody (metody obsługi wyjątków):

    public void exceptionHandling(SomeObject obj) {
        try {
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Mówiłem o powtarzaniu kodu powyżej, ale dodam to tutaj: Jeśli mamy kilka metod z powtarzającymi się częściami kodu, musimy przenieść je do osobnej metody, co zwiększy zwartość zarówno metody, jak i samego kodu klasa. I nie zapomnij o prawidłowych nazwach. O szczegółach prawidłowego nazewnictwa klas, interfejsów, metod i zmiennych opowiem w dalszej części artykułu. I to wszystko co mam na dzisiaj. Zasady pisania kodu: od stworzenia systemu do pracy z obiektami - 9Zasady kodeksu: siła prawidłowego nazewnictwa, dobre i złe komentarze
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION