Статьи

Улучшение проекта Vaadin 4 Spring с более простым MVP

Я использовал   библиотеку Vaadin 4 Spring в своем текущем проекте, и это было очень приятно. Однако в середине проекта мой коллега решил  «улучшить тестируемость» . Намерение было похвальным, хотя проект уже пытался реализовать шаблон MVP (пожалуйста, проверьте  эту статью  для более подробной информации). Вместо того, чтобы исправлять ошибки здесь и там, он реорганизовал всю кодовую базу, используя предоставленный  модуль MVP … ИМХО, это была огромная ошибка. В этой статье я попытаюсь осветить то, что мешает мне в существующей реализации, и альтернативное решение.

Существующая реализация MVP состоит из  одного класса . Вот оно, сокращенно для удобства чтения:

public abstract class Presenter<V extends View> {
 
    @Autowired
    private SpringViewProvider viewProvider;
 
    @Autowired
    private EventBus eventBus;
 
    @PostConstruct
    protected void init() {
        eventBus.subscribe(this);
    }
 
    public V getView() {
        V result = null;
        Class<?> clazz = getClass();
        if (clazz.isAnnotationPresent(VaadinPresenter.class)) {
            VaadinPresenter vp = clazz.getAnnotation(VaadinPresenter.class);
            result = (V) viewProvider.getView(vp.viewName());
        }
        return result;
    }
    // Other plumbing code
}

Этот класс довольно самоуверенный и имеет следующие недостатки:

  1. Он основан на автоматическом подключении в полевых условиях, что крайне затрудняет  модульный тест   классов Presenter . В качестве доказательства предоставленный  тестовый класс  является не модульным тестом, а  интеграционным тестом .
  2. Он основан исключительно на компонентном сканировании, что предотвращает  явное  внедрение зависимостей.
  3. Он обеспечивает реализацию  View интерфейса, независимо от того, требуется он или нет. Когда навигатор не используется, он делает реализацию пустого  enterView() метода обязательной.
  4. Он берет на себя ответственность за создание  представления  от поставщика представления.
  5. Он соединяет  Presenter  и  View с его  @VaadinPresenter аннотацией, не позволяя одному  Presenter  обрабатывать различные   реализации View .
  6. Это требует явного вызова  init() метода  Presenter , так как  @PostConstruct аннотация в суперклассе не вызывается, когда у подкласса есть такая.

Я разработал альтернативный класс, который пытается обратиться к предыдущим пунктам — и также проще:

public abstract class Presenter<T> {
 
    private final T view;
    private final EventBus eventBus;
 
    public Presenter(T view, EventBus eventBus) {
        Assert.notNull(view);
        Assert.notNull(eventBus);
        this.view = view;
        this.eventBus = eventBus;
        eventBus.subscribe(this);
    }
    // Other plumbing code
}

Этот класс делает каждый подкласс легко тестируемым по модулю, как доказывают следующие фрагменты:

public class FooView extends Label {}
 
public class FooPresenter extends Presenter<fooview> {
 
    public FooPresenter(FooView view, EventBus eventBus) {
        super(view, eventBus);
    }
 
    @EventBusListenerMethod
    public void onNewCaption(String caption) {
        getView().setCaption(caption);
    }
}
 
public class PresenterTest {
 
    private FooPresenter presenter;
    private FooView fooView;
    private EventBus eventBus;
 
    @Before
    public void setUp() {
        fooView = new FooView();
        eventBus = mock(EventBus.class);
        presenter = new FooPresenter(fooView, eventBus);
    }
 
    @Test
    public void should_manage_underlying_view() {
        String message = "anymessagecangohere";
        presenter.onNewCaption(message);
        assertEquals(message, fooView.getCaption());
    }
}

Тот же Интеграционный тест, что и для исходного класса, также может быть обработан с использованием явного внедрения зависимости:

public class ExplicitPresenter extends Presenter<FooView> {
 
    public ExplicitPresenter(FooView view, EventBus eventBus) {
        super(view, eventBus);
    }
 
    @EventBusListenerMethod
    public void onNewCaption(String caption) {
        getView().setCaption(caption);
    }
}
 
@Configuration
@EnableVaadin
public class ExplicitConfig {
 
    @Autowired
    private EventBus eventBus;
 
    @Bean
    @UIScope
    public FooView fooView() {
        return new FooView();
    }
 
    @Bean
    @UIScope
    public ExplicitPresenter fooPresenter() {
        return new ExplicitPresenter(fooView(), eventBus);
    }
}
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ExplicitConfig.class)
@VaadinAppConfiguration
public class ExplicitPresenterIT {
 
    @Autowired
    private ExplicitPresenter explicitPresenter;
 
    @Autowired
    private EventBus eventBus;
 
    @Test
    public void should_listen_to_message() {
        String message = "message_from_explicit";
        eventBus.publish(this, message);
        assertEquals(message, explicitPresenter.getView().getCaption());
    }
}

И последнее, но не менее важное: эта альтернатива также позволяет вам использовать автоматическое подключение и сканирование компонентов, если вам это нравится! Единственное отличие состоит в том, что он обеспечивает автоматическое подключение конструктора вместо полевого автоматического подключения (на мой взгляд, это считается плюсом, хотя и более многословным):

@UIScope
@VaadinComponent
public class FooView extends Label {}
 
@UIScope
@VaadinComponent
public class AutowiredPresenter extends Presenter<FooView> {
 
    @Autowired
    public AutowiredPresenter(FooView view, EventBus eventBus) {
        super(view, eventBus);
    }
 
    @EventBusListenerMethod
    public void onNewCaption(String caption) {
        getView().setCaption(caption);
    }
}
 
@ComponentScan
@EnableVaadin
public class ScanConfig {}
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ScanConfig.class)
@VaadinAppConfiguration
public class AutowiredPresenterIT {
 
    @Autowired
    private AutowiredPresenter autowiredPresenter;
 
    @Autowired
    private EventBus eventBus;
 
    @Test
    public void should_listen_to_message() {
        String message = "message_from_autowired";
        eventBus.publish(this, message);
        assertEquals(message, autowiredPresenter.getView().getCaption());
    }
}

Хорошей новостью является то, что этот модуль теперь является частью проекта  vaadin4spring  на Github. Если вам нужно приложение MVP для приложения Vaadin Spring, вам достаточно одного клика!