Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding framework-imposed circular dependencies in Guice

Tags:

java

swing

guice

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.

like image 843
smeeb Avatar asked Mar 16 '16 17:03

smeeb


People also ask

How do you break a circular dependency?

But circular dependencies in software are solvable because the dependencies are always self-imposed by the developers. To break the circle, all you have to do is break one of the links. One option might simply be to come up with another way to produce one of the dependencies, in order to bootstrap the process.

Does constructor injection prevent circular dependency?

Circular dependency in Spring happens when two or more beans require instance of each other through constructor dependency injections. For example: There is a ClassA that requires an instance of ClassB through constructor injection and ClassB requires an instance of class A through constructor injection.

How does circular dependency occur in DI?

A cyclic dependency exists when a dependency of a service directly or indirectly depends on the service itself. For example, if UserService depends on EmployeeService , which also depends on UserService . Angular will have to instantiate EmployeeService to create UserService , which depends on UserService , itself.

How does Guice dependency injection work?

Using GuiceIn each of your constructors that need to have something injected in them, you just add an @Inject annotation and that tells Guice to do it's thing. Guice figures out how to give you an Emailer based on the type. If it's a simple object, it'll instantiate it and pass it in.


1 Answers

There are several techniques available to refactor a circular dependency so that it is no longer circular, and Guice docs recommend doing that whenever possible. For when that is not possible, the error message Guice provided hints at the Guice way to resolve this in general: separate your API from your implementation by using an interface.

Strictly speaking it is only necessary to do this for one class in the circle, but it may improve modularity and ease of writing test code to do it for most or every class. Create an interface, MyAppInterface, declare in it every method that your action listeners need to call directly (getContentPane() seems to be the only one in the code you posted), and have MyApp implement it. Then bind MyAppInterface to MyApp in your module configuration (in your case simply changing the return type of providesMyApp should do it), declare the action listener provider to take a MyAppInterface, and run it. You might also need to switch some other places in your code from MyApp to MyAppInterface, depends on the details.

Doing this will allow Guice itself to break the circular dependency for you. When it sees the circular dependency, it will generate a new zero-dependency implementing class of MyAppInterface that acts as a proxy wrapper, pass an instance of that into the action listener provider, fill out the dependency chain from there until it can make a real MyApp object, and then it will stick the MyApp object inside Guice's generated object which will forward all method calls to it.

Though I used MyAppInterface above, a more common naming pattern would be to actually use the MyApp name for the interface and rename the existing MyApp class to MyAppImpl.

Note that Guice's method of automatic circular dependency breaking requires that you not call any methods on the generated proxy object until after initialization is complete, because it won't have a wrapped object to forward them to until then.

Edit: I don't have Groovy or Gradle ready to try to run your SSCCE for testing, but I think you are very close to breaking the circle already. Annotate the DefaultFizzClient class and each of your @Provides methods with @Singleton, and remove the menuBar parameter from provideExampleApp, and I think that should make it work.

@Singleton: A class or provider method should be marked @Singleton when only a single instance of it should be made. This is important when it is injected in multiple different places or requested multiple times. With @Singleton, Guice makes one instance, saves a reference to it, and uses that one instance every time. Without, it makes a new separate instance for each reference. Conceptually, it's a matter of whether you are defining "how to make an X" or "this here is the X". It seems to me that the UI elements you're creating fall in the latter category - the singular menu bar for your app, not any arbitrary menu bar, etc.

Removing menuBar: You've already commented out the one and only line in that method that uses it. Simply delete the parameter from the method declaration. As for how to get the menu bar into the app anyway, that is already handled by the addMenuToFrame method combined with the requestInjection(this) call.

Perhaps it might help to do a run through of the logic Guice will go through if these alterations are made:

  1. When you create the injector with an ExampleAppModule, it calls your module's configure() method. This sets up some bindings and tells Guice that, whenever it's done with all the bindings setup, it should scan the ExampleAppModule instance for fields and methods annotated with @Inject and fill them in.
  2. configure() returns and, with bindings setup complete, Guice honors the requestInjection(this) call. It scans and finds that addMenuToFrame is annotated with @Inject. Inspecting that method, Guice finds that an ExampleApp instance and a JMenuBar instance are needed in order to call it.
  3. Guice looks for a way to make an ExampleApp instance and finds the provideExampleApp method. Guice examines that method and finds that a FizzClient instance is needed to call it.
  4. Guice looks for a way to make a FizzClient instance and finds the class binding to DefaultFizzClient. DefaultFizzClient has a default no-args constructor, so Guice just calls that to get an instance.
  5. Having acquired a FizzClient instance, Guice is now able to call provideExampleApp. It does so, thereby acquiring an ExampleApp instance.
  6. Guice still needs a JMenuBar in order to call addMenuToFrame, so it looks for a way to make one and finds the providesMenuBar method. Guice examines that method and notes that it needs an ActionListener named "widgetsMenuActionListener".
  7. Guice looks for a way to create an ActionListener named "widgetsMenuActionListener" and finds the providesWidgetsMenuActionListener method. Guice examines that method and finds that it needs an ExampleApp instance and a JPanel named "widgetsPanel". It already has an ExampleApp instance, acquired in step 5, and it got that from something marked as @Singleton, so it reuses the same instance rather than calling provideExampleApp again.
  8. Guice looks for a way to make a JPanel named "widgetsPanel" and finds the providesWidgetPanel method. This method has no parameters, so Guice just calls it and acquires the JPanel it needs.
  9. Having acquired a JPanel with the right name in addition to the already-made ExampleApp, Guice calls providesWidgetsMenuActionListener, thereby acquiring a named ActionListener instance.
  10. Having acquired an ActionListener with the right name, Guice calls providesMenuBar, thereby acquiring a JMenuBar instance.
  11. Having finally, at long last, acquired both an ExampleApp instance and a JMenuBar instance, Guice calls addMenuToFrame, which adds the menu to the frame.
  12. Now after all that has happened, your getInstance(ExampleApp) call in main gets executed. Guice checks, finds that it already has an ExampleApp instance from a source marked @Singleton, and returns that instance.

Having looked through all of that, it seems @Singleton is strictly necessary only on provideExampleApp, but I think makes sense for everything else too.

like image 194
Douglas Avatar answered Sep 22 '22 04:09

Douglas