Guice and MVP pattern: decouple the view from the presenter

134 Views Asked by At

I'm writing an application in Swing and I'd like to make use of Model–view–presenter pattern. I found a sample application using this pattern and then I alter it to make use of Guice to do dependency injection. I was able to make it work with albeit with one problematic piece of code. Let me first show my code and later on the piece of code I find problematic.

Application starting point:

@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class Main {

    private final View view;

    public static void main(final String[] args) {

        final Main main = Guice.createInjector().getInstance(Main.class);

        SwingUtilities.invokeLater(main.view::createUI);
    }
}

Model:

public class Model {

    public String getPassword() {
        return "password";
    }
}

Presenter:

@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class Presenter {

    private final View view;
    private final Model model;

    public void login(final String password) {

        final String result = model.getPassword().equals(password)
                ? "Correct password" : "Incorrect password";

        view.updateStatusLabel(result);
    }
}

Until this point everything seems to be fine.

The problem is in the View constructor. I manually create instance of Presenter using this (due to some coupling involved in MVP pattern) instead of letting Guice does it job.

EDIT

How do I overcome this problem? As per comments,I find this code incorrect and I'd like to rewrite it so that the view could be injected into the presenter via dependency injection.

Guice has this somewhat covered how to do this in its documentation, however, I find it really difficult to understand the docs.

public class View {

    private final Presenter presenter;

    private JLabel statusLabel;
    private JTextField inputField;

    public View() {
        // manually adding this, should be DI
        this.presenter = new Presenter(this, new Model());
    }


    public void createUI() {
        // removed
    }

    //called by the presenter to update the status label
    public void updateStatusLabel(final String text) {
        statusLabel.setText(text);
    }
}
1

There are 1 best solutions below

0
VonC On BEST ANSWER

You are indeed manually instantiating the Presenter in the View and you would ideally want this to be done by Guice through dependency injection. That is a circular dependency issue because Presenter depends on View and... View depends on Presenter.

The simplest solution is to use a method called 'injecting providers'. In Guice, providers are simple interfaces that provide instances of a type T. You can leverage this to decouple the view and presenter creation logic.

View: (using provider binding)

public class View {

    private final Provider<Presenter> presenterProvider;
    private Presenter presenter;

    private JLabel statusLabel;
    private JTextField inputField;

    @Inject
    public View(Provider<Presenter> presenterProvider) {
        this.presenterProvider = presenterProvider;
    }

    // Lazy initialization of Presenter.
    private Presenter getPresenter() {
        if (presenter == null) {
            presenter = presenterProvider.get();
        }
        return presenter;
    }

    public void createUI() {
        // Here you should use getPresenter() instead of presenter
        // removed
    }

    //called by the presenter to update the status label
    public void updateStatusLabel(final String text) {
        statusLabel.setText(text);
    }
}

Presenter:

@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class Presenter {

    private final View view;
    private final Model model;

    public void login(final String password) {

        final String result = model.getPassword().equals(password)
                ? "Correct password" : "Incorrect password";

        view.updateStatusLabel(result);
    }
}

Then you will need to bind these dependencies in Guice's module:

public class ApplicationModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(View.class).in(Singleton.class);
        bind(Presenter.class);
        bind(Model.class);
    }
}

And then finally, your main class should use this module:

public class Main {

    private final View view;

    @Inject
    public Main(View view) {
        this.view = view;
    }

    public static void main(final String[] args) {
        Injector injector = Guice.createInjector(new ApplicationModule());
        final Main main = injector.getInstance(Main.class);

        SwingUtilities.invokeLater(main.view::createUI);
    }
}

That setup will make sure that Presenter is injected into View lazily (on the first call of getPresenter()), breaking the circular dependency.

It uses Just-In-Time Bindings: These are the bindings that you have not explicitly defined in your module, but Guice can still inject them because it knows how to build them.
When you are injecting View into Main and Presenter into View using a Provider, you are not creating explicit bindings for them in the ApplicationModule. But Guice can still make these injections because it can build View and Presenter using their injectable constructors (i.e., constructors that are annotated with @Inject), effectively performing just-in-time bindings.