JavaRush /Blog Java /Random-PL /Pięć podstawowych zasad projektowania klas (SOLID) w Javi...
Ve4niY
Poziom 14

Pięć podstawowych zasad projektowania klas (SOLID) w Javie

Opublikowano w grupie Random-PL
Klasy to bloki, z których zbudowana jest aplikacja. Podobnie jak cegły w budynku. Źle napisane zajęcia mogą pewnego dnia powodować problemy. Pięć podstawowych zasad projektowania klas (SOLID) w Javie - 1Aby zrozumieć, czy klasa jest napisana poprawnie, możesz sprawdzić „standardy jakości”. W Javie są to tak zwane zasady SOLID. Porozmawiajmy o nich.

Zasady SOLID w Javie

SOLID to akronim utworzony z wielkich liter pierwszych pięciu zasad OOP i projektowania. Zasady zostały wymyślone przez Roberta Martina na początku XXI wieku, a akronim został później ukuty przez Michaela Feathersa. Oto, co obejmują zasady SOLID:
  1. Zasada pojedynczej odpowiedzialności.
  2. Zasada otwartego zamkniętego.
  3. Zasada substytucji Liskova.
  4. Zasada segregacji interfejsów.
  5. Zasada inwersji zależności.

Zasada pojedynczej odpowiedzialności (SRP)

Zasada ta stwierdza, że ​​nigdy nie powinien istnieć więcej niż jeden powód do zmiany klasy. Każdy obiekt ma jedną odpowiedzialność, całkowicie zamkniętą w klasie. Wszystkie usługi klasowe mają na celu zapewnienie tej odpowiedzialności. Klasy takie zawsze będzie można łatwo zmienić w razie potrzeby, gdyż wiadomo za co dana klasa odpowiada, a za co nie. Oznacza to, że będzie można wprowadzać zmiany i nie bać się konsekwencji - wpływu na inne obiekty. A taki kod jest dużo łatwiejszy do przetestowania, bo jedną funkcjonalność testuje się w oderwaniu od pozostałych. Wyobraź sobie moduł obsługujący zamówienia. Jeśli zamówienie jest poprawnie utworzone, zapisuje je w bazie danych i wysyła e-mail w celu potwierdzenia zamówienia:
public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // zapis zamówienia do bazy danych

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Wysyłanie listu do klienta
    }
}
Taki moduł może ulec zmianie z trzech powodów. Po pierwsze, inna może być logika realizacji zamówienia, po drugie, sposób jego zapisania (typ bazy danych), po trzecie, sposób wysłania listu potwierdzającego (np. zamiast e-maila należy wysłać SMS). Zasada pojedynczej odpowiedzialności zakłada, że ​​trzy aspekty tego problemu to w rzeczywistości trzy różne obowiązki. Oznacza to, że muszą należeć do różnych klas lub modułów. Łączenie wielu elementów, które mogą zmieniać się w różnym czasie i z różnych powodów, jest uważane za złą decyzję projektową. Znacznie lepiej jest podzielić moduł na trzy osobne, z których każdy będzie pełnił jedną funkcję:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // zapis zamówienia do bazy danych

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Wysyłanie listu do klienta
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

Zasada otwartego/zamkniętego (OCP)

Zasada ta jest zwięźle opisana w następujący sposób: jednostki oprogramowania (klasy, moduły, funkcje itp.) muszą być otwarte na rozbudowę, ale zamknięte na zmiany . Oznacza to, że powinna istnieć możliwość zmiany zewnętrznego zachowania klasy bez dokonywania fizycznych zmian w samej klasie. Kierując się tą zasadą, klasy są rozwijane tak, aby dostosować klasę do konkretnych warunków aplikacji, wystarczy ją rozszerzyć i przedefiniować niektóre funkcje. Dlatego system musi być elastyczny, zdolny do pracy w zmiennych warunkach bez zmiany kodu źródłowego. Kontynuując nasz przykład zamówienia, powiedzmy, że musimy wykonać pewne czynności przed przetworzeniem zamówienia i po wysłaniu wiadomości e-mail z potwierdzeniem. Zamiast zmieniać samą klasę OrderProcessor, rozszerzymy ją i osiągniemy rozwiązanie danego problemu bez naruszania zasady OCP:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }

    private void beforeProcessing() {
        // Wykonaj kilka czynności przed przetworzeniem zamówienia
    }

    private void afterProcessing() {
        // Wykonaj pewne czynności po przetworzeniu zamówienia
    }
}

Zasada substytucji Barbary Liskov (LSP)

Jest to odmiana omówionej wcześniej zasady otwarty/zamknięty. Można to opisać w następujący sposób: obiekty w programie można zastąpić ich spadkobiercami bez zmiany właściwości programu. Oznacza to, że klasa utworzona poprzez rozszerzenie klasy bazowej musi zastąpić swoje metody w sposób, który nie psuje funkcjonalności z punktu widzenia klienta. Oznacza to, że jeśli programista rozszerza twoją klasę i używa jej w aplikacji, nie powinien zmieniać oczekiwanego zachowania przeciążonych metod. Podklasy muszą przesłaniać metody klasy bazowej w sposób, który nie psuje funkcjonalności z punktu widzenia klienta. Można to szczegółowo sprawdzić na poniższym przykładzie. Załóżmy, że mamy klasę odpowiedzialną za walidację zamówienia i sprawdzającą, czy wszystkie pozycje zamówienia są w magazynie. Ta klasa ma metodę isValid, która zwraca wartość true lub false :
public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (! item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
Załóżmy też, że niektóre zamówienia wymagają innej walidacji: sprawdź, czy wszystkie towary w zamówieniu znajdują się w magazynie i czy wszystkie towary są zapakowane. W tym celu rozszerzyliśmy klasę OrderStockValidatoro klasę OrderStockAndPackValidator:
public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
Jednak w tej klasie naruszyliśmy zasadę LSP, ponieważ zamiast zwrócić false w przypadku, gdy zamówienie nie przeszło walidacji, nasza metoda zgłasza wyjątek IllegalStateException. Klienci tego kodu nie oczekują tego: oczekują, że zostanie zwrócona wartość true lub false . Może to prowadzić do błędów w programie.

Zasada podziału interfejsu (ISP)

Charakteryzuje się następującym stwierdzeniem: Klienci nie powinni być zmuszani do wdrażania metod, z których nie będą korzystać . Zasada separacji interfejsów sugeruje, że zbyt „grube” interfejsy należy podzielić na mniejsze i bardziej szczegółowe, tak aby klienci małych interfejsów wiedzieli tylko o metodach niezbędnych do ich pracy. W rezultacie przy zmianie metody interfejsu klienci, którzy nie korzystają z tej metody, nie powinni się zmieniać. Spójrzmy na przykład. Programista Alex stworzył interfejs „raportu” i dodał dwie metody: generateExcel()i generatedPdf(). Teraz Klient A chce korzystać z tego interfejsu, ale zamierza używać tylko raportów w formacie PDF, a nie Excela. Czy będzie zadowolony z tej funkcjonalności? NIE. Będzie musiał wdrożyć dwie metody, z których jedna jest w dużej mierze zbędna i istnieje tylko dzięki Alexowi, projektantowi oprogramowania. Klient albo użyje innego interfejsu, albo pozostawi pole Excela puste. Jakie jest więc rozwiązanie? Polega na podzieleniu istniejącego interfejsu na dwa mniejsze. Jeden to raport w formacie PDF, drugi to raport w formacie Excel. Dzięki temu użytkownik będzie miał możliwość korzystania wyłącznie z niezbędnych mu funkcjonalności.

Zasada inwersji zależności (DIP)

Zasada SOLID w Javie jest opisana następująco: zależności w systemie budowane są w oparciu o abstrakcje . Moduły najwyższego poziomu są niezależne od modułów niższego poziomu. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły muszą zależeć od abstrakcji. Oprogramowanie musi być zaprojektowane tak, aby poszczególne moduły były autonomiczne i łączyły się ze sobą za pomocą abstrakcji. Klasycznym zastosowaniem tej zasady jest framework Spring. W ramach Spring wszystkie moduły są zaimplementowane jako osobne komponenty, które mogą ze sobą współpracować. Są na tyle samodzielne, że można ich używać równie łatwo w innych modułach oprogramowania poza frameworkiem Spring. Osiąga się to poprzez zależność zasad zamkniętych i otwartych. Wszystkie moduły zapewniają dostęp jedynie do abstrakcji, która może zostać wykorzystana w innym module. Spróbujmy to wykazać na przykładzie. Mówiąc o zasadzie wyłącznej odpowiedzialności, rozważaliśmy niektóre OrderProcessor. Przyjrzyjmy się jeszcze raz kodowi tej klasy:
public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
W tym przykładzie nasz OrderProcessorzależy od dwóch konkretnych klas MySQLOrderRepositoryi ConfirmationEmailSender. Przedstawiamy również kod dla tych klas:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // zapis zamówienia do bazy danych

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Wysyłanie listu do klienta
    }
}
Klasy te nie można nazwać abstrakcjami. A z punktu widzenia zasady DIP bardziej słuszne byłoby zacząć od stworzenia jakichś abstrakcji, które pozwolą nam w przyszłości z nimi operować, a nie od konkretnych implementacji. Stwórzmy dwa interfejsy MailSenderi OrderRepository, które staną się naszymi abstrakcjami:
public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Zaimplementujmy teraz te interfejsy w klasach, które są już na to gotowe:
public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Wysyłanie listu do klienta
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // zapis zamówienia do bazy danych

        return true;
    }
}
Wykonaliśmy prace przygotowawcze, aby nasze zajęcia OrderProcessornie opierały się na konkretnych szczegółach, ale na abstrakcjach. Wprowadźmy w nim zmiany wprowadzając nasze zależności w konstruktorze klasy:
public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
Nasza klasa opiera się teraz na abstrakcjach, a nie na konkretnych implementacjach. Możesz łatwo zmienić jego zachowanie, wstrzykując żądaną zależność w momencie tworzenia instancji OrderProcessor. Przyjrzeliśmy się SOLIDowi – zasadom projektowania w Javie. Więcej o OOP w ogóle, podstawy tego języka programowania - nie nudnego i wymagającego setek godzin praktyki - w kursie JavaRush. Czas rozwiązać pewne problemy :) Pięć podstawowych zasad projektowania klas (SOLID) w Javie - 2
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION