在Guice中避免框架强加的循环依赖

时间:2016-03-16 17:29:38

标签: java swing guice

请注意:虽然这个问题特别提到了Swing,但我相信这是一个纯粹的Guice( 4.0 )问题。这突出了Guice和其他见解框架的潜在通用问题。

在Swing中,您拥有应用程序UI,该UI扩展了JFrame

// Pseudo-code
class MyApp extends JFrame {
    // ...
}

您的应用(JFrame)需要一个菜单​​栏:

// Pseudo-code
JMenuBar menuBar = new JMenuBar()

JMenu fileMenu = new JMenu('File')

JMenu manageMenu = new JMenu('Manage')
JMenuItem widgetsSubmenu = new JMenuItem('Widgets')
manageMenu.add(widgetsSubmenu)

menuBar.add(fileMenu)
menuBar.add(manageMenu)

return menuBar

您的菜单项需要动作听众,其中许多都会更新您应用的contentPane

widgetsSubmenu.addActionListener(new ActionListener() {
    @Override
    void actionPerformed(ActionEvent actionEvent) {
        // Remove all the elements from the main contentPane
        yourApp.contentPane.removeAll()

        // Now add a new panel to the contentPane
        yourApp.contentPane.add(someNewPanel)
    }
})

因此本质上存在循环依赖:

  1. 您的应用/ JFrame需要JMenuBar个实例
  2. 您的JMenuBar需要0 + JMenusJMenuItems
  3. 您的JMenuItems(以及那些会更新您JFrame的{​​{1}}'的人)需要行动听众
  4. 然后,为了更新contentPane的{​​{1}},这些动作听众需要引用您的JFrame
  5. 采取以下模块:

    contentPane

    这将在运行时产生循环依赖性错误,例如:

    JFrame

    所以我问:绕过这个是什么方式? Guice是否有API或扩展库来处理这类问题?有没有办法重构代码来打破循环依赖?其他一些解决方案?

    更新

    请参阅我在GitHub上的guice-swing-example项目获取SSCCE。

3 个答案:

答案 0 :(得分:4)

有几种技术可用于重构循环依赖关系,使其不再是循环的,Guice docs recommend doing that whenever possible。如果无法做到这一点,错误消息Guice会以Guice方式提供一般性解决方法的提示:使用接口将API与您的实现分开。

严格来说,只需要为圆圈中的一个类执行此操作,但它可以提高模块性并简化编写测试代码以便为大多数或每个类执行此操作。创建一个接口MyAppInterface,在其中声明您的动作侦听器需要直接调用的每个方法(getContentPane()似乎是您发布的代码中唯一的一个),并MyApp实现它。然后将MyAppInterface绑定到模块配置中的MyApp(在您的情况下只需更改providesMyApp的返回类型应该这样做),声明操作侦听器提供程序采用MyAppInterface ,并运行它。您可能还需要将代码中的其他位置从MyApp切换为MyAppInterface,具体取决于详细信息。

这样做可以让Guice自己打破循环依赖。当它看到循环依赖时,它将生成一个新的零依赖实现类MyAppInterface,它充当代理包装器,将其实例传递给动作侦听器提供程序,从那里填充依赖链直到它可以创建一个真正的MyApp对象,然后它会将MyApp对象粘贴到Guice生成的对象中,该对象会将所有方法调用转发给它。

虽然我上面使用了MyAppInterface,但更常见的命名模式是实际使用接口的MyApp名称,并将现有的MyApp类重命名为MyAppImpl

请注意,Guice的自动循环依赖性中断方法要求在初始化完成之前不要在生成的代理对象上调用任何方法,因为它不会有包装对象将它们转发到然后

编辑:我没有准备好让Groovy或Gradle尝试运行你的SSCCE进行测试,但我认为你已经非常接近打破了这个圈子。使用DefaultFizzClient@Provides类和每个@Singleton方法添加注释,并从menuBar中删除provideExampleApp参数,我认为应该使其有效。

@Singleton:当只有一个类的实例应该被创建时,应该将类或提供者方法标记为@Singleton。当它被注入多个不同的地方或多次请求时,这很重要。使用@Singleton,Guice创建一个实例,保存对它的引用,并且每次都使用该实例。没有,它为每个引用创建一个新的单独实例。从概念上讲,这是一个问题,你是否正在定义"如何制作X"或者"这里是 X"。在我看来,您创建的UI元素属于后一类 - 应用程序的单一菜单栏,而不是任意菜单栏等。

删除menuBar:您已经注释了该方法中使用它的唯一一行。只需从方法声明中删除参数即可。至于如何让菜单栏进入应用程序,已经由addMenuToFrame方法与requestInjection(this)调用相结合。

如果做出这些改动,也许可以帮助我们完成Guice将要经历的逻辑:

  1. 使用ExampleAppModule创建进样器时,它会调用模块的configure()方法。这会设置一些绑定并告诉Guice,无论何时完成所有绑定设置,它都应该扫描ExampleAppModule实例,以获取用@Inject注释的字段和方法并将其填入。< / LI>
  2. configure()返回,并且在绑定设置完成后,Guice尊重requestInjection(this)电话。它会扫描并发现addMenuToFrame已注明@Inject。检查该方法后,Guice发现需要ExampleApp实例和JMenuBar实例才能调用它。
  3. Guice寻找制作ExampleApp实例的方法并找到provideExampleApp方法。 Guice检查该方法并发现需要FizzClient实例来调用它。
  4. Guice寻找制作FizzClient实例的方法,并找到绑定到DefaultFizzClient的类。 DefaultFizzClient有一个默认的no-args构造函数,因此Guice只是调用它来获取实例。
  5. 获得FizzClient个实例后,Guice现在可以致电provideExampleApp。它这样做,从而获得ExampleApp实例。
  6. Guice仍然需要JMenuBar才能调用addMenuToFrame,因此它会寻找一种方法来创建providesMenuBar方法。 Guice检查该方法,并指出它需要ActionListener名为&#34; widgetsMenuActionListener&#34;。
  7. Guice寻找一种方法来创建ActionListener名为&#34; widgetsMenuActionListener&#34;并找到providesWidgetsMenuActionListener方法。 Guice检查该方法并发现它需要一个ExampleApp实例和一个JPanel名为&#34; widgetsPanel&#34;。它已经有一个ExampleApp实例,在第5步中获得,它从标记为@Singleton的内容中获取,因此它重用相同的实例,而不是再次调用provideExampleApp
  8. Guice寻找一种方法来制作JPanel名为&#34; widgetsPanel&#34;并找到providesWidgetPanel方法。此方法没有参数,因此Guice只是调用它并获取它需要的JPanel
  9. 除了已经制作的JPanel之外,已经获得了具有正确名称的ExampleApp,Guice会调用providesWidgetsMenuActionListener,从而获取一个名为ActionListener的实例。
  10. 获得一个名称正确的ActionListener后,Guice会调用providesMenuBar,从而获得JMenuBar个实例。
  11. 最后,终于获得了ExampleApp实例和JMenuBar实例,Guice调用addMenuToFrame,将菜单添加到框架中。
  12. 现在发生这一切后,您getInstance(ExampleApp)的{​​{1}}电话会被执行。 Guice检查,发现它已经标记为main的源中有ExampleApp实例,并返回该实例。
  13. 仔细研究了所有这些内容后,似乎@Singleton仅在@Singleton上才是必要的,但我认为其他一切也是有意义的。

答案 1 :(得分:1)

好吧,如果你确实有一个循环依赖,它不能是一个创建循环依赖 - 因为否则你将无法创建那个循环的对象。

您可以通过请求模块注入来执行创建后配置。例如,您可以使用此功能将JMenuBar添加到JFrame

class MyModule extends AbstractModule {
  @Override void configure() {
    requestInjection(this);
  }

  @Inject void addMenuToFrame(JFrame frame, JMenuBar menu) {
    frame.setMenuBar(menu);
  }
}

我没有对此进行测试以确定它是否有效(因为您没有提供足够的代码来尝试它) - 所以您可能需要尝试在哪里正确的地方打破当前周期以便随后像这样加入它 - 但这样的事情对你有用。

我已经回答了一些关于注入像这样的guice模块的其他问题 - maybe have a look at those too,因为其他人也有一些很好的提示,而不是我在这里尝试并且没有正确地重复它们。 / p>

我一直在玩这个以帮助我的理解,并且我发现你需要将你的JFrameJMenuBar绑定在单一范围内才能使其有效工作,因为方法注入是在创建注入器时发生的,而不是在后续创建JFrameJMenuBar实例时(可能很明显)。

答案 2 :(得分:0)

我无法根据您的代码测试答案,因为我不知道(我也不想学习)Groovy。

但基本上,当你有循环依赖时,打破它的最好方法是使用Provider

package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {

  @Singleton public final static class A {
    B b;
    @Inject A(B b) { this.b = b; }
    void use() { System.out.println("Going through"); }
  }

  @Singleton public final static class B {
    A a;
    @Inject B(A a) { this.a = a; }
    void makeUseOfA() { a.use(); }
  }

  public static void main(String[] args) {
    Guice.createInjector().getInstance(B.class).makeUseOfA(); // Produces a circular dependency error.
  }
}

然后可以这样重写:

package so36042838;
import com.google.common.base.*;
import com.google.inject.Guice;
import javax.inject.*;
public class Question {

  @Singleton public final static class A {
    B b;
    @Inject A(B b) { this.b = b; }
    void use() { System.out.println("Going through"); }
  }

  @Singleton public final static class B {
    Supplier<A> a;
    @Inject B(Provider<A> a) { this.a = Suppliers.memoize(a::get); } // prints "Going through" as expected
    void makeUseOfA() { a.get().use(); }
  }

  public static void main(String[] args) {
    Guice.createInjector().getInstance(B.class).makeUseOfA();
  }
}