JavaRush /Java блог /Random UA /Spring. Урок 2. IoC/DI на практиці
Umaralikhon
3 рівень
Красноярск

Spring. Урок 2. IoC/DI на практиці

Стаття з групи Random UA
І так... На попередньому занятті ми коротко розглянули теоретичну частину IoC та DI. Також налаштували конфігураційний файл pom.xml для нашого проекту. Сьогодні ж розпочинаємо створення основної частини програми. Спочатку покажу як створюється програма без IoC/DI. А потім вже безпосередньо створимо програму, яка самостійно впроваджує залежність. Тобто управління кодом переходить до рук фреймворку (звучить моторошно). Поки програмою керуємо ми Уявимо, що є якась компанія. І в компанії (поки що) є два відділи: відділ розробки Java (JavaDevelopment) та відділ найму (HiringDepartment). Нехай клас, що описує "Відділ розробки Java", має два методи: String getName() - повертає ім'я співробітника, String getJob() - повертає посаду співробітника. (Лістинг 1)
package org.example;

public class JavaDevelopment {

    public String getName(){
        return "Alexa";
    }

    public String getJob(){
        return "Middle Java developer";
    }
}
Нехай клас, що описує відділ найму, має конструктор на вхід, який приймає співробітника, і метод void displayInfo(), який виводить інформацію про співробітників. (Лістинг 2)
package org.example;

public class HiringDepartment {
    private JavaDevelopment javaDevelopment;

    public HiringDepartment(JavaDevelopment javaDevelopment) {
        this.javaDevelopment = javaDevelopment;
    }

    public void displayInfo() {
        System.out.println("Name: " + javaDevelopment.getName());
        System.out.println("Job: " + javaDevelopment.getJob());
    }
}
А також є Main - клас, який керує всіма відділами. (Лістинг 3)
package org.example;

public class Main {
    public static void main(String ... args){
        JavaDevelopment javaDevelopment = new JavaDevelopment();
        HiringDepartment hiringDepartment = new HiringDepartment(javaDevelopment);

        hiringDepartment.displayInfo();
    }
}
Поки що стабільність. При запуску Main - класу отримуємо наступний результат:
Name: Alexa
Job: Middle Java developer
А тепер уявімо, що у компанії справи йдуть чудово. Тому вони вирішабо розширити сферу діяльності і відкрабо відділ Python - розробки. І тут виникає питання: А як описати цей відділ на програмному рівні? Відповідь: треба "копи - пасти" скрізь, де потрібно описати цей відділ (старий, добрий метод 🙃). Для початку створимо саму клас, який би описав відділ "Пітоністів". (Лістинг 4)
package org.example;

public class PythonDevelopment {
    public String getName(){
        return "Mike";
    }

    public String getJob(){
        return "Middle Python developer";
    }
}
А потім передамо його на відділ найму (HiringDepartment). А в HiringDepartment'e про цей відділ нічого не сказано. Тому доведеться створити новий об'єкт класу PythonDevelopment і конструктор, який приймає Python - розробників. А також доведеться змінити метод displayInfo() - щоб він коректно відображав інформацію. (Лістинг 5)
package org.example;

public class HiringDepartment {
    private JavaDevelopment javaDevelopment;

    public HiringDepartment(JavaDevelopment javaDevelopment) {
        this.javaDevelopment = javaDevelopment;
    }


    //Тут создается отдел найма для Python - разработчиков
    private PythonDevelopment pythonDevelopment;

    public HiringDepartment(PythonDevelopment pythonDevelopment) {
        this.pythonDevelopment = pythonDevelopment;
    }

    //Тогда придется изменить метод displayInfo()
    public void displayInfo() {
        if(javaDevelopment != null) {
            System.out.println("Name: " + javaDevelopment.getName());
            System.out.println("Job: " + javaDevelopment.getJob());
        } else if (pythonDevelopment != null){
            System.out.println("Name: " + pythonDevelopment.getName());
            System.out.println("Job: " + pythonDevelopment.getJob());
        }
    }
}
Як бачимо обсяг коду збільшилося вдвічі, або навіть більше. При великому обсязі коду падає його читальність. І найгірше ми всі об'єкти створюємо вручну і робимо класи, що сильно залежать один від одного. Ок, ми погодабось із цим. Лише описали один відділ. Ми цього нічого не втратимо. Ну якщо додасться ще один відділ? А що коли два? Три? А "копи - пастити" ніхто не забороняв. Spring.  Урок 2. IoC/DI на практиці - 1 Так, "Копі – пастити" ніхто не забороняв, але це не професійно. Тиж програміст. І тут можна скористатися DI. Тобто працюватимемо не на рівні класів, а на рівні інтерфейсів. Тепер стан наших об'єктів буде зберігатися в інтерфейсах. Таким чином, залежності між класами будуть мінімальні. Для цього спочатку створимо інтерфейс Development, який має два методи для опису співробітника.
package org.example;

public interface Development {
    String getName();
    String getJob();
}
Нехай тоді два класи JavaDevelopment та PythonDevelopment імплементуються (успадковується) від цього інтерфейсу та перевизначать методи String getName() та String getJob(). (Лістинг 7, 8)
package org.example;

public class JavaDevelopment implements Development {
    @Override
    public String getName(){
        return "Alexa";
    }

    @Override
    public String getJob(){
        return "Middle Java developer";
    }
}
package org.example;

public class PythonDevelopment implements Development {
    @Override
    public String getName(){
        return "Mike";
    }

    @Override
    public String getJob(){
        return "Middle Python developer";
    }
}
Тоді в класі HiringDepartment можна просто визначити об'єкт інтерфейсу типу Development і конструктор теж можна передати такий об'єкт. (Лістинг 9)
package org.example;

public class HiringDepartment {
    private Development development; //Определяем интерфейс

    //Конструктор принимает об'єкт интерфейса
    public HiringDepartment(Development development){
        this.development = development;
    }

    public void displayInfo(){
        System.out.println("Name: " + development.getName());
        System.out.println("Job: " + development.getJob());
    }
}
Як бачимо обсяг коду зменшився. І, головне, залежності мінімізувалися. А як власне впроваджуються значення та залежності до цих об'єктів? Є три способи застосування залежностей:
  • За допомогою конструктора
  • За допомогою сетерів
  • Autowiring (автоматичне зв'язування)
Впровадження за допомогою конструктора Зараз поговоримо про впровадження за допомогою конструктора. Подивіться на листинг 9. Конструктор класу HiringDepartment на вхід чекає на об'єкт типу Development. Ми через цей конструктор спробуємо запровадити залежність. Також варто відзначити, що використання залежностей проводиться за допомогою так званої Spring контейнерів (Spring container). Існує три способи конфігурації Spring container'ів:
  • За допомогою XML-файлів (Застарілий спосіб)
  • За допомогою анотацій + XML – файлів (Сучасний спосіб)
  • За допомогою Java - коду (Сучасний спосіб)
Ми ж зараз використовуємо конфігурацію за допомогою XML-файлів. Незважаючи на те, що цей спосіб вважається застарілим, все ж таки безліч проектів написані саме таким способом. Тож треба знати. Для початку в папці resources повинні створити xml-файл. Йому можна дати будь-яку назву, але бажано осмислені. Я назвав "applicationContext.xml". Spring.  Урок 2. IoC/DI на практиці - 2 У цьому файлі напишемо наступний фрагмент коду (Лістинг 10):
<?xml version="1.0" encoding="UTF-8"?>
<beans  xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="javaDeveloper" class="org.example.JavaDevelopment"/>
    <bean id="pythonDeveloper" class="org.example.PythonDevelopment"/>

    <bean id="hiringDepartment" class="org.example.HiringDepartment">
        <constructor-arg ref="javaDeveloper"/>
    </bean>

</beans>
Тепер по порядку. Перші вісім рядків коду нам не цікаві, дефолтні. Їх можна легко скопіювати. Тег <bean> </bean> визначає Spring bean. Bean - це об'єкт, який створюється і керується Spring container'ом. Простими словами, Spring container сам створює новий об'єкт класу для нас (наприклад: JavaDevelopment javaDevelopment = new JavaDevelopment();). Усередині цього тега є атрибути id та class. id визначає назву біну. Тому id можна буде звертатися до об'єкта. Він еквівалентний назві об'єкта у Java класі. class- Визначає назву класу, до якого прив'язаний наш бін (об'єкт). Потрібно зазначати повний шлях до класу. Зверніть увагу на бін hiringDepartment. Усередині цього бина є ще один тег <constructor-arg ref="javaDeveloper"/>. Тут відбувається використання залежності (у разі використання з допомогою конструктора). <constructor-arg> - вказує Spring'у, що Spring container повинен шукати залежності в конструкторі класу, визначений в атрибуті бина. А з яким об'єктом потрібно зв'язати визначає атрибут ref всередині тега <constructor-arg>. ref - вказує на id бина, з яким потрібно зв'язуватися. Якщо в ref замість javaDeveloper вказуємо id pythonDeveloper, зв'язка відбувається з класом PythonDevelopmen. Тепер потрібно описати Main class. Він виглядатиме ось так:
package org.example;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String ... args){
        //Определяем контекст файл в котором содержатся прописанные нами бины
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        //Получем бины, которые были определены в файле applicationContext.xml
        HiringDepartment hiringDepartment = context.getBean("hiringDepartment", HiringDepartment.class);

        hiringDepartment.displayInfo();

        context.close(); //Контекст всегда должен закрываться
    }
}
Що тут?
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Цей рядок пов'язує Main клас із .xml файлом, в якому описані наші біни. Значення, що передається в конструктор, повинен збігатися з ім'ям .xml файлу. (У нашому випадку applicationContext.xml).
HiringDepartment hiringDepartment = context.getBean("hiringDepartment", HiringDepartment.class);
Вказує, що хочемо отримати бін (об'єкт) класу HiringDepartment. Перший аргумент вказує на id бина що ми написали у xml файлі. Другий аргумент вказує на клас, з яким хочемо зв'язатися. Цей процес називається рефлексією .
hiringDepartment.displayInfo();
 context.close(); //Контекст всегда должен закрываться
Тут ми спокійно отримуємо метод класу HiringDepartment. Зауважте, що для отримання об'єктів ми не використовували ключове слово new, і ніде не визначабо залежні об'єкти типу JavaDevelopment або PythonDevelopment. Їх просто описали у контексті файлу applicationContext.xml. Також зверніть увагу на останній рядок. Перед завершенням роботи завжди маємо закрити контекст. В іншому випадку ресурси не звільняться, може статися витік пам'яті або некоректність під час роботи програми. Якщо є питання чи пропозиції пишіть у коментарях, обов'язково відповім. Дякую за увагу. Вихідний код за посиланням Мій GitHub Віз Зміст курсу Далі буде...
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ