92

GWT App Architecture Best Practices

Embed Size (px)

DESCRIPTION

Google Web Toolkit provides the infrastructure you need to build a high performance web application and leaves the architecture open to fit your needs. Learn from others who have gone before. In this session we'll discuss best practices that real web applications are using to achieve high performance event handling, UI creation, and more.Ray RyanWatch a video at http://www.bestechvideos.com/2009/06/29/google-i-o-2009-best-practices-for-architecting-gwt-app

Citation preview

Page 1: GWT App Architecture Best Practices
Page 2: GWT App Architecture Best Practices

Ray Ryan28 May 2009

GWT App ArchitectureBest Practices

Page 3: GWT App Architecture Best Practices

• How to organize a nontrivial GWT application

• Particular focus on client side

• Lessons learned from new AdWords UI

What are we talking about?

Page 4: GWT App Architecture Best Practices

If you remember nothing else...

Page 5: GWT App Architecture Best Practices

• Get browser history right, and get it right early

If you remember nothing else...

Page 6: GWT App Architecture Best Practices

• Get browser history right, and get it right early

If you remember nothing else...

Page 7: GWT App Architecture Best Practices

• Get browser history right, and get it right early

• Use an Event Bus to fight spaghetti

If you remember nothing else...

Page 8: GWT App Architecture Best Practices

• Get browser history right, and get it right early

• Use an Event Bus to fight spaghetti

If you remember nothing else...

Page 9: GWT App Architecture Best Practices

• Get browser history right, and get it right early

• Use an Event Bus to fight spaghetti

• DI + MVP FTW

If you remember nothing else...

Page 10: GWT App Architecture Best Practices

• Get browser history right, and get it right early

• Use an Event Bus to fight spaghetti

• DI + MVP FTW

If you remember nothing else...

Dependency Injection plusModel / View / Presenterfor the win!

Page 11: GWT App Architecture Best Practices
Page 12: GWT App Architecture Best Practices

Demo new AdWords UI

Page 13: GWT App Architecture Best Practices

1.Embrace asynchrony

2.Always be decoupling

3.Strive to achieve statelessness

Three major themes

Page 14: GWT App Architecture Best Practices
Page 15: GWT App Architecture Best Practices

Embrace Asynchrony

Page 16: GWT App Architecture Best Practices

• Everything might require an async call sometimes

• So assume it does all the time

Remember what that A in AJAX is for

Page 17: GWT App Architecture Best Practices

• Everything might require an async call sometimes

• So assume it does all the time

Remember what that A in AJAX is for

class Contact { String name; List<ContactDetail> details; List<ContactDetail> getContactDetails() { return details; }

String getName() { return name; }}

Page 18: GWT App Architecture Best Practices

• Everything might require an async call sometimes

• So assume it does all the time

Remember what that A in AJAX is for

class Contact { String name; List<ContactDetail> details; List<ContactDetail> getContactDetails() { return details; }

String getName() { return name; }}

X

Page 19: GWT App Architecture Best Practices

class Contact { String name; ArrayList<ContactDetailId> detailIds;

ArrayList<ContactDetailId> getDetailIds() { return detailIds; }

String getName() { return name; }}

• Everything might require an async call sometimes

• So assume it does all the time

Remember what that A in AJAX is for

Page 20: GWT App Architecture Best Practices

• Leverage point for

• Caching

• Batching

• Centralize failure handling • Lays the groundwork for

• GWT.runAsync()

• Undo / Redo

• Gears / HTML5 DB

Command Pattern to make async tolerable

Page 21: GWT App Architecture Best Practices

Use Command pattern RPC

/** The name Command is taken */interface Action<T extends Response> { }

interface Response { }

interface ContactsService extends RemoteService { <T extends Response> T execute(Action<T> action);} interface ContactsServiceAsync { <T extends Response> void execute(Action<T> action, AsyncCallback<T> callback);}

Page 22: GWT App Architecture Best Practices

Command style RPC exampleWrite an Action…

class GetDetails implements Action<GetDetailsResponse> { private final ArrayList<ContactDetailId> ids;

public GetDetails(ArrayList<ContactDetailId> ids) { this.ids = ids; }

public ArrayList<ContactDetailId> getIds() { return ids; }}

Page 23: GWT App Architecture Best Practices

Command style RPC example…and its response…

class GetDetailsResponse implements Response { private final ArrayList<ContactDetail> details;

public GetDetailsResponse(ArrayList<ContactDetail> details) { this.details = details; }

public ArrayList<ContactDetail> getDetails() { return new ArrayList<ContactDetail>(details); }}

Page 24: GWT App Architecture Best Practices

Command style RPC example…plus convenience callback

abstract class GotDetails implements AsyncCallback<GetDetailsResponse> {

public void onFailure(Throwable oops) { /* default appwide failure handling */ }

public void onSuccess(GetDetailsResponse result) { got(result.getDetails()); }

public abstract void got(ArrayList<ContactDetail> details);}

Page 25: GWT App Architecture Best Practices

Command style RPC exampleMake it go

void showContact(final Contact contact) { service.execute(new GetDetails(contact.getDetailIds()), new GotDetails() { public void got(ArrayList<ContactDetail> details) { renderContact(contact); renderDetails(details); } });}

Page 26: GWT App Architecture Best Practices
Page 27: GWT App Architecture Best Practices

Always be decoupling

Page 28: GWT App Architecture Best Practices

• An event bus

• MVP pattern for your custom widgets

• Dependency injection of app-wide services

Always be decouplingWith the combination of

Page 29: GWT App Architecture Best Practices

• An event bus

• MVP pattern for your custom widgets

• Dependency injection of app-wide services

Always be decouplingWith the combination of

Easy rejiggering of the app

You get...

Page 30: GWT App Architecture Best Practices

• An event bus

• MVP pattern for your custom widgets

• Dependency injection of app-wide services

Always be decoupling

Easy to defer pokey DOM operations

With the combination of

Easy rejiggering of the app

You get...

Page 31: GWT App Architecture Best Practices

• An event bus

• MVP pattern for your custom widgets

• Dependency injection of app-wide services

Always be decoupling

Easy to defer pokey DOM operations

Easy unit testing

With the combination of

Easy rejiggering of the app

You get...

Page 32: GWT App Architecture Best Practices

• An event bus

• MVP pattern for your custom widgets

• Dependency injection of app-wide services

Always be decoupling

Easy to defer pokey DOM operations

Fast test execution

Easy unit testing

With the combination of

Easy rejiggering of the app

You get...

Page 33: GWT App Architecture Best Practices
Page 34: GWT App Architecture Best Practices

Decoupling via event bus

Page 35: GWT App Architecture Best Practices

Coupling

Page 36: GWT App Architecture Best Practices

Coupling

Page 37: GWT App Architecture Best Practices

Coupling

Page 38: GWT App Architecture Best Practices

Coupling

Page 39: GWT App Architecture Best Practices

Coupling

Page 40: GWT App Architecture Best Practices

Coupled

Page 41: GWT App Architecture Best Practices

Looser

Page 42: GWT App Architecture Best Practices

Loose

Page 43: GWT App Architecture Best Practices

EventBusImplement with GWT HandlerManager

void showContact(final Contact contact) { service.execute(new GetDetails(contact.getDetailIds()), new GotDetails() { public void got(ArrayList<ContactDetail> details) { renderContact(contact); renderDetails(details); } } });}

Page 44: GWT App Architecture Best Practices

ContactId currentContact;

void showContact(final Contact contact) { if (!currentContactId.equals(contact)) { currentContactId = currentContactId; service.execute(new GetDetails(contact.getDetailIds()), new GotDetails() { public void got(ArrayList<ContactDetail> details) { renderContact(contact); renderDetails(details); } }); } }

EventBusImplement with GWT HandlerManager

Page 45: GWT App Architecture Best Practices

EventBus

HandlerManager eventBus; void listenForContactUpdates() { eventBus.addHandler(ContactChangeEvent.TYPE, new ContactChangeEventHandler() { public void onContactChange(ContactChangeEvent event) { Contact contact = event.getContact(); if (currentContactId.equals(contact.getId())) { renderContact(contact); } } });}

Implement with GWT HandlerManager

Page 46: GWT App Architecture Best Practices

EventBus

public void execute(final UpdateContact update, final AsyncCallback<GetContactsResponse> cb) { realService.execute(update, new AsyncCallback<UpdateContactResponse>() { public void onFailure(Throwable caught) { cb.onFailure(caught); } public void onSuccess(UpdateContactResponse result) { recache(update.getContact()); cb.onSuccess(result); ContactChangeEvent e = new ContactChangeEvent(update.getContact()); eventBus.fireEvent(e); } });}

Tossing the event

Page 47: GWT App Architecture Best Practices

EventBus

public void execute(final UpdateContact update, final AsyncCallback<GetContactsResponse> cb) { realService.execute(update, new AsyncCallback<UpdateContactResponse>() { public void onFailure(Throwable caught) { cb.onFailure(caught); } public void onSuccess(UpdateContactResponse result) { recache(update.getContact()); cb.onSuccess(result); ContactChangeEvent e = new ContactChangeEvent(update.getContact()); eventBus.fireEvent(e); } });}

Tossing the event

Page 48: GWT App Architecture Best Practices
Page 49: GWT App Architecture Best Practices

Decoupling via MVP

Page 50: GWT App Architecture Best Practices

Classic Model View Controller pattern

Page 51: GWT App Architecture Best Practices

Classic Model View Controller patternHow 1980s

Page 52: GWT App Architecture Best Practices

Classic Model View Controller patternHow 1980s

public domain image http://vintageprintable.com/wordpress/wp-content/uploads/2009/04/deirochelys_reticulariaholbrookv1p07a.jpg

Page 53: GWT App Architecture Best Practices

MVP

public domain image http://vintageprintable.com/wordpress/wp-content/uploads/2009/04/deirochelys_reticulariaholbrookv1p07a.jpg

Page 54: GWT App Architecture Best Practices

MVP

GWTMockUtilities

public domain image by http://www.freeclipartnow.com/animals/rabbits/Brown-Hare.jpg.html

Page 55: GWT App Architecture Best Practices

MVP

Page 56: GWT App Architecture Best Practices

MVP

class Phone implements ContactDetail { final ContactDetailId id; String number; String label; // work, home...}

Page 57: GWT App Architecture Best Practices

MVP

class Phone implements ContactDetail { final ContactDetailId id; String number; String label; // work, home...}

Page 58: GWT App Architecture Best Practices

MVP

class Phone implements ContactDetail { final ContactDetailId id; String number; String label; // work, home...}

class PhoneEditor { interface Display { HasClickHandlers getSaveButton(); ...

Page 59: GWT App Architecture Best Practices

Sample Presenter: PhoneEditor

class PhoneEditor { interface Display { HasClickHandlers getSaveButton(); HasClickHandlers getCancelButton(); HasValue<String> getNumberField(); HasValue<String> getLabelPicker(); ...}

Display interface using characteristic interfaces

Page 60: GWT App Architecture Best Practices

Sample Presenter: PhoneEditor

void bindDisplay(Display display) { this.display = display;

display.getSaveButton().addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { doSave(); } });

display.getCancelButton().addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { doCancel(); } });}

Binding the display

Page 61: GWT App Architecture Best Practices

Sample Presenter: PhoneEditor

void editPhone(Phone phone) { this.phone = Phone.from(phone);

display.getNumberField().setValue(phone.getNumber()); display.getLabelPicker().setValue(phone.getLabel());}

Start editing

Page 62: GWT App Architecture Best Practices

Sample Presenter: PhoneEditor

void doSave() { phone.setNumber(display.getNumberField().getValue()); phone.setLabel(display.getLabelPicker().getValue());

service.execute(new UpdatePhone(phone), new UpdatedPhone() { public void updated() { tearDown(); }

public void hadErrors(HashSet<PhoneError> errors) { renderErrors(errors); } });}

Save it

Page 63: GWT App Architecture Best Practices
Page 64: GWT App Architecture Best Practices

Decoupling via DI

Page 65: GWT App Architecture Best Practices

Dependency Injection

• Just a pattern:

o No globals

o No service locator

o Dependencies pushed in, preferably via constructor

• Not hard to do manually

• GIN (client) and Guice (server) can automate it

• http://code.google.com/p/google-guice/

• http://code.google.com/p/google-gin/

Page 66: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 67: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 68: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 69: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 70: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 71: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 72: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 73: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 74: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { ContactsServiceAsync realService = GWT.create(ContactsServiceAsync.class); CachedBatchingService rpcService = new CachedBatchingService(realService); GVoiceService voiceService = GWT.create(GVoiceService.class); HandlerManager eventBus = new HandlerManager(null);

PhoneEditWidget phoneEditWidget = new PhoneEditWidget(); PhoneEditor phoneEditor = new PhoneEditor(phoneEditWidget, rpcService, voiceService);

ContactWidget contactWidget = new ContactWidget(); ContactViewer contactViewer = new ContactViewer(contactWidget, phoneEditor, rpcService, eventBus); contactViewer.go(RootPanel.get());

Page 75: GWT App Architecture Best Practices

DI slice for the PhoneEditor

public void onModuleLoad() { MyGinjector factory = GWT.create(MyGinjector.class);

factory.createContactViewer().go(RootPanel.get());}

Page 76: GWT App Architecture Best Practices
Page 77: GWT App Architecture Best Practices

Decoupling payoff: fast tests

Page 78: GWT App Architecture Best Practices

Test the PhoneEditor

class MockContactsService implements ContactsServiceAsync { Action<?> lastAction; AsyncCallback<?> lastCallback;

public <T extends Response> void execute(Action<T> action, AsyncCallback<T> callback) { lastAction = action; lastCallback = callback; }}

Define some mocks

Page 79: GWT App Architecture Best Practices

Test the PhoneEditorDefine some mocks

class MockClickEvent extends ClickEvent { }

class MockHasClickHandlers implements HasClickHandlers { ClickHandler lastClickHandler;

public HandlerRegistration addClickHandler( ClickHandler handler) { lastClickHandler = handler;

return new HandlerRegistration() { public void removeHandler() { } }; } public void fireEvent(GwtEvent<?> event) { }}

Page 80: GWT App Architecture Best Practices

Test the PhoneEditorDefine some mocks

class MockHasValue<T> implements HasValue<T> { T lastValue;

public T getValue() { return lastValue; }

public void setValue(T value) { this.lastValue = value; }

public void setValue(T value, boolean fireEvents) { setValue(value); } ...

Page 81: GWT App Architecture Best Practices

Test the PhoneEditorDefine some mocks

class MockPhoneEditorDisplay implements PhoneEditor.Display { MockHasClickHandlers save = new MockHasClickHandlers(); HasClickHandlers cancel = new MockHasClickHandlers(); HasValue<String> labelPicker = new MockHasValue<String>(); HasValue<String> numberField = new MockHasValue<String>();

public HasClickHandlers getCancelButton() { return cancel; } public HasClickHandlers getSaveButton() { return save; } public HasValue<String> getLabelPicker() { return labelPicker; } public HasValue<String> getNumberField() { return numberField; }}

Page 82: GWT App Architecture Best Practices

Test the PhoneEditorSet up the test...

public void testSave() { MockContactsService service = new MockContactsService(); MockPhoneEditorDisplay display = new MockPhoneEditorDisplay(); // Build before and after values ContactDetailId id = new ContactDetailId();

Phone before = new Phone(id); before.setLabel("Home"); before.setNumber("123 456 7890");

Phone expected = Phone.from(before); expected.setLabel("Work"); expected.setNumber("098 765 4321");

Page 83: GWT App Architecture Best Practices

Test the PhoneEditor...and run it

PhoneEditor editor = new PhoneEditor(display, service); editor.editPhone(before); display.labelPicker.setValue("Work"); display.numberField.setValue("098 765 4321"); display.save.lastClickHandler.onClick(new MockClickEvent()); // Verify UpdatePhone action = (UpdatePhone) service.lastAction; assertEquals(new UpdatePhone(expected), action); Phone actual = action.getPhone(); assertEquals(expected.getLabel(), actual.getLabel()); assertEquals(expected.getNumber(), actual.getNumber());}

Page 84: GWT App Architecture Best Practices
Page 85: GWT App Architecture Best Practices

Strive to achieve statelessness

Page 86: GWT App Architecture Best Practices

• The browser embodies the session

• Server effectively stateless (except for caching)

• User should not notice server restart

• On AppEngine, MemCache works well with this attitude

o "Values can expire from the memcache at any time"

Disposable servers

Page 87: GWT App Architecture Best Practices

• Use GWT History right, and get it right early

• Back button, refresh as a feature, not a catastrophe

• Use Place abstraction

o Layer above Historyo Announce place change via event bus

Disposable clients

Page 88: GWT App Architecture Best Practices

Recall

Page 89: GWT App Architecture Best Practices

+Place

Page 90: GWT App Architecture Best Practices
Page 91: GWT App Architecture Best Practices

Q & A

Page 92: GWT App Architecture Best Practices