И так...
На предыдущем занятии мы коротко рассмотрели теоретическую часть IoC и DI. А так же настроили конфигурационный файл pom.xml для нашего проекта. Сегодня же приступаем к созданию основной части программы. Сначала покажу как создается программа без IoC / DI. А потом уже непосредственно создадим программу, которая самостоятельно внедряет зависимости. То есть управление кодом переходит в руки фреймворка (звучит жутковато).
Пока программой управляем мы
Представим, что есть некая компания. И в компании (пока что) есть два отдела: отдел разработки Java (JavaDevelopment) и отдел найма (HiringDepartment). Пусть класс описывающий "Отдел разработки Java" имеет два метода: String getName() - возвращающий имя сотрудника, String getJob() - возвращающий должность сотрудника. (Листинг 1)
Да, "Копи - пастить" никто не запрещал, но это не профессионально. Тыж программист. И тут можно воспользоваться DI. То есть будем работать не на уровне классов, а на уровне интерфейсов. Теперь состояния наших объектов будут хранится в интерфейсах. Таким образом зависимости между классами будут минимальны. Для этого сначала создадим интерфейс Development, который имеет два метода для описания сотрудника. (Листинг 6)
В этом файле напишем следующий фрагмент кода (Листинг 10):
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());
}
}
}
Как мы видим объем кода увеличилось в два раз, а то и больше. При большом объеме кода, падает его читаемость. И самое худшее мы все объекты создаем вручную и делаем классы сильно зависящие друг от друга. Ок, мы согласились с этим. Всего лишь описали один отдел. Мы от этого ничего не потеряем. Ну что если добавится ещё один отдел? А что если два? Три? А "копи - пастить" же никто не запрещал.

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 (автоматическое связывание)
- С помощью XML - файлов (Устаревший способ)
- С помощью аннотаций + XML - файлов (Современный способ)
- С помощью Java - кода(Современный способ)

<?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
Телега
Содержание курса
Продолжение следует...
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ