JavaRush /Blog Java /Random-PL /Przerwa kawowa #85. Trzy lekcje Java, których nauczyłem s...

Przerwa kawowa #85. Trzy lekcje Java, których nauczyłem się na własnej skórze. Jak używać zasad SOLID w kodzie

Opublikowano w grupie Random-PL

Trzy lekcje Java, których nauczyłem się na własnej skórze

Źródło: Medium Nauka języka Java jest trudna. Uczyłem się na swoich błędach. Teraz i Ty możesz uczyć się na moich błędach i gorzkich doświadczeniach, których wcale nie musisz mieć. Przerwa kawowa #85.  Trzy lekcje Java, których nauczyłem się na własnej skórze.  Jak używać zasad SOLID w kodzie - 1

1. Lambdy mogą powodować problemy.

Lambdy często przekraczają 4 linie kodu i są większe niż oczekiwano. To obciąża pamięć roboczą. Czy musisz zmienić zmienną z lambda? Nie możesz tego zrobić. Dlaczego? Jeśli lambda może uzyskać dostęp do zmiennych witryny wywołania, mogą pojawić się problemy z wątkami. Dlatego nie można zmienić zmiennych z lambda. Ale szczęśliwa ścieżka w lambdzie działa dobrze. W przypadku awarii środowiska wykonawczego otrzymasz następującą odpowiedź:
at [CLASS].lambda$null$2([CLASS].java:85)
at [CLASS]$$Lambda$64/730559617.accept(Unknown Source)
Trudno jest śledzić ślad stosu lambda. Nazwy są mylące i trudne do wyśledzenia i debugowania. Więcej lambd = więcej śladów stosu. Jaki jest najlepszy sposób debugowania lambd? Użyj wyników pośrednich.
map(elem -> {
 int result = elem.getResult();
 return result;
});
Innym dobrym sposobem jest użycie zaawansowanych technik debugowania IntelliJ. Użyj klawisza TAB, aby wybrać kod, który chcesz debugować i połączyć go z wynikami pośrednimi. „Kiedy zatrzymamy się na linii zawierającej lambdę i naciśniemy klawisz F7 (wejście), wówczas IntelliJ podświetli fragment, który wymaga debugowania. Możemy przełączyć blok na debugowanie za pomocą klawisza Tab, a gdy już podejmiemy taką decyzję, ponownie naciśnij klawisz F7. Jak uzyskać dostęp do zmiennych witryny wywołań z lambda? Możesz uzyskać dostęp tylko do zmiennych końcowych lub faktycznie końcowych. Musisz utworzyć opakowanie wokół zmiennych lokalizacji połączenia. Albo używając AtomicType , albo używając własnego typu. Możesz zmienić utworzone opakowanie zmiennej za pomocą lambda. Jak rozwiązać problemy ze śledzeniem stosu? Użyj nazwanych funkcji. W ten sposób możesz szybko znaleźć odpowiedzialny kod, sprawdzić logikę i rozwiązać problemy. Użyj nazwanych funkcji, aby usunąć tajemniczy ślad stosu. Czy powtarza się ta sama lambda? Umieść go w nazwanej funkcji. Będziesz miał jeden punkt odniesienia. Każda lambda otrzymuje wygenerowaną funkcję, co utrudnia jej śledzenie.
lambda$yourNamedFunction
lambda$0
Funkcje nazwane rozwiązują inny problem. Duże lambdy. Funkcje nazwane dzielą duże lambdy, tworzą mniejsze fragmenty kodu i tworzą funkcje podłączalne.
.map(this::namedFunc1).filter(this::namedFilter1).map(this::namedFunc2)

2. Problemy z listami

Musisz pracować z listami ( Listy ). Potrzebujesz HashMap dla danych. Do ról będziesz potrzebować TreeMap . I tak dalej. Nie ma sposobu, aby uniknąć pracy z kolekcjami. Jak zrobić listę? Jakiej listy potrzebujesz? Czy powinno być niezmienne czy zmienne? Wszystkie te odpowiedzi wpływają na przyszłość Twojego kodu. Wybierz odpowiednią listę z wyprzedzeniem, aby później nie żałować. Arrays::asList tworzy listę „od końca do końca”. Czego nie możesz zrobić z tą listą? Nie możesz zmienić jego rozmiaru. On jest niezmienny. Co możesz tutaj zrobić? Określ elementy, sortowanie lub inne operacje, które nie wpływają na rozmiar. Używaj Arrays::asList ostrożnie, ponieważ jego rozmiar jest niezmienny, ale jego zawartość nie. new ArrayList() tworzy nową listę „modyfikowalną”. Jakie operacje obsługuje utworzona lista? To wszystko i jest to powód, aby zachować ostrożność. Twórz modyfikowalne listy z niezmiennych, używając new ArrayList() . List::of tworzy „niezmienną” kolekcję. Jego rozmiar i zawartość pozostają niezmienione pod pewnymi warunkami. Jeśli zawartość to prymitywne dane, takie jak int , lista jest niezmienna. Spójrz na następujący przykład.
@Test
public void testListOfBuilders() {
  System.out.println("### TESTING listOF with mutable content ###");

  StringBuilder one = new StringBuilder();
  one.append("a");

  StringBuilder two = new StringBuilder();
  two.append("a");

  List<StringBuilder> asList = List.of(one, two);

  asList.get(0).append("123");

  System.out.println(asList.get(0).toString());
}
### TESTOWAListalistOF ze zmienną zawartością ### a123
Musisz utworzyć niezmienne obiekty i wstawić je do List::of . Jednak List::of nie zapewnia żadnej gwarancji niezmienności. List::of zapewnia niezmienność, niezawodność i czytelność. Wiedz, kiedy używać struktur mutowalnych, a kiedy niezmiennych. Lista argumentów, które nie powinny się zmieniać, powinna znajdować się na liście niezmiennej. Lista zmienna może być listą zmienną. Dowiedz się, jakiej kolekcji potrzebujesz, aby utworzyć niezawodny kod.

3. Adnotacje spowalniają

Czy korzystasz z adnotacji? Czy je rozumiesz? Czy wiesz, co oni robią? Jeśli myślisz, że adnotacja Logged jest odpowiednia dla każdej metody, to się mylisz. Użyłem Loged do rejestrowania argumentów metod. Ku mojemu zaskoczeniu, to nie zadziałało.
@Transaction
@Method("GET")
@PathElement("time")
@PathElement("date")
@Autowired
@Secure("ROLE_ADMIN")
public void manage(@Qualifier('time')int time) {
...
}
Co jest nie tak z tym kodem? Jest tu dużo podsumowań konfiguracji. Spotkasz się z tym wiele razy. Konfiguracja jest zmieszana ze zwykłym kodem. Samo w sobie nie jest złe, ale przyciąga wzrok. Adnotacje są potrzebne, aby zredukować kod szablonowy. Nie musisz pisać logiki rejestrowania dla każdego punktu końcowego. Nie ma potrzeby konfigurowania transakcji, użyj @Transactional . Adnotacje redukują wzorzec poprzez wyodrębnienie kodu. Nie ma tutaj wyraźnego zwycięzcy, ponieważ obaj biorą udział w grze. Nadal używam XML i adnotacji. Kiedy znajdziesz powtarzający się wzór, najlepiej przenieść logikę do adnotacji. Na przykład rejestrowanie jest dobrą opcją dodawania adnotacji. Morał: nie nadużywaj adnotacji i nie zapomnij o XML.

Bonus: Możesz mieć problemy z opcją Opcjonalną

Użyjesz orElse z Option . Niepożądane zachowanie występuje, gdy nie zostanie przekazana stała orElse . Warto o tym wiedzieć, aby uniknąć problemów w przyszłości. Spójrzmy na kilka przykładów. Kiedy getValue(x) zwraca wartość, wykonywana jest funkcja getValue(y) . Metoda w orElse jest wykonywana, jeśli getValue(x) zwróci niepustą wartość opcjonalną .
getValue(x).orElse(getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x
Source: y
Użyj orElseGet . Nie wykona kodu dla niepustych Opcjonalnych .
getValue(x).orElseGet(() -> getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x

Wniosek

Nauka Javy jest trudna. Javy nie nauczysz się w 24 godziny. Doskonalij swoje umiejętności. Poświęć czas, ucz się i doskonal się w swojej pracy.

Jak używać zasad SOLID w kodzie

Źródło: Cleanthecode Pisanie niezawodnego kodu wymaga zasad SOLID. W pewnym momencie każdy z nas musiał nauczyć się programować. I bądźmy szczerzy. Byliśmy GŁUPI. A nasz kod był taki sam. Dzięki Bogu mamy SOLIDNE. Przerwa kawowa #85.  Trzy lekcje Java, których nauczyłem się na własnej skórze.  Jak używać zasad SOLID w kodzie — 2

SOLIDNE zasady

Jak więc napisać kod SOLID? To właściwie proste. Wystarczy przestrzegać pięciu zasad:
  • Zasada pojedynczej odpowiedzialności
  • Zasada otwarte-zamknięte
  • Zasada zastępstwa Liskowa
  • Zasada separacji interfejsów
  • Zasada inwersji zależności
Nie martw się! Zasady te są znacznie prostsze, niż się wydaje!

Zasada pojedynczej odpowiedzialności

W swojej książce Robert C. Martin opisuje tę zasadę w następujący sposób: „Klasa powinna mieć tylko jeden powód do zmiany”. Przyjrzyjmy się razem dwóm przykładom.

1. Czego nie robić

Mamy klasę o nazwie User , która pozwala użytkownikowi wykonywać następujące czynności:
  • Zarejestruj konto
  • Zaloguj sie
  • Otrzymuj powiadomienie przy pierwszym logowaniu
Ta klasa ma teraz kilka obowiązków. Jeżeli proces rejestracji ulegnie zmianie, zmieni się również klasa Użytkownika . To samo stanie się w przypadku zmiany procesu logowania lub procesu powiadamiania. Oznacza to, że klasa jest przeciążona. Ma zbyt wiele obowiązków. Najłatwiejszym sposobem rozwiązania tego problemu jest przeniesienie odpowiedzialności na swoje klasy, tak aby klasa User była odpowiedzialna tylko za łączenie klas. Jeśli proces się następnie zmieni, będziesz mieć jedną jasną, oddzielną klasę, którą należy zmienić.

2. Co robić

Wyobraź sobie klasę, która powinna wyświetlać powiadomienie nowemu użytkownikowi, FirstUseNotification . Będzie składać się z trzech funkcji:
  • Sprawdź, czy powiadomienie nie zostało już wyświetlone
  • Pokaż powiadomienie
  • Oznacz powiadomienie jako już pokazane
Czy ta klasa ma wiele powodów do zmiany? NIE. Klasa ta posiada jedną czytelną funkcję - wyświetlanie powiadomienia o nowym użytkowniku. Oznacza to, że klasa ma jeden powód do zmiany. Mianowicie, jeśli ten cel się zmieni. Zatem ta klasa nie narusza zasady pojedynczej odpowiedzialności. Oczywiście jest kilka rzeczy, które mogą się zmienić: sposób oznaczania powiadomień jako przeczytane może się zmienić lub sposób wyświetlania powiadomienia. Ponieważ jednak cel zajęć jest jasny i podstawowy, nie ma w tym nic złego.

Zasada otwarte-zamknięte

Zasada open-closed została ukuta przez Bertranda Meyera: „Obiekty oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację”. Zasada ta jest w rzeczywistości bardzo prosta. Musisz napisać swój kod, aby można było dodawać do niego nowe funkcje bez zmiany kodu źródłowego. Pomaga to zapobiec sytuacji, w której konieczna będzie zmiana klas zależnych od zmodyfikowanej klasy. Zasada ta jest jednak znacznie trudniejsza do wdrożenia. Meyer zasugerował użycie dziedziczenia. Ale to prowadzi do silnego połączenia. Omówimy to w Zasadach separacji interfejsów i Zasadach inwersji zależności. Dlatego Martin zaproponował lepsze podejście: użyj polimorfizmu. Zamiast konwencjonalnego dziedziczenia w tym podejściu wykorzystuje się abstrakcyjne klasy bazowe. W ten sposób specyfikacje dziedziczenia można ponownie wykorzystać, gdy implementacja nie jest wymagana. Interfejs można zapisać raz, a następnie zamknąć w celu wprowadzenia zmian. Nowe funkcje muszą następnie implementować ten interfejs i go rozszerzać.

Zasada zastępstwa Liskowa

Zasada ta została wymyślona przez Barbarę Liskov, zdobywczynię nagrody Turinga za jej wkład w języki programowania i metodologię oprogramowania. W swoim artykule zdefiniowała swoją zasadę w następujący sposób: „Obiekty w programie powinny być zastępowalne przez instancje ich podtypów, bez wpływu na prawidłowe wykonanie programu”. Przyjrzyjmy się tej zasadzie jako programista. Wyobraź sobie, że mamy kwadrat. Może to być prostokąt, co brzmi logicznie, ponieważ kwadrat jest specjalnym kształtem prostokąta. Tutaj na ratunek przychodzi zasada zastąpienia Liskowa. Gdziekolwiek spodziewasz się zobaczyć w kodzie prostokąt, możliwe jest również pojawienie się kwadratu. Teraz wyobraź sobie, że Twój prostokąt ma metody SetWidth i SetHeight . Oznacza to, że kwadrat również potrzebuje tych metod. Niestety, nie ma to żadnego sensu. Oznacza to, że naruszona jest tutaj zasada zastępowania Liskowa.

Zasada separacji interfejsów

Podobnie jak wszystkie zasady, zasada separacji interfejsów jest znacznie prostsza, niż się wydaje: „Wiele interfejsów specyficznych dla klienta jest lepszych niż jeden interfejs ogólnego przeznaczenia”. Podobnie jak w przypadku zasady pojedynczej odpowiedzialności, celem jest ograniczenie skutków ubocznych i liczby wymaganych zmian. Oczywiście nikt nie pisze takiego kodu celowo. Ale łatwo go spotkać. Pamiętasz kwadrat z poprzedniej zasady? Teraz wyobraź sobie, że decydujemy się na realizację naszego planu: z prostokąta produkujemy kwadrat. Teraz zmuszamy kwadrat do wdrożenia setWidth i setHeight , które prawdopodobnie nic nie dają. Gdyby to zrobili, prawdopodobnie coś byśmy zepsuli, ponieważ szerokość i wysokość nie byłyby takie, jakich się spodziewaliśmy. Na szczęście dla nas oznacza to, że nie naruszamy już zasady podstawienia Liskowa, ponieważ teraz pozwalamy na użycie kwadratu wszędzie tam, gdzie używamy prostokąta. Stwarza to jednak nowy problem: naruszamy teraz zasadę separacji interfejsów. Zmuszamy klasę pochodną do zaimplementowania funkcjonalności, której nie chce używać.

Zasada inwersji zależności

Ostatnia zasada jest prosta: moduły wysokiego poziomu powinny nadawać się do ponownego użycia i zmiany w modułach niskiego poziomu nie powinny mieć na nie wpływu.
  • O. Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obydwa muszą zależeć od abstrakcji (takich jak interfejsy).
  • B. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły (konkretne realizacje) muszą zależeć od abstrakcji.
Można to osiągnąć poprzez wdrożenie abstrakcji oddzielającej moduły wysokiego i niskiego poziomu. Nazwa zasady sugeruje, że zmienia się kierunek zależności, ale tak nie jest. Rozdziela jedynie zależności wprowadzając pomiędzy nimi abstrakcję. W rezultacie otrzymasz dwie zależności:
  • Moduł wysokiego poziomu, w zależności od abstrakcji
  • Moduł niskiego poziomu w zależności od tej samej abstrakcji
Może się to wydawać trudne, ale w rzeczywistości dzieje się to automatycznie, jeśli poprawnie zastosujesz zasadę otwartego/zamkniętego i zasadę podstawienia Liskova. To wszystko! Znasz już pięć podstawowych zasad leżących u podstaw SOLID. Dzięki tym pięciu zasadom możesz uczynić swój kod niesamowitym!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION