Inversion of Control

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

На примере Spring

Интерфейсы vs. классы

Интерфейсы vs. классы

  • Неявный интерфейс класса - что доступно извне
  • Интерфейс - это способ взаимодействия
  • Класс реализует интерфейс
  • Что необходимо и достаточно реализовать

Почему это важно?

  • Я пишу один, без команды!
  • Моим кодом не пользуются другие программисты!
  • Инкапсуляция - основа хорошего дизайна
  • Явный контракт легче соблюдать чем неявный
  • Контракт не только для пользователя, но и для владельца
  • Легче читать код
  • Легче тестировать
  • Легче рефакторить
  • Вы через год - это другой человек

Фреймворки и библиотеки

Фреймворки и библиотеки

  • Всё уже написано
  • И протестировано (как тестами, так и другими программистами)
  • Новые технологии (на которые у вас нет времени)
  • Улучшения (на которые у вас нет времени)
  • Простое вхождение в проект

Фреймворки и библиотеки. Проблемы

  • Отступления - могут быть дорогими / невозможными
  • Развитие может уйти в ненужную для компании сторону
  • Переход с версии на версию требует затрат
  • Окончание поддержки и развития

Библиотека

  • Набор функций, которые можно вызвать
  • Библиотека делает работу и возвращает результат
  • Примеры: прочитать файл; сделать запись в лог

Фреймворк

  • Воплощает какой-то дизайн, архитектуру
  • Поведение уже встроено в фреймворк
  • Фреймворк - это каркас

Чтобы использовать фреймворк

  • Встроить своё поведение в различные места фреймворка
  • Собственные классы, или реализации интерфейсов (наследники)
  • Код фреймворка вызывает ваш код в конкретных точках

Inversion of Control (Инверсия управления)

  • Ключевая характеристика фреймворка
  • Контроль остаётся в коде фреймворка
  • Пользователь теряет контроль

Пример 1: командная строка


public static void main(String args[]) {
  Scanner scanner = new Scanner(System.in);
  String str = "";
  while (!str.equals("exit")) {
    str = scanner.nextLine();
    ...
					
  • Полное управление
  • Ждём команды пользователя - пишем результат

Пример 2: HTTP-запрос


@RequestMapping(path = "/")
public ModelAndView index() {
    List clients = influxDAO.getDbList();
    ...
    return new ModelAndView("clients", model, HttpStatus.OK);
}
					
  • Мы просто возвращаем ответ
  • Что происходило до вызова метода?
  • Теряем управление

Inversion of Control (Инверсия управления)

  • Ключевая характеристика фреймворка
  • Контроль остаётся в коде фреймворка
  • Пользователь теряет контроль
  • Это и есть Инверсия управления

Как встроить свой код

  • Фреймворк генерирует события, клиентский код - подписывается
  • Интерфейс, который должен быть реализован клиентом
  • Шаблонный метод (Template method) (см. JUnit)

JUnit

  • Мы не управляем порядком вызовов
  • Только встраиваем свой код

public abstract class TestCase extends Assert implements Test {
  ....
  protected void runTest() throws Throwable {
  }
  protected void setUp() throws Exception {
  }
  protected void tearDown() throws Exception {
  }
}
					

Как встроить свой код

  • Шаблон “Фабрика” (Factory)
  • Service locator (JNDI)

Service locator


MovieFinder finder = (MovieFinder) 
		ServiceLocator.getService("MovieFinder");

Dependency Injection

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

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

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

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

    return allMovies;
  }
  • Определим интерфейс

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

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


public class MovieLister {
  
  private MovieFinder finder;

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

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

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

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

Dependency Injection

Решение

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

Dependency Injection

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


@Component
public class ColonDelimitedMovieFinder implements MovieFinder {
}

@Component
public class MovieLister {
  @Autowired
  private MovieFinder finder;
}
	

Dependency Injection

Было/Стало


public class MovieLister {
  private MovieFinder finder;

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

@Component
public class MovieLister {
  @Autowired
  private MovieFinder finder;
}

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


@Autowired
public class ColonDelimitedMovieFinder implements MovieFinder {
}

@Component
public class MovieLister {

  private MovieFinder finder;

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

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

  • Удобно для тестирования
  • Лучше видны зависимости
  • Не удобно делать слишком много зависимостей

Spring framework

Spring

Все проекты

Spring framework

  • Dependency Injection
  • Aspect-Oriented Programming including Spring's declarative transaction management
  • Spring MVC web application and RESTful web service framework
  • Foundational support for JDBC, JPA, JMS
  • Much more…

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

  • IoC-контейнер - любая реализация DI, например Spring
  • BeanFactory (Фабрика бинов) - Ассемблер, сборщик
  • Bean (Бин) - объект системы, содержащий логику

Бин (Bean)

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

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

Spring-ом, т.е. IoC-контейнером

Как создать Bean

Может быть помечен аннотацией

  • @Named (JSR-330)
  • @Component (Spring)
  • @Service (Spring)
  • и много других в Spring

Как создать Bean


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

Как создать Bean

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


@Configuration
public class MovieFinderConfiguration {

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

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

Как создать Bean

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


@Configuration
public class DatabaseConfiguration {

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

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

  • @Inject (JSR-330)
  • @Autowired (Spring)

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

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


@Component
public class MovieLister {
  
  @Autowired
  private MovieFinder finder;
}
	

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

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


@Component
public class MovieLister {

  private MovieFinder finder;

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

Внедрение Bean

Если внедряется один бин:

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

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

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


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

@Component
public class DirectorsService {
  @Autowired
  private List<Director> allDirectors;

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

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

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


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

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

@Component
public class MovieLister {
  @Autowired @Qualifier("csvFinder")
  private MovieFinder finder;

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

Имя Bean

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

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

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


@Component
public class MovieLister {...

  @Autowired
  private BeanFactory factory;

  private String beanName;

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

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

  • @PostConstruct
  • @PreDestroy

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

@PostConstruct

  • Вызван конструктор
  • Внедрены все зависимости
  • Сразу после этого - вызов метода

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

@PostConstruct


@Component
public class CachingMovieLister {

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

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

@PreDestroy

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

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

@PreDestroy

Зависит от Scope


@Component
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");
  }
}

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


@Component
public class SomeBean {

  @Value("#{systemProperties['influxHost']}")
  private String influxHost;
}

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

    Аргумент VM:
  • -DinfluxHost="localhost"
  • Переменную окружения

Аналог: System.getProperty("influxHost")

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


@Component
public class InfluxDAO {
  public InfluxDAO(
  	@Value("${influx.host}") String influxHost...
}

Конфигурация задаётся в 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...

Spring MVC

Spring MVC

  • Model - инкапсуляция данных приложения
  • View - отображение данных (сделает HTML на выходе)
  • Controller - обработка запроса, создание Модели и передача в Вид

Dispatcher Servlet

Контроллер


@Controller
public class HistoryController {
    
    @RequestMapping(path = "/history/{client}")
    public ModelAndView indexLast864(
			@PathVariable("client") String client,
			@RequestParam(name = "count") int count)

        ...
        Map model = new HashMap<>(d.asModel());
        model.put("client", client);

        return new ModelAndView("history", model, HttpStatus.OK);
    }

Вид - history.jsp


...
<div class="container">
	<br>
    <h1>Performance data for "${client}"</h1>
    <h3>
    	<a class="btn btn-success btn-lg" href="/">Client list</a>
    </h3>
    <h4 id="date_range"></h4>

${client} - подстановка значения из модели

Контроллер - 2


@Controller
public class HelloController {
   @RequestMapping(value = "/hello", method = RequestMethod.GET)
   public String printHello(ModelMap model) {
      model.addAttribute("message", "Hello Spring MVC Framework!");
      return "hello";
   }
}