Guice Singleton Static Injection Pattern

Я новичок в Google Guice и концептуально понимаю внедрение зависимостей, но сталкиваюсь с проблемами, пытаясь включить его в свое приложение. Мой конкретный вопрос касается объектов Singleton. Вот пример:

Во-первых, мой класс Module, который связывает тяжелый интерфейс Singleton Connection со своей реализацией.

public class MyModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Connection.class).to(MyConnection.class).asEagerSingleton();
    }
}

Теперь в моем основном методе я создаю экземпляр своего сервера приложений и внедряю Connection:

public class MyApplication {
    @Inject
    public MyApplication(Connection cxn) {

    }

    public static void main(String[] args) {
        Injector injector = Guice.createInjector(new MyModule());
        MyApplication app = injector.getInstance(MyApplication.class);
        // Start application, add ShutdownHook, etc...
    }
}

Пока все хорошо... Теперь у меня есть несколько классов DAO, которые используют мой объект Connection, но извлекаются с помощью статических методов, например:

public class MyConfiguration {
    private Config conf;
    private Connection cxn; // Would like to have this injected

    private MyConfiguration(Config conf) {
        this.conf = conf;
    }

    public static MyConfiguration getConfig(String name) {
        return new MyConfiguration(cxn.getConfig(name));
    }
}

Мое первое предположение состояло в том, что я просто добавлю @Inject к cxn, но это не работает, потому что я не получаю экземпляр от Guice; это просто дает мне NPE. Как я это вижу, у меня есть 2 варианта получения объекта Connection:

  1. Предоставьте метод getConnection() в MyApplication по существу в соответствии с шаблоном локатора сервисов.
  2. Добавьте requestStaticInjection(MyConfiguration) к MyModule

Я выбрал № 2, однако документы говорят:

Этот API не рекомендуется для общего использования

Как лучше всего предоставить мой синглтон тем классам, которым он нужен, без необходимости каждый раз проходить Injector.getInstance? Что мне не хватает?


person lamarvannoy    schedule 26.02.2015    source источник
comment
Вы сами сказали, измените метод на нестатический;)   -  person Nir Alfasi    schedule 26.02.2015
comment
Я согласен с @alfasin в том, что вы теряете одну из основных причин для выполнения DI, если у вас есть статические вызовы, которые нельзя вводить по-другому. Почему вы не использовали одноэлементный шаблон без DI для MyConfiguration?   -  person JasonOfEarth    schedule 26.02.2015
comment
Мой пример несколько надуман для пояснения. В моем приложении конфигурация на самом деле является соединением с базой данных графа. Cxn.getConfig на самом деле является обходом графа, который находит узел с заданным именем. Объект MyConfiguration представляет собой оболочку вокруг этого узла, предоставляющую специфичную для домена информацию. Я планировал использовать его свободно: MyConfiguration.getConfig(x).getName() и т. д. Допустим, я удалил экземпляр статического объекта, должен ли я использовать Injector.getInstance во всем моем коде всякий раз, когда он мне нужен? Как обрабатывать параметры конструктора, которые не следует вводить?   -  person lamarvannoy    schedule 26.02.2015


Ответы (1)


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

По сути, «это черепахи до самого низа»! Каждая зависимость вашего класса должна быть внедрена. Если MyApplication нужен объект MyConfiguration, он должен просто принять объект MyConfiguration в качестве параметра конструктора и не беспокоиться о том, как он был сконструирован.

Это не означает, что вы никогда не сможете использовать new вручную, но вы должны зарезервировать это для объектов типа значения, которые не имеют внешних зависимостей. (И в этих случаях я бы сказал, что в любом случае вам лучше использовать статический фабричный метод, чем публичный конструктор, но это не относится к делу.)

Теперь есть несколько способов сделать это. Один из способов — разделить MyConfiguration на множество мелких кусочков, чтобы вместо myConfiguration.getConfig("x") вы делали @Inject @Configuration("x") String или что-то в этом роде. В качестве альтернативы вы можете сделать сам MyConfiguration инжектируемым, а затем предоставить ему методы доступа для частей. Правильный ответ в некоторой степени зависит от типа данных, которые вы пытаетесь смоделировать — сделайте зависимости слишком мелкими, и ваши привязки могут стать сложными в обслуживании (хотя есть способы сделать это лучше); сделайте зависимости слишком грубыми, и вы усложните тестирование (например: что проще, предоставить только конфигурацию «x», которая нужна тестируемому классу, или создать конфигурацию всего приложения?).

Вы даже можете сделать оба:

/** Annotates a configuration value. */
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
  String value();
}

/** Installs bindings for {@link MyConfiguration}. */
final class MyConfigurationModule extends AbstractModule {
  @Override protected void configure() {}

  @Provides
  @Singleton
  MyConfiguration provideMyConfiguration() {
    // read MyConfiguration from disk or somewhere
  }

  @Provides
  @Config("x")
  String provideX(MyConfiguration config) {
    return config.getConfig("x").getName();
  }
}

// elsewhere:

/** The main application. */
final class MyApplication {
  private final String xConfig;

  @Inject MyApplication(@Config("x") String xConfig) {
    this.xConfig = xConfig;
  }

  // ...
}

Вы можете использовать аналогичный подход в модульных тестах:

/** Tests for {@link MyApplication}. */
@RunWith(JUnit4.class)
public final class MyApplicationTest {
  // Note that we don't need to construct a full MyConfiguration object here
  // since we're providing our own binding, not using MyConfigurationModule.
  // Instead, we just bind the pieces that we need for this test.
  @Bind @Config("x") String xConfig = "x-configuration-for-test";

  @Before public void setUp() {
    // See https://github.com/google/guice/wiki/BoundFields
    Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this);
  }

  @Inject MyApplication app;

  @Test public void testMyApp() {
    // test app here
  }
}

Внедрение зависимостей также поощряет другую передовую практику, которую я очень рекомендую, а именно разработку системы типов таким образом, чтобы недопустимые состояния не представлялись (в максимально возможной степени). Если вся конфигурация, необходимая MyApplication, передается в его конструктор, невозможно получить объект MyApplication, который не имеет допустимой конфигурации. Это позволяет вам «заранее загружать» инварианты вашего класса, что значительно упрощает рассмотрение поведения ваших объектов.

Наконец, примечание о Injector.getInstance(). В идеале вы используете Injector ровно один раз в своей программе: сразу после ее создания. То есть вы должны иметь возможность делать Guice.createInjector(...).getInstance(MyApplication.class).start() и никогда нигде не хранить ссылку на Injector. Я обычно создаю приложения, используя абстракцию Guava ServiceManager (см. также < href="https://stackoverflow.com/questions/18502650/how-to-use-guava-servicemanager-with-guice-injection">этот вопрос), поэтому единственное, что мне нужно сделать является:

public static void main(String[] args) throws Exception {
  Injector injector = Guice.createInjector(...);
  ServiceManager manager = injector.getInstance(ServiceManager.class);
  manager.startAsync().awaitHealthy();
}
person Daniel Pryden    schedule 26.02.2015