пятница, 26 августа 2011 г.

Разработка GWT приложения с использованием MVP и Mvp4G

1) MVC vs MVP 
     MVC (Model-View-Controller) - шаблон проектирования, в котором приложение разделено на три отдельные компоненты: модель данных, пользовательский интерфейс и логика.

  • Модель предоставляет данные и реагирует на запросы от контроллера, изменяя свое состояние 
  • Представление (View) отвечает за отображение информации 
  • Контроллер (Controller) - обрабатывает различные данные, и говорит модели и представлению о необходимости взаимодействия.
 
     В шаблоне проектирования MVP (Model-View-Presenter) логика отделяется от отображения. Presenter для получения и установки данных модели использует View, который определяется как интерфейс. Реализация View содержит ссылку на класс Presenter’а и предоставляет ссылку на себя. Когда происходит событие View - вызывается определенный метод Presentera’а. Далее Presenter получает данные из View, через интерфейс. Затем Presenter вызывает методы модели, и устанавливает данные из модели во View через интерфейс.


2) Mvp4g - превосходная библиотека для управления структуры GWT приложения. Особенно когда используется MVP подход, где Presenter’ы общаются между собой через события (events). Mvp4g позволяет эффективно строить приложение, особенно когда применяются след. приемы построения :
  • Event Bus 
  • Dependency Injection (Внедрение зависимости) 
  • MVP 
  • History Management/Place Service (Управление историей) 
3) Guice - dependency injection фреймворк от Google, который позволяет улучшить тестируемость и модульность кода, избавляет от необходимости написания собственных фабрик.
4) Gin - позволяет автоматически внедрять зависимость в клиентскую часть GWT. 
     Рассмотрим разработку приложения с подходом MVP для построения архитектуры GWT-проекта с использованием Mvp4g. Приложение будет разделено на отправителей(Senders), которые будут отсылать данные и получателей(Receivers), которые будут реагировать на изменения.
Для интеграции Mvp4g в проект нужно добавить след. строку в GWT конфигурационный файл:
<inherits name="com.mvp4g.Mvp4gModule" />
     В classpath проекта должны лежать след. библиотеки:

     Создадим стартовую точку GWT приложения и пропишем ее в конфигурационном файле GWT:
package com.dmitrynikol.gwt.mvp.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.mvp4g.client.Mvp4gModule;

public class Mvp4gApp implements EntryPoint {

    public void onModuleLoad() {
        Mvp4gModule module = (Mvp4gModule) GWT.create(Mvp4gModule.class);
        module.createAndStartModule();
        RootPanel.get().add((Widget) module.getStartView());
    }
}
<entry-point class='com.dmitrynikol.gwt.mvp.client.Mvp4gApp'/>
     Теперь когда проект настроен для использования с Mvp4g, перейдем к созданию других необходимых частей приложения (eventBus, handlers, presenters, view).
     Mvp4g архитектура построена вокруг шины событий (EventBus). EventBus похож на командный центр, который связывает вместе все остальные части. Основная его идея в том, что другие части не знают о друг друге и могут работать независимо в свободно связанной форме. Что в свою очередь означает, что мы получаем защищенное, поддерживаемое и масштабируемое решение. EventBus является очень важной частью, так как в приложении выступает в качестве точки входа. 
     Создадим шину событий:
@Events(startView = MainWidget.class)
public interface AppEventBus extends EventBus {

    @Start
    @Event(handlers = { PresenterHandler.class })
    public void init();

    @Event(handlers = { MainPresenter.class })
    public void showSenderWidget(IsWidget widget);

    @Event(handlers = { MainPresenter.class })
    public void showReceiverWidget(IsWidget widget);

    @Event(handlers = {ReceiverPresenter.class})
    public void setSelectedItem(String genre, String value);
}
  • @Events(startView = MainWidget.class) - говорим, что класс MainWidget будет использоваться в качестве основного view. 
      Часть приложения, которая отвечает за отображения информации(View) состоит из главного view - MainWidget и виджетов для отправки и приема данных(SenderWidget, ReceiverWidget).
public class MainWidget extends Composite implements IMainWidget {
    private static MainWidgetUiBinder uiBinder = GWT.create(MainWidgetUiBinder.class);

    @UiTemplate("MainWidget.ui.xml")
    interface MainWidgetUiBinder extends UiBinder<Widget, MainWidget> {
    }

    @UiField SplitLayoutPanel mainPanel;
    @UiField FlowPanel senderPanel;
    @UiField FlowPanel receiverPanel;

    private List<IsWidget> receiverList = new ArrayList<IsWidget>();

    public MainWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    @Override
    public void addSenderWidget(IsWidget widget) {
        senderPanel.add(widget);
    }

    @Override
    public void addReceiverWidget(IsWidget widget) {
        addInnerReceiverWidget(widget);
        if (!receiverList.isEmpty() && receiverList.size() == 3) {
            HorizontalPanel receiverGameCardPanel = new HorizontalPanel();
            for (IsWidget gameCard : receiverList)
                receiverGameCardPanel.add(gameCard);
            receiverPanel.add(receiverGameCardPanel);
            receiverList.clear();
        }
    }

    private void addInnerReceiverWidget(IsWidget widget) {
        receiverList.add(widget);
    }
}


* This source code was highlighted with Source Code Highlighter.
public class SenderWidget extends Composite implements ISenderWidget {
    private static SenderWidgetUiBinder uiBinder = GWT.create(SenderWidgetUiBinder.class);

    @UiTemplate("SenderWidget.ui.xml")
    interface SenderWidgetUiBinder extends UiBinder<Widget, SenderWidget> {
    }

    @UiField InlineLabel type;
    @UiField ListBox genreList;

    public SenderWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    @Override
    public void showGroup(String group) {
        type.setText(group);
        addStyleName(group);
    }

    @Override
    public HasChangeHandlers getSelectableComponent() {
        return genreList;
    }

    @Override
    public String getSelectedValue() {
        return genreList.getItemText(genreList.getSelectedIndex());
    }

    @Override
    public void displayValues(List<Genre> values) {
        genreList.addItem("");
        for (Genre item : values) {
            genreList.addItem(item.toString());
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
public class ReceiverWidget extends Composite implements IReceiverWidget {
    private static ReceiverWidgetUiBinder uiBinder = GWT.create(ReceiverWidgetUiBinder.class);

    @UiTemplate("ReceiverWidget.ui.xml")
    interface ReceiverWidgetUiBinder extends UiBinder<Widget, ReceiverWidget> {
    }

    @UiField InlineLabel gameType;
    @UiField InlineLabel gameName;

    public ReceiverWidget() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    @Override
    public void showGroup(String group) {
        gameType.setText(group);
        addStyleName(group);
    }

    @Override
    public void setReceivedValue(String value) {
        gameName.setText(value);
    }
}


* This source code was highlighted with Source Code Highlighter.
     К этим виджетам также идут шаблоны(*.ui.xml) из которых генерируется UiBinder.
     Презентерам нужны два аргумента - представление и шина событий. 
  • @Presenter(view = MainWidget.class) - говорим mvp4g системе, какой view этот класс будет обслуживать 
     Обслуживанием view занимаются презентеры. ReceiverPresenter и SenderPresenter наследуют класс AbstractGroupPresenter, где будет хранится тип игры. 
   Не так давно в Mvp4g появилась возможность множественного создания экземпляров презентеров. Презентер должен быть обьявлен так: 
  • @Presenter(view = SenderWidget.class, multiple = true) 
  Использование multiple = true, говорим о том, что мы должны управлять созданием экземпляров презентеров, примерно так: 
  • ReceiverPresenter presenter = (ReceiverPresenter) eventBus.addHandler(ReceiverPresenter.class, false)
    Так мы создаем экземпляр презентера и прикрепляем его к шине событий. После этого они будут готовы слушать события.
     Когда у нас будет множество экземпляров одного и того же презентера, как же заставить их взаимодействовать друг с другом? Нужно их сгруппировать! В примере используется след. событие для связи с выбранным значением(параметр genre определяет группу презентеров):
@Event(handlers = {ReceiverPresenter.class})
public void setSelectedItem(String genre, String value);

     Когда в приложении будет несколько экземпляров ReceiverPresenter и сработает событие, то все подключенные презентеры среагируют на него.
eventBus.setSelectedItem(Game.Action, “ThirdPersonShooter”);
     Презентеры слушают только те события к которым они подписаны, а это зависит от того к какой группе они принадлежат:
ReceiverPresenter presenter = (ReceiverPresenter) eventBus.addHandler(ReceiverPresenter.class, false);
presenter.setGame(Game.Action);
presenter.bind();
.....

Так осуществляется обработка событий в Receivers презентерах и проверка выбранной группы:
public void onSetSelectedItem(String genre, String value) {
        if (isThisGenre(genre)) {
            // show some game
        }
}
Пакет презентеров представлен след. классами:
public abstract class AbstractGroupPresenter<V, E extends EventBus> extends BasePresenter<V, E> {
    private String game;

    public String getGame() {
        return game;
    }

    public void setGame(String game) {
        this.game = game;
    }

    protected boolean isThisGenre(String gameCheck) {
        if (gameCheck == null || gameCheck.equals("")) {
            return false;
        } else {
            return gameCheck.equals(game);
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = MainWidget.class)
public class MainPresenter extends BasePresenter<IMainWidget, AppEventBus> {

    public interface IMainWidget {
        public void addSenderWidget(IsWidget widget);
        public void addReceiverWidget(IsWidget widget);
    }

    public void onShowSenderWidget(IsWidget widget) {
        view.addSenderWidget(widget);
    }

    public void onShowReceiverWidget(IsWidget widget) {
        view.addReceiverWidget(widget);
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = SenderWidget.class, multiple = true)
public class SenderPresenter extends AbstractGroupPresenter<ISenderWidget, AppEventBus> {
    public interface ISenderWidget {
        void showGroup(String group);
        HasChangeHandlers getSelectableComponent();
        String getSelectedValue();
        void displayValues(List<Genre> asList);
    }

    public void bind() {
        view.showGroup(getGame());
        view.getSelectableComponent().addChangeHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                eventBus.setSelectedItem(getGame(), view.getSelectedValue());
            }
        });
        view.displayValues(Game.valueOf(getGame()).getGenre());
    }
}


* This source code was highlighted with Source Code Highlighter.
@Presenter(view = ReceiverWidget.class, multiple = true)
public class ReceiverPresenter extends AbstractGroupPresenter<IReceiverWidget, AppEventBus> {

    public interface IReceiverWidget {
        void showGroup(String group);
        void setReceivedValue(String value);
    }

    @Override
    public void bind() {
        view.showGroup(getGame());
    }

    public void onSetSelectedItem(String genre, String value) {
        if (isThisGenre(genre)) {
            List<String> games = Genre.valueOf(value).getGames();
            view.setReceivedValue(games.get(Random.nextInt(games.size())));
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
Для хранения типа игр, жанров и названий используется enum Game и Genre.
public enum Game {
    Action(Arrays.asList(Genre.FirstPersonShooter, Genre.ThirdPersonShooter)),
    RPG(Arrays.asList(Genre.ActionRPG, Genre.MMORPG, Genre.TacticalRPG)),
    Strategy(Arrays.asList(Genre.TurnBasedStrategy, Genre.RealTimeTactics));

    private List<Genre> genre;

    private Game(List<Genre> genre) {
        this.genre = genre;
    }

    public List<Genre> getGenre() {
        return genre;
    }
}


* This source code was highlighted with Source Code Highlighter.
public enum Genre {
    /** Action game */
    FirstPersonShooter(Arrays.asList("Call of Duty", "Team Fortress",
            "Halo", "Killzone", "Unreal Tournament", "Doom")),
    ThirdPersonShooter(Arrays.asList("Prince of Persia", "Max Payne",
            "Gears of War", "Resident Evil ", "Army of Two", "Kill Switch")),

    /** RPG game */
    ActionRPG(Arrays.asList("Diablo", "Dungeon Siege",
            "Sacred", "Hellgate: London", "Torchlight", "Mass Effect")),
    MMORPG(Arrays.asList("Final Fantasy", "World of Warcraft",
            "Meridian 59", "Ultima Online", "EverQuest", "The Monster")),
    TacticalRPG(Arrays.asList("Silent Storm", "Fallout", "Freedom Force",
            " Dungeons and Dragons Tactics", "Rebelstar: Tactical Command", "Gladius")),

    /** Strategy game */
    TurnBasedStrategy(Arrays.asList("Civilization", "Heroes of Might and Magic",
            "Making History", "Advance Wars", "Master of Orion")),
    RealTimeTactics(Arrays.asList("Warcraft", "Age of Empires",
            "Dawn of War", "Command and Conquer", "World In Conflict", "Halo Wars"));

    private List<String> games;

    private Genre(List<String> games) {
        this.games = games;
    }

    public List<String> getGames() {
        return games;
    }
}


* This source code was highlighted with Source Code Highlighter.
     Создадим PresenterHandler, который будет отвечать за инициализацию приложения. Метод onInit() вызывается на старте и заполняется виджетами, которые будет отправлять и слушать изменения.
@EventHandler
public class PresenterHandler extends BaseEventHandler<AppEventBus> {
    public void onInit() {
        addWidget(SenderPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);
        addWidget(ReceiverPresenter.class, Game.Action);

        addWidget(SenderPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);
        addWidget(ReceiverPresenter.class, Game.RPG);

        addWidget(SenderPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
        addWidget(ReceiverPresenter.class, Game.Strategy);
    }

    private void addWidget(
            Class<? extends AbstractGroupPresenter<?, ?>> component, Game game) {
        AbstractGroupPresenter<?, ?> presenter = (AbstractGroupPresenter<?, ?>) eventBus
                .addHandler(component, false);
        presenter.setGame(game.toString());
        presenter.bind();
        if (presenter instanceof SenderPresenter) {
            eventBus.showSenderWidget((Widget) presenter.getView());
        } else {
            eventBus.showReceiverWidget((Widget) presenter.getView());
        }
    }
}


* This source code was highlighted with Source Code Highlighter.
Вот так выглядит запущенное приложение:


Eclipse проект можно скачать по следующей ссылке.

Комментариев нет:

Отправить комментарий