-
Notifications
You must be signed in to change notification settings - Fork 55
change pattern for attributes in ceylon.html #697
Description
Somewhere “fluent” APIs win over Ceylon’s named argument syntax is when there is inheritance. With a class that specifies a bunch of arguments (with potentially a lot being defaulted), every subclass that wants to allow these arguments to be specified during construction must explicitly declare the same arguments and specify the same default values.
This can become a burden and is especially bothersome if the class is open for other modules to inherit from. A parameter’s default value might be an implementation detail (and might change from version to version). Requiring that it be specified in a subclass out of its module is unacceptable.
Additionally, it makes it hard to extend the API: if someone wants to add a new parameter at the end of the class, one must do so for every single subclass. If there is a trailing iterable parameter, it becomes impossible to do so without ruining backward compatibility.
With the builder pattern, fluent APIs allow similar (yet slightly different) instances to be produced very easily.
Together with (and even without) the builder pattern, fluent APIs win over the current named argument pattern in almost every aspect. The only case I can see in which fluent APIs lose is when a subclass doesn’t want to allow a specific set of parameters of the superclass to be specified.
Now, differently from what you may have been thinking, I am not going to propose to use a fluent API on ceylon.html, I am actually going to propose an entirely new pattern. I call it “the attributes pattern”.
The attributes pattern is pretty straightforward. You just write an interface to represent the parameters (here called Attributes). For ceylon.html, it ought to look something like the following.
shared interface Attributes
{
shared Attribute<String> id = null;
shared Attribute<[String+]> classes = null;
shared Attribute<String> accessKey = null;
shared Attribute<Boolean> contentEditable = null;
shared Attribute<String> contextMenu = null;
shared Attribute<Direction> dir = null;
shared Attribute<Boolean> draggable = null;
shared Attribute<DropZone> dropZone = null;
shared Attribute<Boolean> hidden = null;
shared Attribute<String> lang = null;
shared Attribute<Boolean> spellcheck = null;
shared Attribute<String> style = null;
shared Attribute<Integer> tabIndex = null;
shared Attribute<String> title = null;
shared Attribute<Boolean> translate = null;
}Then, the Element class can look like:
shared abstract class Element(shared String tagName, Arttributes attributes = object satisfies Attributes {}, {Content<Node>*} children = [])
extends Node(...)
{
shared String? id = getAttribute(attributes.id);
shared [String+]? classes = getAttribute(attributes.classes);
shared String? accessKey = getAttribute(attributes.accessKey);
shared Boolean? contentEditable = getAttribute(attributes.contentEditable);
shared String? contextMenu = getAttribute(attributes.contextMenu);
shared Direction? dir = getAttribute(attributes.dir);
shared Boolean? draggable = getAttribute(attributes.draggable);
shared DropZone? dropZone = getAttribute(attributes.dropZone);
shared Boolean? hidden = getAttribute(attributes.hidden);
shared String? lang = getAttribute(attributes.lang);
shared Boolean? spellcheck = getAttribute(attributes.spellcheck);
shared String? style = getAttribute(attributes.style);
shared Integer? tabIndex = getAttribute(attributes.tabIndex);
shared String? title = getAttribute(attributes.title);
shared Boolean? translate = getAttribute(attributes.translate);
}Subclasses can be much shorter. A subclass that doesn’t introduce any new attributes can merely accept the same Attributes interface and pass it up to the superclass for it to use.
shared class Div(Attributes attributes = object satisfies Attributes {}, {Content<FlowCategory>*} children = [])
extends Element("div", attributes, children)
{
}However, subclasses that do need to declare more attributes can define a subtype of Attributes and specify them there.
shared interface FormAttributes
satisfies Attributes
{
Attribute<String> acceptCharset = null;
Attribute<String> action = null;
Attribute<Boolean> autocomplete = null;
Attribute<String>|Attribute<FormEnctype> enctype = null;
Attribute<String>|Attribute<FormMethod> method = null;
Attribute<String> name = null;
Attribute<Boolean> novalidate = null;
Attribute<String>|Attribute<FormTarget> target = null;
}
shared class Form(FormAttributes attributes = object satisfies FormAttributes {}, {Content<FlowCategory>*} children = [])
extends Element("form", attributes, children)
{
String acceptCharset = getAttribute(attributes.acceptCharset);
String action = getAttribute(attributes.action);
Boolean autocomplete = getAttribute(attributes.autocomplete);
String|FormEnctype enctype = getAttribute(attributes.enctype);
String|FormMethod method = getAttribute(attributes.method);
String name = getAttribute(attributes.name);
Boolean novalidate = getAttribute(attributes.novalidate);
String|FormTarget target = getAttribute(attributes.target);
}These classes can be instantiated very easily:
Div
{
object attributes
satisfies Attributes
{
classes = ["form-container", "foo-bar"];
lang = "en";
}
Form
{
object attributes
satisfies Attributes
{
classes = ["my-form"];
id = "main-form";
}
P
{
Label{ "Login", Input {}}
},
P
{
Label
{
"Password",
Input
{
object attributes
satisfies InputAttributes
{
type = password;
}
}
}
},
P
{
Button { "Submit" }
}
}
}To instantiate look‐alike elements, one can refine the non‐defaulted attributes, and make the changeable ones variable.
object fakeBuilder
satisfies Attributes
{
classes = ["foo-container", "pretty"];
dir = rtl;
shared actual variable Attribute<String> id = super.id;
}
fakeBuilder.id = "pretty1";
value div1 = Div {attributes = fakeBuilder;};
fakeBuilder.id = "pretty2";
value div2 = Div {attributes = fakeBuilder;};A class can make an attribute unspecifiable by refining it as non‐refinable in its own Attributes interface.
Of course, I am not going to deny that writing Div { lang = "en"; "Hello" } looks much nicer than writing Div { object attributes satisfies Attributes { lang = "en"; } "Hello" }, however, as shown here, the later, while more verbose, is much more flexible for both the writer of the class Div and for its users.