JavaRush/Java блог/Random/Spring. Урок 2. IoC / DI на практике
Umaralikhon
3 уровень

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

Статья из группы Random
участников
И так... На предыдущем занятии мы коротко рассмотрели теоретическую часть 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, который имеет два метода для описания сотрудника. (Листинг 6)
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. Он будет выглядеть вот так: (Листинг11)
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 Телега Содержание курса Продолжение следует...
Комментарии (8)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
just_DO_it
Уровень 18
14 октября 2021, 14:32
внедрение зависимости Developmen это не реализация DI, это реализация DIP (D из SOLID) А вот то что спринг находит бины и инжектит их куда нужно - это как раз таки и есть DI - реализация IoC.
Nick
Уровень 20
20 июня 2021, 19:43
И да самое главное во всех этих начальных уроках по спрингу (по крайне мере для меня) Берем метод через интерфейс. Потом берем спринг, через конфигуратор xml. И получаем: Метод через интерфейс = спрингу + строка подключения конфигуратора, плюс конфигуратор xml, плюс наименования тех же бинов в виде текста. И у меня как у новичка - сразу возникает вопрос, а нафуя это всё городить, в чем соль то? Из-за чего народ в своё время так возбудился и массово начал использовать спринг? Пока ещё вразумительного примера в подобных учебных материалах, кроме малопонятного "зависимости минимизировались" ещё не встречал.
Nick
Уровень 20
20 июня 2021, 18:52
Очень не удачный первый пример. Этот урок по факту реклама, которая должна показать преимущество одного метода над другими. И как это увидел я:
Чем лучше забивать гвозди?  Вот этим камнем? Или вот этим нашем супер авто-молотком!
з.ы. но всё равно плюсик за статью, таких бы побольше.
Luicich
Уровень 39
2 июня 2021, 13:19
спасибо за статью, хотелось бы подробнее про разные способы внедрения зависимостей (плюсы/минусы)
Umaralikhon Java Developer
11 июня 2021, 23:33
Тут особой разницы нет. Все зависит от вкусовщины разработчика. Тут я немного описал про внедрение через property файлы. Здесь быть может найдете что - то полезное)
Nick
Уровень 20
20 июня 2021, 19:18
В любом случае, как мне сказали сотрудники из очень большой команды - нужно знать все способы. Ибо они все встречаются в разных проектах.
ILYCH Java Developer в InDevLabs
1 июня 2021, 12:11
Класс main надо создавать в директории java?
Umaralikhon Java Developer
1 июня 2021, 13:06
Да