JavaRush /Java блог /Random UA /П'ять основних принципів дизайну класів (SOLID) у Java
Ve4niY
14 рівень

П'ять основних принципів дизайну класів (SOLID) у Java

Стаття з групи Random UA
Класи - це блоки, з яких будується програма. Так само, як цегла в будівлі. Погано написані класи якось можуть принести проблеми. П'ять основних принципів дизайну класів (SOLID) в Java - 1Щоб зрозуміти, чи правильно написаний клас, можна звіритися зі стандартами якості. У Java це звані принципи SOLID. Про них і поговоримо.

Принципи SOLID у Java

SOLID - це акронім, утворений з великих літер перших п'яти принципів ОВП та проектування. Принципи придумав Роберт Мартін на початку двохтисячних, а абревіатуру пізніше впровадив Майкл Фезерс. Ось що входить у принципи SOLID:
  1. Single Responsibility Principle (Принцип єдиної відповідальності).
  2. Open Closed Principle (Принцип відкритості/закритості).
  3. Liskov's Substitution Principle (Принцип підстановки Барбари Лисков).
  4. Interface Segregation Principle (Принцип розподілу інтерфейсу).
  5. Dependency Inversion Principle (Принцип інверсії залежностей).

Принцип єдиної відповідальності (SRP)

Цей принцип говорить: ніколи не повинно бути більше однієї причини змінити клас. На кожен об'єкт покладається один обов'язок, повністю інкапсульований у клас. Усі послуги класу спрямовані забезпечення цього обов'язку. Такі класи завжди просто змінюватимуть, якщо це знадобиться, бо зрозуміло, за що клас відповідає, а за що — ні. Тобто можна буде вносити зміни та не боятися наслідків впливу на інші об'єкти. А ще подібний код набагато простіше тестувати, адже ви покриваєте тестами одну функціональність в ізоляції від решти. Уявіть модуль, який обробляє замовлення. Якщо замовлення правильно сформовано, він зберігає його в базу даних і надсилає листа для підтвердження замовлення:
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");
        // зберігаємо замовлення до бази даних

        return true;
    }

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

        // Шолом лист клієнту
    }
}
Такий модуль може змінитися з трьох причин. По-перше може стати іншою логікою обробки замовлення, по-друге, спосіб його збереження (тип бази даних), по-третє — спосіб відправки листа підтвердження (скажімо, замість email потрібно відправляти SMS). Принцип єдиного обов'язку передбачає, що три аспекти цієї проблеми насправді три різні обов'язки. Отже, повинні знаходитися в різних класах або модулях. Об'єднання кількох сутностей, які можуть змінюватись у різний час та з різних причин, вважається поганим проектним рішенням. Набагато краще розділити модуль на три окремі, кожен з яких виконуватиме одну єдину функцію:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // зберігаємо замовлення до бази даних

        return true;
    }
}

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

        // Шолом лист клієнту
    }
}

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)

Цей принцип ємно описують так: програмні сутності (класи, модулі, функції тощо) мають бути відкриті розширення, але закриті зміни . Це означає, що має бути можливість змінювати зовнішню поведінку класу, не вносячи фізичні зміни до самого класу. Дотримуючись цього принципу, класи розробляються так, щоб для підстроювання класу до конкретних умов застосування було достатньо розширити його та перевизначити деякі функції. Тому система має бути гнучкою, з можливістю роботи у змінних умовах без зміни вихідного коду. Продовжуючи наш приклад із замовленням, припустимо, що нам потрібно виконувати якісь дії перед обробкою замовлення та після надсилання листа з підтвердженням. Замість того, щоб міняти сам класOrderProcessor, ми розширимо його і досягнемо вирішення поставленого завдання, не порушуючи принцип OCP:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

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

    private void beforeProcessing() {
        // Здійснимо деякі дії перед обробкою замовлення
    }

    private void afterProcessing() {
        // Здійснимо деякі дії після обробки замовлення
    }
}

Принцип підстановки Барбари Лисків (LSP)

Це варіація принципу відкритості/закритості, про який йшлося раніше. Його можна описати так: об'єкти у програмі можна замінити їх спадкоємцями без зміни властивостей програми. Це означає, що клас, розроблений шляхом розширення на основі базового класу, повинен перевизначати його методи так, щоб не порушувалася функціональність з погляду клієнта. Тобто, якщо розробник розширює ваш клас і використовує його в додатку, він не повинен змінювати очікувану поведінку перевизначених методів. Підкласи повинні перевизначати методи базового класу те щоб не порушувалася функціональність з погляду клієнта. Докладно можна розглянути на наступному прикладі. Припустимо, у нас є клас, який відповідає за валідацію замовлення та перевіряє, чи всі з товарів замовлення знаходяться на складі. Цей клас має методisValidякий повертає true або 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;
    }
}
Однак у даному класі ми порушабо принцип LSP, тому що замість того, щоб повернути false , якщо замовлення не пройшло валідацію, наш метод кидає виняток IllegalStateException. Клієнти цього коду не розраховують на таке: вони чекають на повернення true або false . Це може призвести до помилок у роботі програми.

Принцип поділу інтерфейсу (ISP)

Характеризується наступним твердженням: клієнти повинні бути змушені реалізовувати методи, які вони використовуватимуть . Принцип поділу інтерфейсів свідчить, що занадто «товсті» інтерфейси необхідно розділяти більш дрібні і специфічні, щоб клієнти дрібних інтерфейсів знали лише методах, необхідні роботи. У результаті, при зміні методу інтерфейсу нічого не винні змінюватися клієнти, які цей метод не використовують. Розглянемо приклад. Розробник Алекс створив інтерфейс "звіт" і додав два методи: generateExcel()іgeneratedPdf(). Тепер клієнт А хоче використовувати цей інтерфейс, але він має намір використовувати звіти лише у форматі PDF, а не в Excel. Чи влаштує його така функціональність? Ні. Він повинен буде реалізувати два методи, один з яких, за великим рахунком, не потрібен і існує тільки завдяки Алексу — дизайнеру програмного забезпечення. Клієнт скористається іншим інтерфейсом, або залишить поле для Excel порожнім. То в чому ж рішення? Воно полягає в поділі існуючого інтерфейсу на два дрібніші. Один – звіт у форматі PDF, другий – звіт у форматі Excel. Це дасть користувачеві можливість використовувати лише необхідний функціонал.

Принцип інверсії залежностей (DIP)

Цей принцип SOLID Java описують так: залежності всередині системи будуються на основі абстракцій. Модулі верхнього рівня не залежить від модулів нижнього рівня. Абстракції не повинні залежати від деталей. Деталі мають залежати від абстракцій. Програмне забезпечення потрібно розробляти так, щоб різні модулі були автономними та з'єднувалися один з одним за допомогою абстракції. Класичне застосування цього принципу Spring framework. У рамках Spring framework всі модулі виконані у вигляді окремих компонентів, які можуть працювати разом. Вони настільки автономні, що можуть бути з такою ж легкістю задіяні в інших програмних модулях, крім Spring framework. Це досягнуто за рахунок залежності закритих та відкритих принципів. Усі модулі надають доступ лише до абстракції, яка може використовуватись в іншому модулі. Спробуємо продемонструвати це з прикладу. Говорячи про принцип єдиної відповідальності,OrderProcessor. Погляньмо ще раз на код цього класу:
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. Наведемо також код даних класів:
public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // зберігаємо замовлення до бази даних

        return true;
    }
}

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

        // Шолом лист клієнту
    }
}
Ці класи далекі від того, щоб називатися абстракціями. І з погляду принципу DIP було б правильніше спочатку створити деякі абстракції, які дозволять нам оперувати надалі ними, а чи не конкретними реалізаціями. Створимо два інтерфейси 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();

        // Шолом лист клієнту
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // зберігаємо замовлення до бази даних

        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. Більше про ОВП загалом, основи цієї мови програмування - ненудно і з сотнями годинами практики - в курсі JavaRush. Час вирішити кілька завдань :) П'ять основних принципів дизайну класів (SOLID) у Java - 2
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ