CodeGym /Java блог /Случаен /SOLID: Пет основни принципа на проектиране на класове в J...
John Squirrels
Ниво
San Francisco

SOLID: Пет основни принципа на проектиране на класове в Java

Публикувано в групата
Класовете са градивните елементи на applicationsта. Точно като тухли в сграда. Лошо написаните класове в крайна сметка могат да причинят проблеми. SOLID: Пет основни принципа на дизайн на класове в Java - 1За да разберете дали даден клас е правилно написан, можете да проверите How отговаря на „стандартите за качество“. В Java това са така наречените SOLID принципи и ние ще говорим за тях.

SOLID принципи в Java

SOLID е акроним, образуван от главните букви на първите пет принципа на ООП и клас дизайн. Принципите са изразени от Робърт Мартин в началото на 2000-те години, а след това съкращението е въведено по-късно от Майкъл Федърс. Ето принципите на SOLID:
  1. Принцип на единната отговорност
  2. Отворен затворен принцип
  3. Принцип на заместване на Лисков
  4. Принцип на разделяне на интерфейса
  5. Принцип на инversion на зависимостта

Принцип на единната отговорност (SRP)

Този принцип гласи, че никога не трябва да има повече от една причина за смяна на клас. Всеки обект има една отговорност, която е напълно капсулирана в класа. Всички услуги на класа са насочени към поддържане на тази отговорност. Такива класове винаги ще бъдат лесни за промяна, ако е необходимо, защото е ясно Howъв е класът и за Howво не отговаря. С други думи, ще можем да правим промени и да не се страхуваме от последствията, т.е. въздействието върху други обекти. Освен това такъв code е много по-лесен за тестване, тъй като вашите тестове покриват една част от функционалността изолирано от всички останали. Представете си модул, който обработва поръчки. Ако поръчката е правилно оформена, този модул я записва в база данни и изпраща имейл за потвърждение на поръчката:

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
    }
}
Този модул може да се промени по три причини. Първо, логиката за обработка на поръчки може да се промени. Второ, начинът, по който се запазват поръчките (тип база данни) може да се промени. Трето, начинът, по който се изпраща потвърждението, може да се промени (например, да предположим, че трябва да изпратим текстово съобщение, а не имейл). Принципът на единната отговорност предполага, че трите аспекта на този проблем всъщност са три различни отговорности. Това означава, че те трябва да бъдат в различни класове or модули. Комбинирането на няколко обекта, които могат да се променят по различно време и по различни причини, се счита за лошо дизайнерско решение. Много по-добре е един модул да се раздели на три отделни модула, всеки от които изпълнява една единствена функция:

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);
        }
    }

}

Отворен затворен принцип (OCP)

Този принцип е описан по следния начин: софтуерните единици (класове, модули, функции и т.н.) трябва да бъдат отворени за разширение, но затворени за модификация . Това означава, че трябва да е възможно да се промени външното поведение на класа, без да се правят промени в съществуващия code на класа. Съгласно този принцип класовете са проектирани така, че настройването на клас, за да отговаря на конкретни условия, просто изисква разширяването му и отмяната на някои функции. Това означава, че системата трябва да бъде гъвкава, да може да работи в променящи се условия, без да променя изходния code. Продължавайки нашия пример, включващ обработка на поръчка, да предположим, че трябва да извършим някои действия, преди поръчката да бъде обработена, Howто и след изпращането на имейла за потвърждение. Вместо да променитеOrderProcessorсамия клас, ние ще го разширим, за да постигнем нашата цел, без да нарушаваме отворения затворен принцип:

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
    }
}

Принцип на заместване на Лисков (LSP)

Това е вариант на отворения затворен принцип, който споменахме по-рано. Може да се дефинира по следния начин: обектите могат да бъдат заменени с обекти от подкласове, без да се променят свойствата на програмата. Това означава, че клас, създаден чрез разширяване на базов клас, трябва да замени своите методи, така че функционалността да не бъде компрометирана от гледна точка на клиента. Тоест, ако разработчикът разшири вашия клас и го използва в приложение, той or тя не трябва да променя очакваното поведение на Howвито и да е заменени методи. Подкласовете трябва да заменят методите на базовия клас, така че функционалността да не бъде нарушена от гледна точка на клиента. Можем да разгледаме това подробно в следния пример. Да предположим, че имаме клас, който отговаря за валидирането на поръчка и проверката дали всички стоки в поръчката са на склад.isValid()метод, който връща true or false :

public class OrderStockValidator {

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

        return true;
    }
}
Да предположим също, че някои поръчки трябва да бъдат валидирани по различен начин от други, например за някои поръчки трябва да проверим дали всички стоки в поръчката са на склад и дали всички стоки са опаковани. За да направим това, разширяваме OrderStockValidatorкласа, като създаваме 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;
    }
}
Но тук сме нарушor принципа на заместване на Liskov, защото instead of да върне false , ако поръчката не е валидирана, нашият метод хвърля IllegalStateException. Клиентите, използващи този code, не очакват това: те очакват върната стойност true or false . Това може да доведе до грешки по време на изпълнение.

Принцип на разделяне на интерфейса (ISP)

Този принцип се характеризира със следното твърдение: клиентът не трябва да бъде принуждаван да прилага методи, които няма да използва . Принципът на разделяне на интерфейса означава, че интерфейсите, които са твърде „дебели“, трябва да бъдат разделени на по-малки, по-специфични, така че клиентите, използващи малки интерфейси, да знаят само за методите, от които се нуждаят за тяхната работа. В резултат на това, когато метод на интерфейс се промени, всички клиенти, които не използват този метод, не трябва да се променят. Помислете за този пример: Алекс, разработчик, създаде интерфейс за „отчет“ и добави два метода: generateExcel()иgeneratedPdf(). Сега клиент иска да използва този интерфейс, но възнамерява да използва отчети само в PDF формат, а не в Excel. Тази функционалност ще удовлетвори ли този клиент? Не. Клиентът ще трябва да приложи два метода, единият от които до голяма степен не е необходим и съществува само благодарение на Алекс, този, който е проектирал софтуера. Клиентът ще използва or различен интерфейс, or ще направи нищо с метода за отчети на Excel. И така, Howво е решението? Той е да раздели съществуващия интерфейс на два по-малки. Едната за PDF отчети, другата за Excel отчети. Това позволява на клиентите да използват само функционалността, от която се нуждаят.

Принцип на инversion на зависимостта (DIP)

В Java този SOLID принцип е описан по следния начин: зависимостите в рамките на системата са изградени въз основа на абстракции. Модулите от по-високо ниво не зависят от модулите от по-ниско ниво. Абстракциите не трябва да зависят от детайлите. Подробностите трябва да зависят от абстракциите. Софтуерът трябва да бъде проектиран така, че различните модули да са самостоятелни и свързани помежду си чрез абстракция. Класическо приложение на този принцип е Spring Framework. В Spring Framework всички модули са внедрени като отделни компоненти, които могат да работят заедно. Те са толкова автономни, че могат да се използват също толкова лесно в програмни модули, различни от Spring Framework. Това се постига благодарение на зависимостта на затворения и отворения принцип. Всички модули осигуряват достъп само до абстракцията, която може да се използва в друг модул. Нека се опитаме да илюстрираме това с пример. Говорейки за принципа на единната отговорност, ние разгледахмеOrderProcessorклас. Нека да погледнем отново codeа на този клас:

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);
        }
    }

}
В този пример нашият OrderProcessorклас зависи от два конкретни класа: MySQLOrderRepositoryи ConfirmationEmailSender. Ще представим и codeа на тези класове:

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
    }
}
Тези класове са далеч от това, което бихме нарекли абстракции. И от гледна точка на принципа на инversion на зависимостта, би било по-добре да започнем със създаване на някои абстракции, с които можем да работим в бъдеще, а не със специфични реализации. Нека създадем два интерфейса: MailSenderи OrderRepository). Това ще бъдат нашите абстракции:

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

public interface OrderRepository {
    boolean save(Order order);
}
Сега прилагаме тези интерфейси в класове, които вече са подготвени за това:

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;
    }
}
Направихме подготвителната работа, така че OrderProcessorкласът ни да зависи не от конкретни детайли, а от абстракции. Ще го променим, като добавим нашите зависимости към конструктора на класа:

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);
        }
    }
}
Сега нашият клас зависи от абстракции, а не от конкретни реализации. Можем лесно да променим поведението му, като добавим желаната зависимост в момента OrderProcessorна създаване на обект. Разгледахме принципите на проектиране на SOLID в Java. Ще научите повече за ООП като цяло и за основите на програмирането на Java — нищо скучно и стотици часове практика — в курса CodeGym. Време е за решаване на няколко задачи :)
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION