Inversion of Control

Инверсия управления

На примере Spring

Dependency Injection

Внедрение зависимостей

Пример от Мартина Фаулера

http://www.martinfowler.com/articles/injection.html

Dependency Injection

Внедрение зависимостей

public class MovieLister {...
  public Collection<Movie> moviesDirectedBy(String director) {
    List<Movie> allMovies = finder.findAll();
    for(Iterator<Movie> it = allMovies.iterator(); it.hasNext();) {
      Movie movie = it.next();
      if(!movie.getDirector().equals(director)) it.remove();
    }
    return allMovies;
  }

Dependency Injection

Внедрение зависимостей
  • Определим интерфейс

public interface MovieFinder {
  List<Movie> findAll();
}

Dependency Injection

Откуда мы возьмём конкретную реализацию?


public class MovieLister {
  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

На прошлой практике был вариант с Singleton

Dependency Injection

Схема зависимостей

Dependency Injection

В чём проблема?

  • Lister зависит и от интерфейса, и от реализации Finder-а
  • Если убрать зависимость - откуда брать реализацию?
  • Как использовать разные реализации в разных условиях?

Dependency Injection

Решение

  • Assembler, управляющий зависимостями
  • Предоставляет конкретную реализацию для интерфейса

Dependency Injection

Spring: внедрение через поле


@Named
public class ColonDelimitedMovieFinder implements MovieFinder {
}

@Named
public class MovieLister {
  @Inject
  private MovieFinder finder;
}
	

Dependency Injection

Было/Стало


public class MovieLister {
  private MovieFinder finder;

  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

@Named
public class MovieLister {
  @Inject
  private MovieFinder finder;
}

Dependency Injection

Spring: внедрение через конструктор


@Inject
public class ColonDelimitedMovieFinder implements MovieFinder {
}

@Named
public class MovieLister {

  private MovieFinder finder;

  @Inject
  public MovieLister(MovieFinder fnd) {
    this.finder = fnd;
  }
...
	

Dependency Injection

Spring: внедрение через конструктор

Удобно для тестирования

Внедрение зависимостей в Spring

  • BeanFactory (Фабрика бинов) - Ассемблер, сборщик
  • Bean (Бин) - объект системы, содержащий логику

Bean

Бин

Это объект системы, который

  • создан
  • управляется Spring-ом

Как создать Bean

Пометить класс аннотацией

  • @Named
  • @Component

Как создать Bean


@Named
public class CsvMovieFinder implements MovieFinder {
  private String fileName;
	...
}

Как создать Bean

Может быть создан конфигурацией


@Configuration
public class MovieFinderConfiguration {

  @Bean
  public MovieFinder movieFinder() {
    ...
    Collection<Movie> movies =
        moviesDao.getAllMoviesFromDataBase();
    ...
    return new CollectionMovieFinder(movies);
  }
}

Например, если нужно выполнить дополнительные действия, которые не хочется делать частью логики бина

Как создать Bean

Bean может быть даже строкой


@Configuration
public class DatabaseConfiguration {

  @Bean
  public String databaseVendor() {
    ...
    String vendor = getVendor();
    ...
    return vendor;
  }
}

Как внедрить Bean

Внедрение через поле


@Named
public class MovieLister {
  @Inject
  private MovieFinder finder;
}
	

Как внедрить Bean

Внедрение через конструктор


@Named
public class MovieLister {

  private MovieFinder finder;

  @Inject
  public MovieLister(MovieFinder finder) {
    this.finder = finder;
  }
...

Внедрение Bean

  • Если внедряется один бин, то он должен определяться однозначно
  • Или не должно быть других бинов с таким интерфейсом
  • Или внедрение должно быть по имени
  • Или должны быть заданы приоритеты

Как внедрить Bean

Внедрение списка бинов


@Named
public class MartinScorsese implements Director {
@Named
public class JamesCameron implements Director {

@Named
public class DirectorsService {
  @Inject
  private List<Director> allDirectors;

Если есть несколько реализаций одного интерфейса

Как внедрить Bean

Внедрение бина по имени


@Named("csvFinder")
public class CsvMovieFinder implements MovieFinder {

@Named("oracleFinder")
public class OracleMovieFinder implements MovieFinder {

@Named
public class MovieLister {
  @Inject @Named("csvFinder")
  private MovieFinder finder;

Если есть несколько реализаций одного интерфейса

Имя Bean

  • Указано явно в аннотации @Named
  • Имя класса со строчной буквы, если создан через аннотацию
  • Имя метода со строчной буквы, если создан в конфигурации

Как получить Bean

Получение бина из фабрики


@Named
public class MovieLister {...

  @Inject
  private BeanFactory factory;

  private String beanName;

  public Collection moviesDirectedBy(String director) {
    MovieFinder finder = factory.getBean(beanName);
    List allMovies = finder.findAll();

Жизненный цикл бина

  • @PostConstruct
  • @PreDestroy

Жизненный цикл бина

@PostConstruct

  • Бин создан
  • Собран (есть все зависимости)
  • Вызов сразу после инъекции зависимостей

Жизненный цикл бина

@PostConstruct


@Named
public class CachingMovieLister {

  @PostConstruct
  public void populateMovieCache() {
    // Загружаем данные из БД в кэш
    cache.addAll(readFromDataBase());
  }
}

Жизненный цикл бина

@PreDestroy

Бин удаляется из фабрики

Жизненный цикл бина

@PreDestroy


@Named
public class CachingMovieLister {

  @PreDestroy
  public void clearMovieCache() {
    // Освобождаем кэш
    cache.clear();
  }
}

Область видимости

(Scope)
  • Singleton - по умолчанию
  • Prototype - новый экземпляр при каждом вызове
  • Request - на один HTTP-запрос
  • Session - на одну HTTP-сессию

Область видимости


@Configuration
public class MovieFinderConfiguration {

  @Bean
  @Scope("prototype")
  public CsvMovieFinder csvMovieFinder() {
    return new CsvMovieFinder("movies.csv");
  }
}

Какой шаблон здесь реализован?

Внедрение свойств

@Value

  • Свойства из property-файлов
    (конфигурация)
  • Системные свойства
    (окружение, виртуальная машина)

Внедрение системных свойств


@Named
public class SomeBean {
  @Value("#{systemProperties['databaseName']}")
  private String databaseName;
}

Системное свойство можно задать через аргумент VM

-DdatabaseName=Oracle

Внедрение конфигурации


@Named
public class SomeBean {
  @Value("${databaseName}")
  private String databaseName;
}

Конфигурация задаётся в property файлах

Внимание! $ вместо #

Аннотации в Spring могут "наследоваться"


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

Controller - это тоже Component, т.е. Bean

Аннотации в Spring могут "наследоваться"


@Documented
@Controller
@ResponseBody
public @interface RestController {

RestController - это тоже Controller, т.е. Component...

Задание на практику

https://github.com/ykalemi/ioc
  • Запустить приложение и проверить его работу
  • Вместо Singleton.getInstance() использовать внедрение зависимости. Выделить интерфейс для хранилища
  • Создать StorageInitializer. Если хранилище при старте пустое - добавить дефолтные значения (убрать их из конструктора)
  • Для добавления данных через REST сделать сервис валидации ValidationService с двумя валидаторами (Validator): один только пишет в System.out, другой проверяет что дата не старше 2017 года
  • Реализовать новое хранилище - в файле (сохранение данных через сериализацию, ObjectOutputStream.writeObject). Внедрять хранилище по имени бина
  • Имя файла для файлового хранилища должно задаваться в application.properties
  • Используемое хранилище должно задаваться при запуске приложения через аргумент виртуальной машины (-DstorageType=fileStorage)