6 Апрель 2008 г.

Знакомимся: guice - IoC контейнер от google

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

Одним из наиболее популярных (да и что греха таить - лучших) способов уменьшения связности кода является использование паттерна Inversion Of Control (Инверсия управления). Очень часто данный паттерн описывают основным правилом Голливуда: "не звони мне, я сам тебе позвоню". При всей философскости данного описания оно является правильным.

Без философии же данный паттерн основывается следующим образом: взаимодействовать должны не реализации (классы), а абстракции (интерфейсы). Не должно быть такого (без фанатизма конечно же) когда класс зависит от класса. Класс должен зависить от интерфейсов. Возникает вопрос: а когда создавать инстанцы интерфейсов, ведь понятно, что в рантайме то интерфейсов не будет, будут классы. Так вот вся прелесть IoC проявляется именно в этом. Вы в одном месте описываете правила по которым будут созданы объекты для реализации интерфейсов. Остальное за вас сделет используемая IoC подсистема, в частности IoC-контейнер.

В мире Java для реализации паттерна IoC очень часто используются специальные средства, именуемые
IoC-контейнерами.
Идея состоит в следующем: все интерфейсы инстанцируются специальной фабрикой (контейнером) по некоторому описанию. Описание собственно указывает контейнеру какой интерфейс в каком случае каким классом инстанцировать.

Наиболее популярными IoC контейнерами в мире Java являются Spring, HiveMind, Tapestry-ioc (следующая реинкарнация HiveMind), PicoConteyner и другие. Но мы сегодня поговорим о Guice - легковесном IoC-контейнере от Google.

Скачать Guice можно как не трудно догадаться с code.google.com. Текущая версия - 1.0. На сайте проекта так-же присутствует ссылка на документацию (в google-doc) но я рекомендую сразу качать pdf - там информация более полная.

Сначала расскажу о том, что понравилось в Guice, потом кратко покажу основные приемы работы.

Итак понравилось в guice:
1. отсутствие xml-конфигов. Очень напрягает читать портянки конфигов да еще и разбитые по нескольким файлам (зачастую даже без всякой логики). Очень сильно грешил этим Spring до 2.5. В guice все конфигурируется в ява коде. Места инъекций (Guice реализует разновидность IoC под названием Dependency Injection - инъекция зависимостей) отмечаются специальным атрибутом - @Inject. Можно также делать именованые инъекции и инъекции с помощью своих аннотаций. Далее вся конфигурация реализуется в одном классе вызовом специальных методов.

2. Модульность. С помощью guice легко построить приложение состоящее из нескольких модулей (я сейчас работаю над реализацией плагинной шины в основе которой лежит Guice). Модуль с точки зрения Guice - отдельный файл, описывающий конфигурацию. Соответственно можно создавать наборы модулей, заменять одни модули другими и делать все это в рантайме.

3. Широкое использование аннотаций. Можно создавать свои аннотации, аннотации с параметрами, использовать втроенную аннотацию @Named("param") для описания точек расширения. Соответственно такой код гораздо читабельнее нежели xml-ная портянка.

4. Инъекцию можно осуществлять на уровне конструктора, метода или непосредственно поля. Причем в отличие от Spring метод не обязательно должен называться setParam. Он может называться как угодно, но быть аннотирован @Inject.

5. Можно использовать свои фабрики для создания объектов, которые будут инъектироваться. В терминах Guice такая фабрика называется провайдером. В частности в документации по Guice приведен пример создания классов с использованием JNDI.

6. Guice - довольно быстрая штука.

Теперь попробуем сделать что-нибудь простое с помощью Guice. Пусть у нас будет простая задачка - написать приложение, копирующее строку на экран. Да, согласен задачка решается то в 2 строки, но эта ее простота очень поможет наглядно продемонстрировать работу с Guice.

Итак, мы напишем класс "читатель", который будет возвращать нам данные. Естественно он будет не просто так, а реализовывать соответствующий интерфейс. Мы же хотим получать данные по-разному. Точно также напишем класс "писатель". Который будет выводить данные на экран. В копировщике же у нас в полном соответствии паттерну IoC будут взаимодействовать не "читатель" с "писателем", а интерфейс "читателя" и интерфейс "писателя". Итак, взглянем на код:

Интерфейс читателя:
package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface IReader {

    public String read();

}


Примитивный читатель:
package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IReader;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleReader implements IReader {



    public static final String SIMPLE_STRING = "SimpleString";

   

    public String read() {

        return SIMPLE_STRING;

    }   

}

 


Интерфейс писателя:
package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface IWriter {

    public void write(String data);

}

 


Примитивный писатель:
package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IWriter;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleWriter implements IWriter {



    public void write(String data) {

        System.out.println(data);

    }

}


Интерфейс копировщика:
package org.google.guice.demo.modules.copier;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public interface ICopier {

    public void copy();

}


Реализация копировщика:
package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.ICopier;

import org.google.guice.demo.modules.copier.IReader;

import org.google.guice.demo.modules.copier.IWriter;



import com.google.inject.Inject;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleCopier implements ICopier {



    private IReader reader;

    private IWriter writer;

   

    @Inject

    public SimpleCopier(IReader reader, IWriter writer) {

        this.reader = reader;

        this.writer = writer;

    }

       

    public void copy() {

        writer.write(reader.read());

    }

}

 



В коде копировщика мы видим перед конструктором аннотацию @Inject. Она уведомляет Guice о том, где именно будет происходить инъекция объектов. В данном случае, при создании экземпляра класса SimpleCopier ему в конструктор будут переданы построенные читатель и писатель. Если бы мы использовали инъекцию через метод - вызывался бы метод, отмеченный аннотацией @Inject. Выглядело бы это так:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.ICopier;

import org.google.guice.demo.modules.copier.IReader;

import org.google.guice.demo.modules.copier.IWriter;



import com.google.inject.Inject;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleCopier implements ICopier {



    private IReader reader;

    private IWriter writer;

   

    public void copy() {

        writer.write(reader.read());

    }

   

    @Inject

    public void setReader(IReader reader) {

        this.reader = reader;

    }

   

    @Inject

    public void injectWriter(IWriter writer) {

        this.writer = writer;

    }

}


как видим нет никаких ограничений на наименования методов.

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

package org.google.guice.demo.modules.copier;



import org.google.guice.demo.modules.copier.impl.SimpleCopier;

import org.google.guice.demo.modules.copier.impl.SimpleReader;

import org.google.guice.demo.modules.copier.impl.SimpleWriter;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class CopyModule extends AbstractModule {



    @Override

    protected void configure() {

        bind(IReader.class)

            .to(SimpleReader.class)

            .in(Scopes.SINGLETON);



        bind(IWriter.class)

            .to(SimpleWriter.class)

            .in(Scopes.SINGLETON);

       

        bind(ICopier.class)

            .to(SimpleCopier.class)

            .in(Scopes.SINGLETON);

    }

}


Ну и наконец - метод main, который все это дело запустит:

package org.google.guice.demo;



import org.google.guice.demo.modules.copier.CopyModule;

import org.google.guice.demo.modules.copier.ICopier;



import com.google.inject.Guice;

import com.google.inject.Injector;



/**

 * <p>Created by 01.04.2008

 *   @author Samolisov Pavel

 */


public class Main {

    public static void main(String[] args) {

        Injector injector = Guice.createInjector(new CopyModule());

        ICopier service = injector.getInstance(ICopier.class);     

       

        service.copy();    

    }

}

 


Как видим - все просто: получили модуль, дернули из него инстанс для нужного интерфейса. Запустили. И ненадо писать много кода для создания классов, от которых зависит данный (это здесь приложение простое). Всю работу за нас делает Guice.

Пойдем дальше. Инстанцирование по типу интерфейса это хорошо, но что делать, когда нам необходимо в разных местах модуля использовать разные реализации интерфейса? Здесь нам на помощь приходят аннотации. Причем можно использовать стандартную, входящую в Guice аннотацию @Named с параметром. А можно написать свои аннотации. Каждый способ имеет право на жизнь: свои аннотации читать понятнее, в случае же @Named легко забыть какое значение параметра за что отвечает. Опять же, когда много аннотаций - их много надо писать )) В общем дилема. Я покажу простой пример - пусть писателя в наш копир мы будет инъектировать с помощью своей аннотации @Writer. Напишем сначала саму аннотацию:
package org.google.guice.demo.modules.copier.annotations;



import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



import com.google.inject.BindingAnnotation;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */




@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.FIELD, ElementType.PARAMETER})

@BindingAnnotation

public @interface Writer {}

 


Обратите внимание: аннотации при инъекции можно вешать только на поля или на параметры метода. Это очень важно! Написать, то мы можем любую аннотацию, но если повесить ее на метод или что еще хуже - конструктор - получим эксепшн при работе контейнера.

Ну а реализация аннотации как видим проста - все дело в волшебных пузырьках, ака @BindingAnnotation.

Изменения в коде класса SimpleCopier небольшие:

    @Inject 

    public void injectWriter(@Writer IWriter writer) {

        this.writer = writer;

    }


Ну и самое главное - изменения в конфигурации (класс CopyModule):
bind(IWriter.class)

    .annotatedWith(Writer.class)

    .to(SimpleWriter.class)

    .in(Scopes.SINGLETON);

 


Теперь попробуем все то же самое сделать, используя стандартную для Guice аннотацию @Named

Изменения в SimpleCopier:
    @Inject 

    public void injectWriter(@Named("writer") IWriter writer) {    

        this.writer = writer;

    }


Изменения в CopyModule:
    bind(IWriter.class)

        .annotatedWith(Names.named("writer"))

        .to(SimpleWriter.class)

        .in(Scopes.SINGLETON);

 


Точка расширения, отмеченная аннотацией @Named попадает в реестр Names. Данный реестр, как мы видем используется в модуле при создании конфигурации.

Еще одной приятной особенностью Guice является возможность инъекции констант. Для примера - будем передавать читателю строку, которую он долже возвращать в виде константы. Доработаем читателя:

package org.google.guice.demo.modules.copier.impl;



import org.google.guice.demo.modules.copier.IReader;



import com.google.inject.Inject;

import com.google.inject.name.Named;



/**

 * <p>Created by 07.04.2008

 *   @author Samolisov Pavel

 */


public class SimpleReader implements IReader {

   

    @Inject

    @Named("message")

    private String message;

   

    public String read() {

        return message;

    }   

}


И добавим такие строки в конфигурацию модуля:
bindConstant()

        .annotatedWith(Names.named("message"))

        .to("Hi! I'm annotated message!");


Тоже очень удобная возможность. Позволяет легко выносить константы в файл конфигурации модуля. Вся конфигурация отныне в одном месте.

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


Понравилось сообщение - подпишись на блог

1 комментариев:

Анонимный комментирует...

Спасибо Паша за статью, редко встретишь человека способного на пальцах рассказать или поделиться о чем то новом без выпендрежа