CodeGym /Blog Java /Random-PL /SOLID: Pięć podstawowych zasad projektowania klas w Javie...
Autor
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

SOLID: Pięć podstawowych zasad projektowania klas w Javie

Opublikowano w grupie Random-PL
Klasy są budulcem aplikacji. Jak cegły w budynku. Źle napisane zajęcia mogą ostatecznie powodować problemy. SOLID: Pięć podstawowych zasad projektowania klas w Javie - 1Aby zrozumieć, czy zajęcia są poprawnie napisane, możesz sprawdzić, jak odpowiadają „standardom jakości”. W Javie są to tak zwane SOLIDNE zasady i będziemy o nich mówić.

SOLIDNE zasady w Javie

SOLID to akronim utworzony z wielkich liter pięciu pierwszych zasad OOP i projektowania klas. Zasady zostały wyrażone przez Roberta Martina na początku 2000 roku, a następnie skrót został wprowadzony później przez Michaela Feathersa. Oto zasady SOLID:
  1. Zasada pojedynczej odpowiedzialności
  2. Zasada otwartego zamkniętego
  3. Zasada podstawienia Liskowa
  4. Zasada segregacji interfejsów
  5. Zasada odwrócenia zależności

Zasada pojedynczej odpowiedzialności (SRP)

Zasada ta mówi, że nigdy nie powinno być więcej niż jednego powodu do zmiany klasy. Każdy obiekt ma jedną odpowiedzialność, która jest w pełni zawarta w klasie. Wszystkie usługi klasy mają na celu wspieranie tej odpowiedzialności. Takie klasy zawsze będzie można łatwo zmodyfikować w razie potrzeby, ponieważ jasne jest, czym jest klasa i za co nie odpowiada. Innymi słowy, będziemy mogli wprowadzać zmiany i nie bać się konsekwencji, czyli wpływu na inne obiekty. Dodatkowo taki kod jest znacznie łatwiejszy do przetestowania, ponieważ twoje testy obejmują jedną funkcjonalność w oderwaniu od wszystkich innych. Wyobraź sobie moduł przetwarzający zamówienia. Jeśli zamówienie jest poprawnie sformułowane, moduł ten zapisuje je w bazie danych i wysyła e-mail potwierdzający zamówienie:

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");
        // Save the order in the database

        return true;
    }

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

        // Send an email to the customer
    }
}
Ten moduł może ulec zmianie z trzech powodów. Po pierwsze, logika przetwarzania zamówień może ulec zmianie. Po drugie, sposób zapisywania zamówień (typ bazy danych) może ulec zmianie. Po trzecie, sposób wysyłania potwierdzenia może ulec zmianie (na przykład załóżmy, że zamiast e-maila musimy wysłać wiadomość tekstową). Zasada pojedynczej odpowiedzialności oznacza, że ​​trzy aspekty tego problemu to w rzeczywistości trzy różne obowiązki. Oznacza to, że powinny znajdować się w różnych klasach lub modułach. Łączenie kilku elementów, które mogą zmieniać się w różnym czasie iz różnych powodów, jest uważane za złą decyzję projektową. Znacznie lepiej jest podzielić moduł na trzy osobne moduły, z których każdy wykonuje jedną funkcję:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

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

        // Send an email to the customer
    }
}

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 opisana w następujący sposób: jednostki oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozbudowę, ale zamknięte na modyfikację . Oznacza to, że powinna istnieć możliwość zmiany zewnętrznego zachowania klasy bez wprowadzania zmian w istniejącym kodzie klasy. Zgodnie z tą zasadą klasy są projektowane w taki sposób, że dostosowanie klasy do określonych warunków wymaga po prostu jej rozszerzenia i nadpisania niektórych funkcji. Oznacza to, że system musi być elastyczny, zdolny do pracy w zmieniających się warunkach bez zmiany kodu źródłowego. Kontynuując nasz przykład dotyczący przetwarzania zamówienia, załóżmy, że musimy wykonać pewne czynności przed przetworzeniem zamówienia, a także po wysłaniu wiadomości e-mail z potwierdzeniem. Zamiast zmieniać tzwOrderProcessorsama klasa, rozszerzymy ją, aby osiągnąć nasz cel bez naruszania zasady otwartego zamkniętego:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Zasada podstawienia Liskowa (LSP)

Jest to odmiana zasady otwartego zamkniętego, o której wspominaliśmy wcześniej. Można to zdefiniować następująco: obiekty można zastępować obiektami podklas bez zmiany właściwości programu. Oznacza to, że klasa utworzona przez rozszerzenie klasy bazowej musi nadpisać jej metody, aby funkcjonalność nie została naruszona z punktu widzenia klienta. Oznacza to, że jeśli programista rozszerzy twoją klasę i użyje jej w aplikacji, nie powinien zmieniać oczekiwanego zachowania żadnych przesłoniętych metod. Podklasy muszą nadpisywać metody klasy bazowej, aby funkcjonalność nie została uszkodzona z punktu widzenia klienta. Możemy to szczegółowo zbadać w poniższym przykładzie. Załóżmy, że mamy klasę odpowiedzialną za walidację zamówienia i sprawdzenie, czy wszystkie towary w zamówieniu są w magazynie.isValid()metoda zwracająca prawdę lub fałsz :

public class OrderStockValidator {

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

        return true;
    }
}
Załóżmy również, że niektóre zamówienia wymagają walidacji inaczej niż inne, np. dla niektórych zamówień musimy sprawdzić, czy wszystkie towary w zamówieniu są na stanie i czy wszystkie towary są spakowane. W tym celu rozszerzamy OrderStockValidatorklasę, tworząc OrderStockAndPackValidatorklasę:

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;
    }
}
Ale tutaj naruszyliśmy zasadę podstawienia Liskova, ponieważ zamiast zwracać fałsz , jeśli zamówienie nie przejdzie walidacji, nasza metoda zgłasza błąd IllegalStateException. Klienci korzystający z tego kodu nie oczekują tego: oczekują wartości zwracanej albo true , albo false . Może to prowadzić do błędów w czasie wykonywania.

Zasada segregacji interfejsów (ISP)

Ta zasada charakteryzuje się następującym stwierdzeniem: nie należy zmuszać klienta do wdrażania metod, z których nie będzie korzystał . Zasada segregacji interfejsów oznacza, że ​​zbyt „grube” interfejsy należy podzielić na mniejsze, bardziej szczegółowe, tak aby klienci korzystający z małych interfejsów wiedzieli tylko o metodach potrzebnych im do pracy. W rezultacie, gdy zmienia się metoda interfejsu, klienci, którzy nie używają tej metody, nie powinni się zmieniać. Rozważmy ten przykład: Alex, programista, stworzył interfejs „raportu” i dodał dwie metody: generateExcel()igeneratedPdf(). Teraz klient chce korzystać z tego interfejsu, ale zamierza używać tylko raportów w formacie PDF, a nie w Excelu. Czy ta funkcjonalność zadowoli tego klienta? Nie. Klient będzie musiał zaimplementować dwie metody, z których jedna jest w dużej mierze niepotrzebna i istnieje tylko dzięki Alexowi, który zaprojektował oprogramowanie. Klient użyje innego interfejsu lub nie zrobi nic z metodą dla raportów Excel. Więc jakie jest rozwiązanie? Ma to na celu podzielenie istniejącego interfejsu na dwa mniejsze. Jeden do raportów PDF, drugi do raportów Excel. Dzięki temu klienci mogą korzystać tylko z tych funkcji, których potrzebują.

Zasada odwrócenia zależności (DIP)

W Javie ta zasada SOLID jest opisana w następujący sposób: zależności w systemie są budowane w oparciu o abstrakcje. Moduły wyższego poziomu nie zależą od modułów niższego poziomu. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji. Oprogramowanie musi być zaprojektowane w taki sposób, aby różne moduły były niezależne i połączone ze sobą poprzez abstrakcję. Klasycznym zastosowaniem tej zasady jest Spring Framework. W Spring Framework wszystkie moduły są zaimplementowane jako osobne komponenty, które mogą ze sobą współpracować. Są na tyle autonomiczne, że równie łatwo można ich używać w modułach programu innych niż Spring Framework. Osiąga się to dzięki zależności zasady zamkniętej i otwartej. Wszystkie moduły zapewniają dostęp tylko do abstrakcji, którą można wykorzystać w innym module. Spróbujmy to zilustrować na przykładzie. Mówiąc o zasadzie pojedynczej odpowiedzialności, rozważaliśmy tzwOrderProcessorklasa. 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 nasza OrderProcessorklasa zależy od dwóch konkretnych klas: MySQLOrderRepositoryi ConfirmationEmailSender. Przedstawimy również kod tych klas:

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

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

        // Send an email to the customer
    }
}
Klasy te są dalekie od tego, co nazwalibyśmy abstrakcjami. A z punktu widzenia zasady odwracania zależności lepiej byłoby zacząć od stworzenia pewnych abstrakcji, z którymi będziemy mogli pracować w przyszłości, niż konkretnych implementacji. Stwórzmy dwa interfejsy: MailSenderi OrderRepository). To będą nasze abstrakcje:

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
Teraz implementujemy te interfejsy w klasach, które zostały już do tego przygotowane:

public class ConfirmationEmailSender implements MailSender {

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

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
Zrobiliśmy prace przygotowawcze, aby nasza OrderProcessorklasa opierała się nie na konkretnych szczegółach, ale na abstrakcjach. Zmienimy to, dodając nasze zależności do konstruktora 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);
        }
    }
}
Teraz nasza klasa zależy od abstrakcji, a nie konkretnych implementacji. Możemy łatwo zmienić jego zachowanie, dodając pożądaną zależność w momencie OrderProcessortworzenia obiektu. Zbadaliśmy zasady projektowania SOLID w Javie. Więcej o OOP i podstawach programowania w Javie — nic nudnego i setki godzin ćwiczeń — dowiesz się na kursie CodeGym. Czas rozwiązać kilka zadań :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION