Avoiding framework-imposed circular dependencies in Guice

Viewed 1641

Please note: Although this question specifically calls out Swing, I believe this to be a pure Guice (4.0) question at heart and which highlights a potential generic issue with Guice and other opinionated frameworks.


In Swing, you have your application UI, which extends JFrame:

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

Your app (JFrame) needs a menubar:

// 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

And your menu items need action listeners, many of which will updated your app's 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)
    }
})

And so there is intrinsically a circular dependency:

  1. Your app/JFrame needs a JMenuBar instance
  2. Your JMenuBar needs 0+ JMenus and JMenuItems
  3. Your JMenuItems (well, the ones that will update your JFrame's contentPane') need action listeners
  4. And then, in order to update the JFrame's contentPane, these action listeners need to reference your JFrame

Take the following module:

// Pseudo-code
class MyModule extends AbstractModule {
    @Override
    void configure() {
        // ...
    }

    @Provides
    MyApp providesMyApp(JMenuBar menuBar) {
        // Remember MyApp extends JFrame...
        return new MyApp(menuBar, ...)
    }

    @Provides
    JMenuBar providesMenuBar(@Named('widgetsListener') ActionListener widgetsMenuActionListener) {
        JMenuBar menuBar = new JMenuBar()

        JMenu fileMenu = new JMenu('File')

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

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

        return menuBar
    }

    @Provides
    @Named('widgetsListener')
    ActionListener providesWidgetsActionListener(Myapp myApp, @Named('widgetsPanel') JPanel widgetsPanel) {
        new ActionListener() {
            @Override
            void actionPerformed(ActionEvent actionEvent) {
                // Here is the circular dependency. MyApp needs an instance of this listener to
                // to be instantiated, but this listener depends on a MyApp instance in order to
                // properly update the main content pane...
                myApp.contentPane.removeAll()
                myApp.contentPane.add(widgetsPanel)
            }
        }
    }
}

This will produce circular dependency errors at runtime, such as:

Exception in thread "AWT-EventDispatcher" com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) Tried proxying com.me.myapp.MyApp to support a circular dependency, but it is not an interface.
    while locating com.me.myapp.MyApp

So I ask: what's the way of circumventing this? Does Guice have an API or extension library for dealing with this sort of problem? Is there a way to refactor the code to break the circular dependency? Some other solution?


Update

Please see my guice-swing-example project on GitHub for a SSCCE.

3 Answers
Related