Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[handly-dev] Proposal for a new design around the model API

Proposal for a new design around the model API
==============================================
 
Recently, I was thinking a lot about a particular problem with the current design: potential conflicts between framework and model-specific methods, which can affect the model API.
 
Let's say we have a model interface such as:
 
  interface IJavaElement { // doesn't extend IHandle
    IJavaElement getParent();
  ...
 
and would like to implement it using Handly:
 
  class JavaElement extends Handle implements IJavaElement {
    @Override
    IJavaElement getParent() {
  ...
 
This class doesn't compile. Its getParent() method definition conflicts with the inherited getParent() method that returns IHandle.
 
The code above would compile if IJavaElement extended IHandle, but method conflicts can still arise in other cases, as the following examples demonstrate.
 
* This method definition conflicts with the inherited getChildren() method that returns an array:
 
  interface IJavaElement extends IHandle {
    List<IJavaElement> getChildren();
  ...
 
* This class doesn't compile, because the class Handle already defines a close() method with the final modifier (and completely different semantics):
 
  class JavaProject extends Handle implements IJavaProject {
    @Override
    void close() {
  ...
 
  interface IJavaProject extends IHandle {
    void close(); // closes the underlying IProject
  ...
 
And what if the Handle.close() method would be introduced in a future version of Handly, when the above class had already been successfully defined?
 
* For a real-world example, recall the ISourceElement#getModule() discussion:
 
  https://dev.eclipse.org/mhonarc/lists/handly-dev/msg00102.html
 
These and similar examples indicate a general problem with the current design.
 
As you may know, Handly 0.4 has introduced an adaptation facility, which can be used, among other things, to define an "adapter model" to a Handly-based model. Then, the Handly-based model can become an implementation detail that clients need to know nothing about: clients deal only with the "adapter model", which API is defined and fully controlled by the model implementor.
 
The adaptation approach works, but it is not a complete solution (method conflicts can still arise in the Handly-based model that underlies the "adapter model"), it is quite complicated (effectively, two models need to be defined and maintained), and it may incur some overhead. I thought it would be good to try and investigate alternative solutions to the problem.
 
To resolve method conflicts, the model implementor would have to choose a different method name in the above examples. But if, for example, IJavaElement were a preexisting interface we had to keep contract compatibility with, we would not be able to change a method name in that interface. Hence, to solve this problem in a general way we should somehow guarantee that methods of model-related Handly API (both in interfaces such as IHandle and in base implementation classes such as Handle) would never conflict with model-specific methods defined by the model implementor.
 
Of course, there can be no such guarantee in a strict sense, but imposing a unique naming convention could greatly reduce the probability of a conflict. That's exactly what EMF does, for example. The "e" prefix for method names in EObject and EObjectImpl significantly reduces the probability of a conflict between framework and model-specific methods.
 
So, a straightforward solution could be to introduce a special naming convention for model-related API methods in Handly (e.g. the common "h" prefix). Thus, getName() would become hName(), getParent() would become hParent(), exists() would become hExists(), etc. Of course, it would be a major breaking change, but let's ignore that for now.
 
However, there is a problem with this straightforward, EMF-like approach: it is a bit *ugly*. Worse, if the model implementor still chooses to extend the model API from Handly interfaces, which is convenient in many cases, the model's clients will have to deal with a quite terrible mix of the "h-methods" and the "normal" methods.
 
Here's my current thinking on how we could make it a little less ugly.
 
Proposed redesign
-----------------
 
Note: Although not exactly required in a technical sense, since it's a major breaking change anyway, I would like to also propose renaming model-related API interfaces/classes for greater consistency and less ambiguity: IHandle -> IHElement, IHandleDelta -> IHElementDelta, IElementChangeEvent -> IHElementChangeEvent, etc.
 
So, the key concept is H-element - I quite like the "abstractness" of the name - represented by the interface IHElement.
 
H-elements are value objects (immutable, equal by value) that represent a model element.
 
H-elements can be reflected in a mirror - IHElementMirror. The mirror has methods such as #hElement(): returns the mirrored element, #getParent(): returns the element's parent H-element, #getChildren(): returns the element's child H-elements, etc.
 
An H-element provides a default mirror - IHElement#hMirror(), and can be a self-mirror, i.e. it can implement IHElementMirror itself and return "this" as its mirror.
 
There is an inheritance hierarchy of H-elements: IHElement -> IHSourceElement -> IHSourceFile, and a corresponding hierarchy of their mirrors: IHElementMirror -> IHSourceElementMirror -> IHSourceFileMirror.
 
An H-model is a containment tree of H-elements. The model can be introspected via element mirrors.
 
There is a hierarchy of base implementation classes for H-elements: HElementBase -> HSourceElementBase -> HSourceFileBase. The classes implement "implementation contracts": IHElementImpl -> IHSourceElementImpl -> IHSourceFileImpl, which the default implementations of element mirrors depend on.
 
There is a naming convention that the name of an API method both in the "implementation contracts" and in the base implementation classes begins with the "h" prefix. Such naming convention significantly reduces the possibility of a conflict with a method defined by the model implementor.
 
To define the model API, the model implementor can choose from three strategies:
 
1. The interface for a model element extends both IHElement and IHElementMirror. The element is a self-mirror. This is very similar to what has been available since Handly 0.1. While simple, this approach is not very flexible and is not very safe (method conflicts can still potentially arise). However, in many cases, it may be sufficient.
 
2. The interface for a model element extends the minimal IHElement (only). The model implementor is free to define any additional methods and form the model API to his liking. This approach enables a number of benefits:
 
  * Complete control over the model API by the model implementor
  * Safer evolution of both the model API and Handly API
  * The ability to implement a preexisting model API using Handly
  * The ability to tailor Handly API to "generic infrastructure" use (e.g. using arrays in Handly API methods is no longer an issue necessarily exposed to the model's clients)
  * All without incurring the "adaptation cost"
 
3. The interface for a model element doesn't extend IHElement or IHElementMirror, so it doesn't depend on Handly at all. The drawback is that explicit casts to IHElement might be required to interact with a generic functionality (like utility methods) expressed in terms of H-elements. That's why the second strategy is generally preferable to the third.
 
Similar to the IHElement design, IHElementDelta can be introspected using HElementDeltaMirror. The class HElementDeltaBase provides the base implementation and implements the "implementation contract" IHElementDeltaImpl; the names of the API methods likewise begin with "h". The same three strategies are available for defining the model-specific API of the delta.
 
A prototype of the new design
-----------------------------
 
See the archived 'newmodel' workspace project attached, which is a very early and greatly simplified prototype, but can illustrate the direction.
 
Package info:
* model - core interfaces related to model
* model.impl - "implementation contracts" and base implementations
* model.test - illustrates the first approach to defining the model-specific API
  * Note the simple and convenient "self-mirror" elements
* model.test2 - illustrates the second approach
  * Note the flexibility in defining the model API, such as choosing the method names freely, using lists instead of arrays for return values, etc.
* model.test3 - illustrates the third approach
  * Note explicit casts to the basic H-element interfaces
 
Note that default methods are actively used to "mix-in" common behavior, so Java 8 is required. However, Java 8 would be useful even in the context of existing design (bug 474391). Besides, Eclipse Neon already requires Java 8.
 
Open questions:
* Should we even have the IHElement#hMirror() method?
  * There might be a static HElements#getMirror(IHElement) method instead.
* Shouldn't the mirrors truck in themselves?
  * E.g. IHElementMirror#getParent() might return IHElementMirror rather than IHElement.
 
Closing words
-------------
 
Fixing the above-stated deficiency of the current design is a lot of work. Worse, it is a major breaking change imposed on the existing adopters. That's why it is important that we all understand and agree on the solution, its costs and benefits.
 
Let a careful discussion begin!

Attachment: newmodel.zip
Description: Binary data


Back to the top