Module Pattern

DEPRECATED Pattern sample

https://github.com/mozilla-b2g/gaia/blob/v1.3/apps/system/js/window_manager.js

Pattern detail

Just don't use that.

The bigest change in new window management system is to deprecate the usage of module pattern. In my opinion, the main reason to use module pattern is to enable private variables. But this is a strong requirement, and the drawback it brings to us is much more than the value of private variables and functions.

The main problem is the private variables/method inside module pattern are not that easy to test. Yes, you could still work hard to figure out all the code path to trigger the inner function, but that is in-efficient and a simple function in object literal saves your time.

Creation Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/53649e7abba84b24bd2b9a391ef92ab7029d675f/apps/system/js/app_window.js#L42

Pattern detail

The initial purpose of this change is described in the comment.

  /**
   * AppWindow creates, contains, manages a
   * [mozbrowser](https://developer.mozilla.org/en-US/docs/WebAPI/Browser)
   * iframe. AppWindow is directly managed by AppWindowManager,
   * by call resize(), open(), close() on AppWindow.
   *
   * Basically AppWindow would manipulate all mozbrowser events
   * fired from the mozbrowser iframe by itself and show relevant UI.
   */

As above, it's some kind of mozBrowser API wrapper class.

In the very beginning, the main purpose is to create a mozbrowser iframe and load the URL of the app into the iframe.

  window.addEventListener('launchapp', function(evt) {
    var iframe = document.createElement('iframe');
    iframe.src = evt.detail.url;
    document.body.append(iframe);
  });

But after the system grows, plenty of features are added,

  • open/close transiton on the iframe, switching app.
  • modal dialog from window.alert()
  • more mozbrowser events to represent the state of the app. An object to represent the state of each iframe is necessary. Moreover:
  • DOM element create/destroy per instance
  • Event handler per instance
  • Diverged object type by inheriting the base class and override the method.

We are using Object.create to create the different window now.

var AppWindow = function() {
};
AppWindow.prototype.open = function() {
};

var HomescreenWindow = function() {
};
HomescreenWindow.prototype = Object.create(AppWindow);
HomescreenWindow.prototype.constructor = HomescreenWindow;

Remember to reassign the constructor after Object.create,
otherwise the reference to this.constructor will point to the super class.

Mixin Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/1de61ebc86e0c8a381f7e169fe01b764a6e03c9e/apps/system/js/browser_mixin.js

Pattern detail

A mixin is another object/class which is "dumped" into current class. The object could be standalone, swapped in the future, or be dumped into another class.

The way how we dump is simple: iterate the mixin object and append it to the target prototype.

// browser_mixin.js, a standalone file.

var BrowserMixin = {
  a: 0,
  b: function() {}
};

// app_window.js

var AppWindow = function() {
};
AppWindow.addMixin = function(mixin) {
  for (var k in mixin) {
    AppWindow.prototype[k] = mixin[k];
  }
};
AppWindow.addMixin(BrowserMixin);

Proxy Pattern

If you look at the Mixin Pattern sample closer, you will find browser_mixin.js is also a sample of proxy pattern. What is a proxy? It creates one more layer to access the mozBrowserAPI: setVisible, getScreenshot, reload... on the mozBrowser iframe. The mediator of AppWindow should not call these API directly but encouraged to call the proxy. The proxy will protect the API at certain degree.

var BrowserMixin = {
  reload: function() {
    if (!this.browser.element) {
      return;
    }
    this.browser.element.reload();
  }
};

Facade Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/53649e7abba84b24bd2b9a391ef92ab7029d675f/apps/system/js/app_window.js#L217

Pattern detail

This pattern might be a most used one but people won't they are using it. Facade encapsulate the detail inside a function and the user of the function don't need to know the detail.

When we want to bring an app from background to foreground,
we do the following:

  • Attach a repaint event listener to the browser iframe and remove the screenshot layer after repaints.
  • Call browser API to bring the iframe to foreground.
  • Remove the aria-hidden attribute from the DOM element to make sure the screen reader see this.

The caller of setVisible(true) don't need to know the detail but just use it.

Here is one another example using facade pattern.

var AppWindow = function() {
};
AppWindow.prototype.ready = function(callback) {
  this.debug('requesting to open');
  if (!this.loaded || this._screenshotOverlayState == 'screenshot') {
    this.debug('loaded yet');
    setTimeout(callback);
    return;
  } else {
    var invoked = false;
    this.waitForNextPaint(function() {
      if (invoked) {
        return;
      }
      invoked = true;
      setTimeout(callback);
    });
    if (this.isHomescreen) {
      this.setVisible(true);
      return;
    }
    this.tryWaitForFullRepaint(function() {
      if (invoked) {
        return;
      }
      invoked = true;
      setTimeout(callback);
    });
  }
};

Knowing an AppWindow is ready to perform the opening animation is complex and full of business logic:

  • If it's never loaded or is protected by screenshot overlay, callback right away.
  • If it's loaded once, try to trigger the repaint before we open it; otherwise the user will notice the screen blinks.
  • Create a timer to protect this operation. Infinite wait is not tolerable. The user of the ready() don't need to worry about the logic but just give the callback function.

Factory Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_window_factory.js

Pattern detail

Exactly, there's no strict factory pattern.
Whenever gecko/platform asks us to open a new window, we will get an event.
AppWindowFactory will instantiate a proper AppWindow instance from the event detail,
or bypass the event to the proper AppWindow's childWindowFactory.

Singleton Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/homescreen_launcher.js#L230

Pattern detail

Although HomescreenWindow is instantiable, we still want to get the same instance each time we need it. HomescreenLauncher provides an interface to query the instance.

Finite State Machine Pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_transition_controller.js

Pattern detail

I am not using a general state machine but having a specific transition state machine to deal with the open/close request on the AppWindow object.

A regular transition could be defined as 4 states: closed, opening, opened, closing.

What we care:

  • State change needs to tell others.
  • We are using animationend event to switch from opening state to opened state; and from closing state to closed state.
  • But sometimes, we will lose the animationend event. We cannot wait infinitely in opening state and in closing state. We need to create timeout event to force the state change.
  • We are bypassing the opening and closing states if the open/close animation is requested as 'immediate'.

Mediator Pattern

AppWindowManager is the mediator of all AppWindow instances.
All AppWindow don’t need to know each other when they are trying to do UI operation.
If the operation affects the other classes, they should publish a request. Mediator will check for them.

var AppWindow = function() {
  this.request('open'); // Let's visit observer later.

  // The instance is requesting to open by passing itself to the mediator.

};

var AppWindowMediator = function() {
  window.addEventListener('apprequestopen', this);
};
AppWindowMediator.prototype.handleEvent = function(evt) {
  // Mediator plays the role as 'permission check'

  if (this.readyToOpenANewApp) {
    evt.detail.open();
  } else {
    // wait until we are ready and do the open operation.

  }
};
var a = new AppWindow();

A real use case is, when an instance requests to open, we need to animate current displayed instance. But according to the rule "module should not know each other's state", we(AppWindow) are not supposed to do close operation to other instance and we don't even know who is currently opened instance.
AppWindowManager(AppWindowMediator) should deal with the open request and make sure the current opened instance is closing.

Observer pattern

Pattern sample

https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_window.js#L1189

Pattern detail

The observer pattern is highly used to prevent tight coupling. However, we are having a more complex event machenism right now. Let's step by step look into it.

Simple Observer

We are using DOM events to realize the publish/subscribe pattern right now.

// publisher

var AppWindow = function() {
  this.element = document.createElement('div');
  document.body.appendChild(this.element);
  this.publish('created');
};
AppWindow.prototype.PREFIX = 'app';
AppWindow.prototype.publish = function(name) {
  this.element.dispatchEvent(new CustomEvent(this.PREFIX + name, {
    detail: this
  }));
};

// subscriber

window.addEventListener('appcreated', function(evt) {
  console.log(evt.detail);
});

The subscriber don't need to know the DOM element to attach the event listener because the event will be bubbling to the window.
The publisher is encourged to have an event prefix to represent itself. Usually it's the class name.
Also note the event detail including the whole object so we could access any information we want in the subscriber, even calling the method.

2 phase observer

AppWindow is not only a simple module but also plays as a mediator of all sub modules inside it.
The event processing is indeed including two phases:

  • Private event phase - module inside the AppWindow will notice the private event.
  • Public event phase - module outside the AppWindow will notice the public event after all private events are resolved.

Let's go through the sample.

// sub_module.js

var SubModule = function(app) {
  this.app = app;
  this.app.element.addEventListener('_opened', function() {
    console.log(Date.now());
  });
};

// app_window.js

var AppWindow = function() {
  this.element = document.createElement('div');
  document.body.appendChild(this.element);
  this.subModuleA = new SubModule(this);
  this.publish('created');
  // Perform the opening animation...

  this.publish('opened');
};

AppWindow.prototype.PREFIX = 'app';
AppWindow.prototype.publish = function(name) {
  this.broadcast(name);
  this.element.dispatchEvent(new CustomEvent(this.PREFIX + name, {
    detail: this
  }));
};
AppWindow.prototype.broadcast = function(name) {
  this.element.dispatchEvent(new CustomEvent('_' + name, {
    detail: this,
    bubble: false
  }));
};

// somewhere.js

window.addEventListener('appopened', function() {
  console.log(Date.now());
});

The _opened subcriber is handled before appopened subsriber.
This is to ensure the UI consistency:

  • When we want to tell others something happens, we need to settle everything corresponding to the event before going public.
  • When something happens but it's not public enough, use broadcast() to tell the modules which are managed by us. It would be dispatched on this.element.
  • The underline before private event is a convension. Internally we don't need a class-like prefix.

To understand the two phase observer, let's treat AppWindow as the local government, and the mediator of AppWindow is the central government.
The central government will know something happens in local government in the long run, but the local government is responsible for manipulating the events at first. In the local government there are many different departments which have different responsibilities. Some of them might be interested in the same thing but has different behavior when they know the event happens.

// local government

function ModuleA(app) {
  this.app.addEventListener('_onfire', function() {
    // pull water

  });
}
function ModuleB(app) {
  this.app.addEventListener('_onfire', function() {
    // rescue

  });
}
var app = new AppWindow();
var departmentA = new ModuleA(app);
var departmentB = new ModuleB(app);

// central government

window.addEventListener('apponfire', function() {
  // do something..

});

app.publish('onfire');
// The _onfire observer in moduleA and moduleB

// will be invoked before the global observer knows.
Mediator as Observer

For some honored reason, AppWindow is playing the role of Mediator as its opened window now.
That is to say, if an app is using window.open(), the newly created AppWindow instance is managed by the caller instance.

But note we are using DOM events to pass the events, if we don't do something, the observer of the outer instance will be invoked if the inner instance triggers some event.

What we will do? Amend the subsribe/publish function() is the solution.
Mediator could stop the event propagation if necessary.
https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_window.js#L990

Command Pattern

Live pattern sample

https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_transition_controller.js#L109

Pattern detail

Honestly I don't know this is a pattern before I see it.
We are using the command pattern in the realization of finite state machine.

When we want to transite the state from A to B by event Z,
we will trigger 3 functions:

  • Leaving A State
  • Handle Z Event
  • Entering B State

Using switch or if-else is something really bad especially if you are having more states and more events. So we are using the same semantic to invoke the function:

  this['_leave_' + previousState](evt);
  this['_handle_' + evt]();
  this['_enter_' + currentState](evt);

The centralized event handler in AppWindow uses the same idea to enhance handleEvent interface.
(https://github.com/mozilla-b2g/gaia/blob/ea96ccb102b67e6cd09903271999e30d3e4334f0/apps/system/js/app_window.js#L994)

Comments

comments powered by Disqus