Component Models and Encapsulation (was Re: Component Model: Landing Experimental Shadow DOM API in WebKit)

I am not a fan of this API because I don't think it provides sufficient encapsulation. The words "encapsulation" and "isolation" have been used in different ways in this discussion, so I will start with an outline of different possible senses of "encapsulation" that could apply here.

== Different kinds of "encapsulation" == 

1) Encapsulation against accidental exposure - DOM Nodes from the shadow tree are not leaked via pre-existing generic APIs - for example, events flowing out of a shadow tree don't expose shadow nodes as the event target, 

2) Encapsulation against deliberate access - no API is provided which lets code outside the component poke at the shadow DOM. Only internals that the component chooses to expose are exposed.

3) Inverse encapsulation - no API is provided which lets code inside the component see content from the page embedding it (this would have the effect of something like sandboxed iframes or Caja).

4) Isolation for security purposes - it is strongly guaranteed that there is no way for code outside the component to violate its confidentiality or integrity.

5) Inverse isolation for security purposes - it is strongly guaranteed that there is no way for code inside the component to violate the confidentiality or integrity of the embedding page.


I believe the proposed API has property 1, but not properties 2, 3 or 4. The webkitShadow IDL attribute violates property #2, I assume it is obvious why the others do not hold.

I am not greatly interested in 3 or 4, but I believe #2 is important for a component model.


== Why is encapsulation (type 2) important for components? ==

I believe type 2 encapsulation is important, because it allows components to be more maintainable, reusable and robust. Type 1 encapsulation keeps components from breaking the containing page accidentally, and can keep the containing page from breaking the component. If the shadow DOM is exposed, then you have the following risks:

(1) A page using the component starts poking at the shadow DOM because it can - perhaps in a rarely used code path.
(2) The component is updated, unaware that the page is poking at its guts.
(3) Page adopts new version of component.
(4) Page breaks.
(5) Page author blames component author or rolls back to old version.

This is not good. Information hiding and hiding of implementation details are key aspects of encapsulation, and are good software engineering practice. Dmitri has argued that pages today do a version of components with no encapsulation whatsoever, because many are written by monolithic teams that control the whole stack. This does not strike me as a good argument. Encapsulation can help teams maintain internal interfaces as they grow, and can improve reusability of components to the point where maybe sites aren't quite so monolithically developed.

Furthermore, consider what has happened with JavaScript. While the DOM has no good mechanism for encapsulation, JavaScript offers a choice. Object properties are not very encapsulated at all, by default anyone can read or write. But local variables in a closure are fully encapsulated. It's more and more consider a good practice in JavaScript to build objects based on closures to hide implementation details. This is the case even though the closure approach is more awkward to code, and may hurt performance. For ECMAScript Harmony, a form of object that provides true encapsulation is being considered.

I don't want us to make the same mistake with DOM components that in retrospect I think was made with JavaScript objects. Let's provide good encapsulation out of the gate.

And it's important to keep in mind here that this form of encapsulation is *not* meant as a security measure; it is meant as a technique for robustness and good software engineering.


== Are there use cases for breaking type 2 encapsulation against the will of the component? ==

I'm not aware of any. I asked Dmitri to explain these uses cases on IRC and he didn't have any specific ones in mind, just said that exposing the shadow DOM directly is the simplest thing that could possibly work and so is easy to prototype and implement. I think there are other starter approaches that are easy to implement but provide stronger encapsulation.


== Are there other limitations created by the lack of encapsulation? ==

My understanding is yes, there are some serious limitations:

(1) It won't be possible (according to Dmitri) to attach a binding to an object that has a native shadow DOM in the implementation (e.g. form controls). That's because there can only be one shadow root, and form controls have already used it internally and made it private. This seems like a huge limitation. The ability to attach bindings/components to form elements is potentially a huge win - authors can use the correct semantic element instead of div soup, but still have the total control over look and feel from a custom script-based implementation.

(2) Attaching more than one binding with this approach is a huge hazard. You'll either inadvertently blow away the previous, or won't be able to attach more than one, or if your coding is sloppy, may end up mangling both of them.

I think these two limitations are intrinsic to the approach, not incidental.


== OK Mr. Fancypants, do you have a better proposal? ==

I haven't thought deeply about this, but here's a sketch of a component model that is just as trivial to use and implement as what is proposed, yet provides true encapsulation. It sticks with the idea that the only way to create a component DOM is programmatically. But it immediately provides some advantages that I'll expand on after throwing out the IDL:

interface Component {
    // deliberately left blank
}

interface TreeScope : Node
 {
    readonly TreeScope parentTreeScope;
    Element getElementById (in DOMString elementId);
}

 interface BindingRoot : TreeScope {
   attribute bool applyAuthorSheets;
   readonly attribute Element bindingHost;
 };

[Callback]
interface ComponentInitializer {
  void initialize(in BindingRoot binding);
};

 partial interface Document {
     Component createComponent(in Node template, in ComponentInitializer initializer);
 };

partial interface Element {
    bindComponent(in Component component);
    unbindComponent(in Component component);
}

The way this works is as follows:

(1) The component provider creates a DOM tree that provides templates for bindings that instantiate that component.
(2) The component provider also makes an init function (represented above as a callback interface) which is called whenever an instance of the component is bound (see below).
(3) The component provider calls createComponent with these things and gets back a Component object, which is completely opaque and exposes no implementation details (though if we wanted we could add some visible metadata attributes like a name or what have you) but can be bound to an element.
(4) The component provider hands back this token to its client.
(5) The client can instantiate the component as many times as it wants by binding it to one or more elements.
(6) When Element.bindComponent is called, it creates the BindingRoot, clones the template into it as children (it can be a DocumentFragment if desired), attaches it to the element, and then calls the initializer function on the newly created binding root, so that it can set up event listeners and such.


Notice that this scheme is not significantly more complex to use, spec or implement than the shadow/shadowHost proposal. And it provides a number of advantages:

A) True encapsulation is possible, indeed it is the easy default path. The component provider has to go out of its way to deliberately break encapsulation, though of course it can if it wants to.
B) Binding is atomic - the shadow root is not built up incrementally so you can't have a half-built binding.
C) Has a natural extension to allowing more than one binding on an element, having a stacking behavior like XBL.
D) Can readily support custom components bound to form controls, with either semantics of stacking on top of the native binding or replacing it.
E) Avoids use of the jargon-ish, mysterious and potentially confusing term "shadow" in favor of "binding" and "component" which are much more clear IMO.
F) The code to build up the DOM for the binding with raw DOM calls only has to run once. After that it just gets cloned, which is likely faster than creating a whole fresh one with raw DOM calls.
G) Naturally extensible to other ways of creating components, perhaps on a declarative template a la XBL2.


I hope this comprehensively expresses my concerns, and I believe I have shown at least one way in which my concerns about encapsulation can be addressed without adding unwarranted complexity.


Regards,
Maciej

Received on Thursday, 30 June 2011 04:06:20 UTC