вторник, 22 октября 2013 г.

IoC Spring Framework

Поговорим о IoC Spring Framework. Дело в том, что меня совсем не радует его использование во всех Java проектах, в которых я имел честь участвовать.

Начнем с того, что сам по себе инструмент не плох. Но вот его использование - это зачастую ужас. Чтобы что-то правильно использовать надо хорошо понимать для чего эта штука создана и где она помогает и где мешает. Хочу попытаться это сформулировать.


Для чего нужен Spring IoC?
Документация сообщает нам, что нужен для работы с "dependency injection"(DI). И я с этим полностью согласен.

Что такое DI?
Это когда класс принимает зависимости, как параметры конструктора или в метод-конструктор или заставляет пользователя проставить зависимость через вызов специального метода после конструирования объекта, вместо того, чтобы самому конструировать нужные объекты в теле конструктора или методов.

Для чего нужно DI?
DI помогает нам использовать класс с разными имплементациями зависимостей, если это необходимо. Как частный случай, помогает тестировать наш класс отдельно от зависимостей пробрасывая в него специальные mock-имплементации.

Вот. Остановимся пока на этом. Важная мысль, что DI нужен для замены имплементации или для разбиения кода на логические составляющие для тестирования. При этом у DI есть обратная сторона: необходим код для инициализации, и в целом это усложняет устройство кода, а значит будут накладные расходы на сопровождение, а так же сам по себе код будет дороже. Именно поэтому я считаю, что DI нужно использовать только для тех частей, которые планируется заменять или разбивать на юниты для тестирования. То есть только там, где выгоды от такой организации кода будет больше чем недостатков.

Часто же многие программисты считают DI неким абсолютным добром по умолчанию и пытаются использовать его как можно больше и везде. Прокидывание всего кроме константных строк (и то не всегда) и логгера (ну этот, честно говоря я не видел, чтобы прокидывали, но кто знает...) во все классы становится самоцелью. Это начинает использоваться там, где у класса или интерфейса есть только одна реализация (нам не на что заменять) и где вообще юнит-тестирование не применяется, то есть никаких выгод нет вообще. Одни только недостатки.

Это о самом DI. Теперь поговорим о IoC Spring Framework. Для этого давайте сначала возьмем простенький примерчик.

Допустим у нас есть некий класс, который должен получить список пользователей из MySQL. Если мы не собираемся менять базу данных или тестировать наш код без настоящей базы данных, то самый простой пусть написать такой класс (код работы с базой данных упрощен специально):

public class SimpleUsers implements Users {
    private final MySqlDriver driver;

    public SimpleUsers(String host, int port, String dbName) {
        driver = new MySqlDriver(host, port, dbName);
    }

    public Collection<User> list() {
        Collection<Row> rows = driver.query(
            "select * from users"
        );
        List<User> users = new ArrayList<User>();
        for (Row row : rows) {
            User user = new User(
                row.getInt("id"),
                row.getString("name"),
                row.getString("email")
            );
            users.add(user);
        }
        return users;
    }
}

Как этот код смотрелся бы если бы мы захотели поддержать несколько реализаций базы данных или захотели бы заменить реализацию работы с базой данных, чтобы протестировать логику работы самого класса без настоящего запроса к базе данных:

public class Users implements Users {
    private final DbDriver driver;
    private final String query;
    private final RowMapper<User> userRowMapper;

    public Users(DbDriver driver,
                   String query,
                   RowMapper<User> userRowMapper) {
        this.driver = driver;
        this.query = query;
        this.userRowMapper = userRowMapper;
    }

    public Collection<User> list() {
        Collection<Row> rows = driver.query(query);
        List<User> users = new ArrayList<User>();
        for (Row row : rows) {
            User user = userRowMapper.map(row);
            users.add(user);
        }
        return users;
    }
}

Видно, что теперь мы пробрасываем и сам запрос, и получение результатов из него. Код стал с одной стороны сложнее, а с другой  гибче.

Как этот код обычно используется вместе с Spring IoC:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations"
                  value="classpath:sample.properties"/>
    </bean>

    <bean id="dbDriver"
          class="org.example.MySqlDriver">
        <constructor-arg value="${db.host}"/>
        <constructor-arg value="${db.port}"/>
        <constructor-arg value="${db.name}"/>
    </bean>

    <bean id="userRowMapper"
          class="org.example.UserRowMapper"/>

    <bean id="users"
          class="org.example.Users">
        <constructor-arg ref="dbDriver"/>
        <constructor-arg>
            <value>select * from users</value>
        </constructor-arg>
        <constructor-arg ref="userRowMapper"/>
    </bean>
</beans>

Теперь посмотрим как этот код будет использоваться. Можно ли им воспользоваться из Java не используя Spring IoC? Да легко!

public class ApplicationContext {
    private Properties properties;
    private Users users;
    private MySqlDriver dbDriver;
    private UserRowMapper userRowMapper;

    public ApplicationContext(File propertiesFile)
     throws IOException {
        properties = new Properties();
        properties.load(new FileInputStream(propertiesFile));
    }

    private MySqlDriver getDbDriver() {
        if (dbDriver == null) {
            dbDriver = new MySqlDriver(
                    properties.getProperty("db.host", "localhost"),
                    Integer.parseInt(properties.getProperty("db.port", "3306")),
                    properties.getProperty("db.name", "sample")
            );
        }
        return dbDriver;
    }

    private RowMapper<User> getUserRowMapper() {
        if (userRowMapper == null) {
            userRowMapper = new UserRowMapper();
        }
        return userRowMapper;
    }

    public Users getUsers() {
        if (users == null) {
            users = new Users(
                    getDbDriver(),
                    properties.getProperty(
                        "list.users.query",
                        "select * from users"
                    ),
                    getUserRowMapper()
            );
        }
        return users;
    }
}

Думаю, что тут все просто и понятно. Мы просто создаем нужный класс и прокидываем ему зависимости в коде инициализации приложения.

В чем у нас тут разница по существу? Один язык заменили на другой (Java на XML). В случае Spring IoC появляется дополнительная зависимость. А в месте с ней часть ошибок перелезает из времени компиляции во время выполнения, приложение будет инициализироваться медленнее из-за разбора и интерпретации XML. А есть ли преимущества в использовании Spring IoC? Ну есть одно: XML файл можно сделать внешним и менять его без перекомпиляции приложения, дав возможность пользователю выбрать имплементацию драйвера базы данных. Что ж это зачастую полезно и перевешивает упомянутые выше недостатки.

Подытожу выводы:
  • DI по умолчанию — зло, так так увеличивает сложность написания приложений
  • DI может быть полезно, только если мы подменяем реализацию части компонентов для нужд приложения или для разбиения на юниты для тестирования
  • код с DI можно и лучше инициализировать из Java
  • если мы хотим дать возможность конфигурировать часть компонент пользователю без перекомпиляции приложения, то инициализацию таких компонент (и только их, без лишнего мусора) можно реализовать с помощью таких библиотек, как Spring IoC
К сожалению, я видел только одно приложение, где Spring IoC использовался согласно этим принципам. Это было приложение для нагрузочного тестирования, где пользователь поставлял свой код в jar-файле и писал Spring контекст для его инициализации (приложение запускало пользовательский код во многих потоках и следило за ходом выполнения и составляло много отчетов). Во всех остальных случаях использование Spring IoC только усложняло код, раздувало его, приводило к трудно исправляемым ошибкам и к ошибкам времени выполнения.

Продолжение

Комментариев нет:

Отправить комментарий