CodeGym /Blog Java /Random-PL /Zasady kodowania: od tworzenia systemu do pracy z obiekta...
John Squirrels
Poziom 41
San Francisco

Zasady kodowania: od tworzenia systemu do pracy z obiektami

Opublikowano w grupie Random-PL
Dzień dobry wszystkim! Dzisiaj chcielibyśmy porozmawiać z Tobą o pisaniu dobrego kodu. Oczywiście nie każdy chce od razu przeżuwać książki takie jak Clean Code, ponieważ zawierają one duże ilości informacji, ale na początku niewiele jest jasnych. A zanim skończysz czytać, możesz zabić wszelkie pragnienie kodowania. Biorąc to wszystko pod uwagę, dzisiaj chcę przedstawić mały przewodnik (mały zestaw zaleceń) dotyczący pisania lepszego kodu. W tym artykule omówimy podstawowe zasady i koncepcje związane z tworzeniem systemu oraz pracą z interfejsami, klasami i obiektami. Przeczytanie tego artykułu nie zajmie dużo czasu i mam nadzieję, że nie znudzi. Przejdę od góry do dołu, czyli od ogólnej struktury aplikacji do jej węższych szczegółów. Zasady kodowania: od stworzenia systemu do pracy z obiektami - 1

Systemy

Poniżej przedstawiono ogólnie pożądane cechy systemu:
  • Minimalna złożoność. Należy unikać zbyt skomplikowanych projektów. Najważniejsza jest prostota i przejrzystość (prościej = lepiej).
  • Łatwość konserwacji. Tworząc aplikację, musisz pamiętać, że będzie ona wymagała konserwacji (nawet jeśli osobiście nie będziesz odpowiedzialny za jej utrzymanie). Oznacza to, że kod musi być jasny i oczywisty.
  • Luźne powiązanie. Oznacza to, że minimalizujemy ilość zależności pomiędzy poszczególnymi częściami programu (maksymalizując naszą zgodność z zasadami OOP).
  • Wielokrotnego użytku. Projektujemy nasz system z możliwością ponownego wykorzystania komponentów w innych aplikacjach.
  • Ruchliwość. Dostosowanie systemu do innego środowiska powinno być łatwe.
  • Jednolity styl. Nasz system projektujemy w jednolitym stylu w różnych jego elementach.
  • Rozszerzalność (skalowalność). Możemy rozbudować system bez naruszania jego podstawowej struktury (dodanie lub zmiana komponentu nie powinna wpływać na wszystkie pozostałe).
Praktycznie niemożliwe jest zbudowanie aplikacji, która nie wymaga modyfikacji lub nowej funkcjonalności. Będziemy stale dodawać nowe części, aby pomóc naszemu dziecku nadążyć za duchem czasu. Tutaj w grę wchodzi skalowalność. Skalowalność to zasadniczo rozszerzanie aplikacji, dodawanie nowych funkcjonalności i praca z większą ilością zasobów (lub innymi słowy z większym obciążeniem). Innymi słowy, aby ułatwić dodawanie nowej logiki, trzymamy się pewnych zasad, takich jak zmniejszenie sprzężenia systemu poprzez zwiększenie modułowości.Zasady kodowania: od stworzenia systemu do pracy z obiektami - 2

Źródło obrazu

Etapy projektowania systemu

  1. System oprogramowania. Zaprojektuj aplikację ogólnie.
  2. Podział na podsystemy/pakiety. Zdefiniuj logicznie odrębne części i zdefiniuj zasady interakcji między nimi.
  3. Podział podsystemów na klasy. Podziel części systemu na określone klasy i interfejsy oraz zdefiniuj interakcje między nimi.
  4. Podział klas na metody. Utwórz pełną definicję niezbędnych metod dla klasy, w oparciu o przypisaną jej odpowiedzialność.
  5. Projekt metody. Stwórz szczegółową definicję funkcjonalności poszczególnych metod.
Zazwyczaj zwykli programiści zajmują się tym projektem, podczas gdy architekt aplikacji zajmuje się punktami opisanymi powyżej.

Ogólne zasady i koncepcje projektowania systemów

Leniwa inicjalizacja. W tym idiomie programistycznym aplikacja nie traci czasu na tworzenie obiektu, dopóki nie zostanie on faktycznie użyty. Przyspiesza to proces inicjalizacji i zmniejsza obciążenie modułu wyrzucania elementów bezużytecznych. To powiedziawszy, nie powinieneś posuwać się za daleko, ponieważ może to naruszyć zasadę modułowości. Być może warto przenieść wszystkie wystąpienia konstrukcji do jakiejś konkretnej części, na przykład do metody głównej lub do klasy fabrycznej . Cechą charakterystyczną dobrego kodu jest brak powtarzalnego, szablonowego kodu. Z reguły taki kod jest umieszczany w osobnej klasie, aby można go było wywołać w razie potrzeby.

AOP

Chciałbym również zwrócić uwagę na programowanie zorientowane aspektowo. Ten paradygmat programowania polega na wprowadzeniu przejrzystej logiki. Oznacza to, że powtarzalny kod jest umieszczany w klasach (aspektach) i jest wywoływany, gdy spełnione są określone warunki. Na przykład podczas wywoływania metody o określonej nazwie lub uzyskiwania dostępu do zmiennej określonego typu. Czasami aspekty mogą być mylące, ponieważ nie jest od razu jasne, skąd kod jest wywoływany, ale nadal jest to bardzo przydatna funkcjonalność. Zwłaszcza podczas buforowania lub logowania. Dodajemy tę funkcjonalność bez dodawania dodatkowej logiki do zwykłych klas. Cztery zasady Kenta Becka dotyczące prostej architektury:
  1. Ekspresyjność — intencja zajęć powinna być jasno wyrażona. Osiąga się to poprzez odpowiednie nazewnictwo, mały rozmiar i przestrzeganie zasady pojedynczej odpowiedzialności (którą rozważymy bardziej szczegółowo poniżej).
  2. Minimalna liczba zajęć i metod — Chcąc, aby zajęcia były jak najmniejsze i wąsko skoncentrowane, możesz posunąć się za daleko (co skutkuje antywzorcem chirurgii ze strzelbą). Ta zasada wymaga utrzymania zwartości systemu i nie posuwania się za daleko, tworząc oddzielną klasę dla każdej możliwej akcji.
  3. Brak duplikatów — zduplikowany kod, który powoduje zamieszanie i wskazuje na nieoptymalny projekt systemu, jest wyodrębniany i przenoszony w inne miejsce.
  4. Przeprowadza wszystkie testy — System, który pomyślnie przejdzie wszystkie testy, jest łatwy w zarządzaniu. Każda zmiana może spowodować niepowodzenie testu, ujawniając nam, że nasza zmiana w wewnętrznej logice metody również zmieniła zachowanie systemu w nieoczekiwany sposób.

SOLIDNY

Projektując system warto kierować się dobrze znanymi zasadami SOLID:

S (pojedyncza odpowiedzialność), O (otwarte-zamknięte), L (podstawienie Liskova), I (segregacja interfejsów), D (odwrócenie zależności).

Nie będziemy rozwodzić się nad każdą indywidualną zasadą. Byłoby to trochę poza zakresem tego artykułu, ale możesz przeczytać więcej tutaj .

Interfejs

Być może jednym z najważniejszych kroków w tworzeniu dobrze zaprojektowanej klasy jest stworzenie dobrze zaprojektowanego interfejsu, który reprezentuje dobrą abstrakcję, ukrywając szczegóły implementacji klasy i jednocześnie prezentując grupę metod, które są ze sobą wyraźnie spójne. Przyjrzyjmy się bliżej jednej z zasad SOLID — segregacji interfejsów: klienci (klasy) nie powinni implementować zbędnych metod, których nie będą używać. Innymi słowy, jeśli mówimy o stworzeniu interfejsu z jak najmniejszą liczbą metod mających na celu wykonanie jedynej pracy interfejsu (co moim zdaniem jest bardzo podobne do zasady pojedynczej odpowiedzialności), lepiej zamiast tego stworzyć kilka mniejszych jednego rozdętego interfejsu. Na szczęście klasa może implementować więcej niż jeden interfejs. Pamiętaj, aby odpowiednio nazwać swoje interfejsy: nazwa powinna jak najdokładniej odzwierciedlać przypisane zadanie. I oczywiście im jest krótszy, tym mniej zamieszania spowoduje. Komentarze do dokumentacji są zwykle pisane na poziomie interfejsu. Te komentarze zawierają szczegółowe informacje o tym, co każda metoda powinna zrobić, jakie argumenty przyjmuje i co zwróci.

Klasa

Zasady kodowania: od stworzenia systemu do pracy z obiektami - 3

Źródło obrazu

Przyjrzyjmy się, jak zajęcia są zorganizowane wewnętrznie. A raczej pewne perspektywy i zasady, którymi należy się kierować przy pisaniu zajęć. Z reguły klasa powinna zaczynać się od listy zmiennych w określonej kolejności:
  1. publiczne stałe statyczne;
  2. prywatne stałe statyczne;
  3. prywatne zmienne instancji.
Następnie pojawiają się różne konstruktory, w kolejności od tych z najmniejszą liczbą argumentów do tych z największą liczbą. Po nich następują metody od najbardziej publicznych do najbardziej prywatnych. Ogólnie rzecz biorąc, prywatne metody, które ukrywają implementację niektórych funkcjonalności, które chcemy ograniczyć, znajdują się na samym dole.

Rozmiar klasy

Teraz chciałbym porozmawiać o wielkości klas. Przypomnijmy jedną z zasad SOLID — zasadę pojedynczej odpowiedzialności. Stwierdza, że ​​każdy obiekt ma tylko jeden cel (odpowiedzialność), a logika wszystkich jego metod ma na celu jego osiągnięcie. Mówi nam to, aby unikać dużych, rozdętych klas (które w rzeczywistości są anty-wzorcem obiektu Boga), a jeśli mamy wiele metod z różnymi rodzajami logiki upchniętymi w klasie, musimy pomyśleć o rozbiciu jej na części kilka części logicznych (klas). To z kolei zwiększy czytelność kodu, ponieważ zrozumienie celu każdej metody nie zajmie dużo czasu, jeśli znamy przybliżony cel danej klasy. Zwróć także uwagę na nazwę klasy, która powinna odzwierciedlać logikę, którą zawiera. Na przykład, jeśli mamy klasę z ponad 20 słowami w nazwie, musimy pomyśleć o refaktoryzacji. Żadna szanująca się klasa nie powinna mieć tak wielu zmiennych wewnętrznych. W rzeczywistości każda metoda działa z jednym lub kilkoma z nich, powodując dużą spójność w klasie (co jest dokładnie takie, jak powinno być, ponieważ klasa powinna być jednolitą całością). W rezultacie zwiększenie spójności klasy prowadzi do zmniejszenia liczebności klasy i oczywiście zwiększa się liczba klas. Jest to irytujące dla niektórych osób, ponieważ trzeba więcej przeglądać pliki klas, aby zobaczyć, jak działa określone duże zadanie. Co więcej, każda klasa to mały moduł, który powinien być minimalnie powiązany z innymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić, dodając dodatkową logikę do klasy. każda metoda działa z jednym lub kilkoma z nich, powodując dużą spójność w obrębie klasy (co jest dokładnie takie, jakie powinno być, ponieważ klasa powinna być jednolitą całością). W rezultacie zwiększenie spójności klasy prowadzi do zmniejszenia liczebności klasy i oczywiście zwiększa się liczba klas. Jest to irytujące dla niektórych osób, ponieważ trzeba więcej przeglądać pliki klas, aby zobaczyć, jak działa określone duże zadanie. Co więcej, każda klasa to mały moduł, który powinien być minimalnie powiązany z innymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić, dodając dodatkową logikę do klasy. każda metoda działa z jednym lub kilkoma z nich, powodując dużą spójność w obrębie klasy (co jest dokładnie takie, jakie powinno być, ponieważ klasa powinna być jednolitą całością). W rezultacie zwiększenie spójności klasy prowadzi do zmniejszenia liczebności klasy i oczywiście zwiększa się liczba klas. Jest to irytujące dla niektórych osób, ponieważ trzeba więcej przeglądać pliki klas, aby zobaczyć, jak działa określone duże zadanie. Co więcej, każda klasa to mały moduł, który powinien być minimalnie powiązany z innymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić, dodając dodatkową logikę do klasy. spójność prowadzi do zmniejszenia liczebności klas i oczywiście liczba klas wzrasta. Jest to irytujące dla niektórych osób, ponieważ trzeba więcej przeglądać pliki klas, aby zobaczyć, jak działa określone duże zadanie. Co więcej, każda klasa to mały moduł, który powinien być minimalnie powiązany z innymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić, dodając dodatkową logikę do klasy. spójność prowadzi do zmniejszenia liczebności klas i oczywiście liczba klas wzrasta. Jest to irytujące dla niektórych osób, ponieważ trzeba więcej przeglądać pliki klas, aby zobaczyć, jak działa określone duże zadanie. Co więcej, każda klasa to mały moduł, który powinien być minimalnie powiązany z innymi. Ta izolacja zmniejsza liczbę zmian, które musimy wprowadzić, dodając dodatkową logikę do klasy.

Obiekty

Kapsułkowanie

Tutaj najpierw porozmawiamy o zasadzie OOP: enkapsulacji. Ukrywanie implementacji nie jest równoznaczne z tworzeniem metody izolowania zmiennych (bezmyślne ograniczanie dostępu przez poszczególne metody, gettery i settery, co nie jest dobre, ponieważ traci się cały sens enkapsulacji). Ukrywanie dostępu ma na celu tworzenie abstrakcji, to znaczy, że klasa udostępnia wspólne konkretne metody, których używamy do pracy z naszymi danymi. A użytkownik nie musi dokładnie wiedzieć, jak pracujemy z tymi danymi — to działa i to wystarczy.

Prawo Demeter

Możemy również rozważyć prawo Demeter: jest to mały zestaw reguł, który pomaga w zarządzaniu złożonością na poziomie klasy i metody. Załóżmy, że mamy obiekt Car i 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, obiektu „ten”);
  • metody obiektów tworzonych w ramach metody move ;
  • metody obiektów przekazywane jako argumenty ( arg1 , arg2 );
  • metody wewnętrznych obiektów Car (ponownie „to”).
Innymi słowy, Prawo Demeter jest czymś w rodzaju tego, co rodzice mogą powiedzieć dziecku: „możesz rozmawiać z przyjaciółmi, ale nie z nieznajomymi”.

Struktura danych

Struktura danych to zbiór powiązanych ze sobą elementów. Rozważając obiekt jako strukturę danych, istnieje zestaw elementów danych, na których działają metody. Istnienie tych metod jest domyślnie zakładane. Oznacza to, że struktura danych to obiekt, którego celem jest przechowywanie i praca z (przetwarzaniem) przechowywanych danych. Kluczową różnicą w stosunku do zwykłego obiektu jest to, że zwykły obiekt jest zbiorem metod, które operują na elementach danych, co do których zakłada się, że istnieją. Czy rozumiesz? Głównym aspektem zwykłego obiektu są metody. Zmienne wewnętrzne ułatwiają ich poprawne działanie. Ale w strukturze danych istnieją metody wspierające pracę z przechowywanymi elementami danych, które są tutaj najważniejsze. Jednym z typów struktury danych jest obiekt przesyłania danych (DTO). Jest to klasa ze zmiennymi publicznymi i bez metod (lub tylko metody odczytu/zapisu), która służy do przesyłania danych podczas pracy z bazami danych, parsowania komunikatów z gniazd itp. Dane zwykle nie są przechowywane w takich obiektach przez dłuższy czas. Jest on niemal natychmiast konwertowany na typ podmiotu, na jaki działa nasza aplikacja. Jednostka z kolei jest również strukturą danych, ale jej celem jest uczestnictwo w logice biznesowej na różnych poziomach aplikacji. Celem DTO jest transport danych do/z aplikacji. Przykład DTO: jest również strukturą danych, ale jej celem jest uczestnictwo w logice biznesowej na różnych poziomach aplikacji. Celem DTO jest transport danych do/z aplikacji. Przykład DTO: jest również strukturą danych, ale jej celem jest uczestnictwo w logice biznesowej na różnych poziomach aplikacji. Celem DTO jest transport danych 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ę dość jasne, ale tutaj dowiadujemy się o istnieniu hybryd. Hybrydy to obiekty, które mają metody obsługujące ważną logikę, przechowują elementy wewnętrzne, a także zawierają metody dostępu (get/set). Takie obiekty są bałaganiarskie i utrudniają dodawanie nowych metod. Należy ich unikać, ponieważ nie jest jasne, do czego służą — przechowywanie elementów czy wykonywanie logiki?

Zasady tworzenia zmiennych

Zastanówmy się trochę nad zmiennymi. Dokładniej, zastanówmy się, jakie zasady obowiązują podczas ich tworzenia:
  1. Idealnie byłoby zadeklarować i zainicjować zmienną tuż przed jej użyciem (nie twórz i zapomnij o niej).
  2. Jeśli to możliwe, deklaruj zmienne jako ostateczne, aby zapobiec zmianie ich wartości po inicjalizacji.
  3. Nie zapomnij o licznikach, których zwykle używamy w jakiejś pętli for . Oznacza to, że nie zapomnij ich wyzerować. W przeciwnym razie cała nasza logika może się załamać.
  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 opcję bez, ponieważ po użyciu obiektu zostanie on usunięty podczas następnego cyklu wyrzucania elementów bezużytecznych, a jego zasoby nie zostaną zmarnowane.
  6. Staraj się, aby czas życia zmiennej (odległość między utworzeniem zmiennej a ostatnim odwołaniem) był jak najkrótszy.
  7. Inicjalizuj zmienne używane w pętli tuż przed pętlą, a nie na początku metody zawierającej pętlę.
  8. Zawsze zaczynaj od najbardziej ograniczonego zakresu i rozszerzaj tylko wtedy, gdy jest to konieczne (powinieneś starać się, aby zmienna była jak najbardziej lokalna).
  9. Używaj każdej zmiennej tylko w jednym celu.
  10. Unikaj zmiennych o ukrytym przeznaczeniu, np. zmiennej podzielonej między dwa zadania — oznacza to, że jej typ nie nadaje się do rozwiązania jednego z nich.

Metody

Zasady kodowania: od stworzenia systemu do pracy z obiektami — 4

z filmu "Gwiezdne wojny: Część III - Zemsta Sithów" (2005)

Przejdźmy od razu do implementacji naszej logiki, czyli do metod.
  1. Zasada nr 1 — Zwięzłość. Idealnie, metoda nie powinna przekraczać 20 linii. Oznacza to, że jeśli metoda publiczna znacznie się „powiększy”, trzeba pomyśleć o rozbiciu logiki i przeniesieniu jej do oddzielnych metod prywatnych.

  2. Zasada nr 2 — instrukcje if , else , while i inne nie powinny mieć mocno zagnieżdżonych bloków: duża liczba zagnieżdżeń znacznie zmniejsza czytelność kodu. Idealnie byłoby mieć nie więcej niż dwa zagnieżdżone bloki {} .

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

  3. Zasada nr 3 — Metoda powinna wykonywać tylko jedną operację. Oznacza to, że jeśli metoda wykonuje wszelkiego rodzaju złożoną logikę, dzielimy ją na podmetody. W rezultacie sama metoda będzie fasadą, której celem jest wywołanie wszystkich innych operacji we właściwej kolejności.

    Ale co, jeśli operacja wydaje się zbyt prosta, aby umieścić ją w oddzielnej metodzie? To prawda, że ​​czasami można odnieść wrażenie, że strzela się z armaty do wróbli, ale małe metody mają wiele zalet:

    • Lepsze zrozumienie kodu;
    • W miarę postępu prac metody stają się coraz bardziej złożone. Jeśli metoda jest na początku prosta, nieco łatwiej będzie skomplikować jej funkcjonalność;
    • Szczegóły implementacji są ukryte;
    • Łatwiejsze ponowne użycie kodu;
    • Bardziej niezawodny kod.

  4. Zasada stepdown — Kod należy czytać od góry do dołu: im niżej czytasz, tym głębiej zagłębiasz się w logikę. I odwrotnie, im wyżej zajdziesz, tym bardziej abstrakcyjne będą metody. Na przykład instrukcje switch są raczej niekompaktowe i niepożądane, ale jeśli nie możesz uniknąć używania przełącznika, powinieneś spróbować przenieść go jak najniżej, do metod najniższego poziomu.

  5. Argumenty metody — Jaka jest liczba idealna? Najlepiej wcale :) Ale czy tak się dzieje naprawdę? To powiedziawszy, powinieneś starać się mieć jak najmniej argumentów, ponieważ im mniej ich jest, tym łatwiej jest użyć metody i tym łatwiej jest ją przetestować. W razie wątpliwości spróbuj przewidzieć wszystkie scenariusze użycia metody z dużą liczbą parametrów wejściowych.

  6. Dodatkowo dobrze byłoby oddzielić metody, które mają flagę logiczną jako parametr wejściowy, ponieważ to samo w sobie oznacza, że ​​metoda wykonuje więcej niż jedną operację (jeśli prawda, wykonaj jedną rzecz; jeśli fałsz, wykonaj inną). Jak napisałem powyżej, nie jest to dobre i należy tego unikać, jeśli to możliwe.

  7. Jeśli metoda ma dużą liczbę parametrów wejściowych (skrajność to 7, ale naprawdę warto zacząć myśleć po 2-3), część argumentów powinna być zgrupowana w osobny obiekt.

  8. Jeśli istnieje kilka podobnych (przeciążonych) metod, wówczas podobne parametry muszą być przekazywane w tej samej kolejności: poprawia to czytelność i użyteczność.

  9. Kiedy przekazujesz parametry do metody, musisz mieć pewność, że wszystkie są używane, w przeciwnym razie po co ich potrzebujesz? Wytnij wszystkie nieużywane parametry z interfejsu i gotowe.

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

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

Mówiłem powyżej o zduplikowanym kodzie, ale powtórzę jeszcze raz: jeśli mamy kilka metod z powtarzającym się kodem, musimy przenieść go do osobnej metody. Dzięki temu zarówno metoda, jak i klasa będą bardziej zwarte. Nie zapomnij o zasadach rządzących nazwami: szczegóły dotyczące prawidłowego nazywania klas, interfejsów, metod i zmiennych zostaną omówione w dalszej części artykułu. Ale to wszystko, co mam dla ciebie dzisiaj.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION