Avoiding cast in generic factory-based MVP framework
Asked Answered
G

1

7

I was trying to implement a way to decouple View and Presenter in the MVP pattern to provide a framework, which does exactly this, but after a point I got confused.

Background

I have a View interface with a generic type for the connected presenter and vice versa. Those interfaces are to be extended by the implementing developer. The concrete interface is not of interest for this question, but the class definition of those both look like the following:

public interface Presenter<T extends View>

and

public interface View<T extends Presenter>

The idea is that both the View and the Presenter know the opposing interface.

For using this structure, the developer should provide a factory, that instantiates the View he wants to show and the Presenter that is handling this View. He gives both of them to a class called the SuperController. They are associated by the class of the View.

The PresenterFactory interface, which creates the Presenter, has no parameters and returns a Presenter implementation and looks the following:

public interface PresenterFactory<T extends View> {

    <S extends Presenter<T>> S create();

}

The ViewFactory interface, which creates the View, creates a View implementation, based on the Presenter and looks the following:

public interface ViewFactory<T extends View, S extends Presenter<T>> {

    T create(S presenter);

}

Problem

The problem I am encountering is the following:

I wanted to provide an example with a TestView and a TestPresenter. Those look like this:

public interface TestPresenter extends Presenter<TestView> {...}

public interface TestView extends View<TestPresenter> {...}

Also, a ViewFactory is provided, which looks like this:

class TestViewFactory implements ViewFactory<TestView, TestPresenter> {

    @Override
    public TestView create(TestPresenter presenter) {
        return new TestViewImpl(presenter);
    }
}

And this is the TestPresenterFactory:

private class TestPresenterFactory implements PresenterFactory<TestView> {

    @Override
    public <S extends Presenter<TestView>> S create() {
        return new TestPresenterImpl();
    }
}

This code cannot be compiled. The problem is the return value of the TestPresenterFactory. Java says, it expects S and not a TestPresenterImpl. Also, casting to TestPresenter will not work. However, casting to S will work. It than can be compiled and also runs successfully, but this is not what I wanted the implementing developer to do.

Why does this problem exist? And why does casting to the concrete interface not work? In my opinion, this should work, because the TestPresenterImpl implements the TestPresenter, which extends Presenter of the generic type TestView yet it is not compilable.


The following does work. If you change the PresenterFactory definition like this:

public interface PresenterFactory<T extends View, S extends Presenter<T>> {

    S create();

}

with the example implementation now looking like this:

private class TestPresenterFactory implements PresenterFactory<TestView, TestPresenter> {

    @Override
    public TestPresenter create() {
        return new TestPresenterImpl();
    }
}

It can be compiled and run, just like if I cast to S in the above example.

However, this is not what I want. The generic type declaration is redundant. Letting the implementing developer declare the View and the presenter, to only create the Presenter looks awkward. It would be really great if the type could inferred at the method.

Also, I do not want to force the implementing developer to cast to S.

Is there a more elegant way for this?


edit

A question was brought up as a duplicate and I want to distance myself from this question.

The problem that arises from this has nothing to do with Producer Extends Consumer Super. I have two producers (factories) in use. Both of which are producing a class. One depends on another class (the View depends on the Presenter), while the other one is without dependency (the Presenter can be instantiated with a default constructor). All respected interfaces have the T extends XYZ definition, to allow the production of interfaces, which inherit of that interface (either the Presenter or the View).

The real problem here is that the generic within the ViewFactory can be inferred by Java. Both of the generic types are declared within the class definition. Within the PresenterFactory, the method-level generic cannot be inferred by Java. Even though, the generic type is the same as the one in the view factory <S extends Presenter<T>>; at the method, this type cannot be inferred.

The solution is to either cast (which I don't want the using developer to do) or to declare the PresenterType in the class definition (which seems to be redundant. The only thing of interest is, that the view is defined and any Presenter for that view is returned).

My question is what can I do to work around said issues that are a result of the extends clause?

Garvy answered 29/8, 2018 at 17:7 Comment(5)
How is your solution not what you want? What is your object to that solution? The generic type declaration is not redundant; it controls the type returned by create. Also, your bounds for your type parameters use the raw forms of your interfaces.Rayleigh
The View type definition on the PresenterFactory is redundant, because it is never used, except for the return value (or, more concrete, the generic type of the Presenter). This is the reason, i want the generic type to be infered on the method. The developer should not have to declare the View type, as well as the associated Presenter type. They should only have to provide the View type and return any Presenter type that fullfills this contract.Garvy
Just introduce an abstract class and you'll be able to work around this. But your classes all depend on each other, that's not good practice and it will probably lead to troubles down the line.Sarcocarp
@NyamiouTheGaleanthrope Could you elaborate on that? From what i understand, in MVP the Presenter depends on the View-interface and the View depends on the Presenter-interface. So they will depend on each others abstraction regardless, but they do not know the implementation.Garvy
"They should only have to provide the View type and return any Presenter type that fullfills this contract." Then it sounds like the return type of the create method should actually be Presenter<T>.Curiosa
F
1

For starters, this will solve the problem:

interface PresenterFactory<T extends View, S extends Presenter<T>> {
    S create();
}

class TestPresenterFactory implements PresenterFactory<TestView, TestPresenterImpl> {

    @Override
    public TestPresenterImpl create() {
        return new TestPresenterImpl();
    }
}

But I still don't like:

  • the circular dependency between View and Presenter;
  • the use of raw types: actually, if you blindly use View<?> and Presenter<?> in their respective interface declarations, then again it won't compile (TestPresenterFactory will fail).

--- edit ---

So how about this to fix the last 2 points, along with your original issue:

interface Presenter<P extends Presenter<P, V>, V extends View<P, V>> {

}

interface View<P extends Presenter<P, V>, V extends View<P, V>> {

}

interface PresenterFactory<P extends Presenter<P, V>, V extends View<P, V>> {

    P create();

}

interface ViewFactory<P extends Presenter<P, V>, V extends View<P, V>> {

    V create(P presenter);

}

interface TestPresenter extends Presenter<TestPresenter, TestView> {}

class TestPresenterImpl implements TestPresenter {

}

interface TestView extends View<TestPresenter, TestView> {}

class TestViewImpl implements TestView {

    public TestViewImpl(Presenter<?, ?> presenter) {
    }
}

class TestViewFactory implements ViewFactory<TestPresenter, TestView> {

    @Override
    public TestView create(TestPresenter presenter) {
        return new TestViewImpl(presenter);
    }
}

class TestPresenterFactory implements PresenterFactory<TestPresenter, TestView> {

    @Override
    public TestPresenter create() {
        return new TestPresenterImpl();
    }
}
Fanion answered 5/9, 2018 at 12:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.