Avoiding a Domain Model

I’m an object oriented kind of developer.  I likes a strong domain model, a clear separation of responsibility, and all of the goodness that makes code clean.  Why, then, am I so resistant to introducing a domain model into the FreeIPA WebUI?

Simple:  FreeIPA already has a strong domain model.  It is server side, and driven by the plugin architecture.  What we are writing for the WebUI is deliberately avoiding implementing business logic.  We slip sometimes, but not for long.  Our design goal is for the WebUI and the CLI to provide the same functionality, just presented differently.

I’ve been a system administrator on more than one occasion.  Sysadmins script things.  You might use GUI for one off actions, but the command line is still king.  It is not the biggest tool in the tool box, it is a toolbox all its own, and we value it highly.

The risk of a GUI is that it will provide some functionality that cannot be easily scripted.  Then, the sysadmin is stuck with a time consuming task.  I’ll avoid mentioning former projects by name, but if you’ve worked with me, you can probably guess which ones have cause me the most pain in the past.

The FreeIPA domain model projects into the WebUI via Metadata.  Metadata is a JSON call on a plugin just like any other, but it should always return the same values, with the exception of Internationalized and localized messages.  The meta data provides  all of the information we need in order to create a domain model.  Thus, if were were to code one by hand, we would likely be spending more time synchronizing it with the real model.

There is another aspect.  Our code base is attempting to drive strong user interaction and a positive user experience.   The focus of our code, the UI domain model is the components of the user’s world.  While we want to be able to write custom components for a domain object, it is not the norm.  A control for displaying associations might start of to the relationship of users and roles, but will quickly get refactored into a general component and a small customization layer.  This is a continuous process.  We need an approach that emphasizes and reinforces refactoring.

Martin Fowler recently published a book on Domain Specific Languages (DSL).  This valuable addition to the body of software development literature lays out many of the issues and approaches to building an application programmers interface (API) that is tailored to your domain object.  Our goal was to make a DSL that was specific to user interface.

The main objects of our DSL are:

  • Entities:  the objects from the domain model
  • Facets:  different views of the entities.  Specifically, we have search (a list view),details (a property view) and associations.
  • Widgets:  The visual elements of the page.

Here’s how the Hostgroup entity  looks in our DSL:

IPA.entity_factories.hostgroup = function() {

    return IPA.entity_builder().
        entity('hostgroup').
        search_facet({
            columns:['cn','description'],
             add_fields:['cn','description']}).
        details_facet([{
            section:'identity',
            label: IPA.messages.objects.hostgroup.identity,
            fields:['cn','description']
        }]).
        association_facet({
            name: 'memberof_hostgroup',
            associator: IPA.serial_associator
        }).
        standard_associations().
        build();
};

It is declarative. While strictly speaking it is not JSON, very little imagination is required to see how it could be implemented in strict JSON.

We’ve introduced a builder object as it helps with a common problem: How do you maintain interim state in a declarative object creating process. I wrote about this issue in an earlier post.

The declarations have to balance consistency and the requirements of the declared object. The most common case for qa widget in our code is a text widget that gets all of its necessary display information from the metadata. Thus, if can declare fields as a simple string, that string is used as the key for the rest of the information.

If, however, the field is better displayed using a more complex widget, the syntax gets more verbose, but also more explicit. Here’s a chunk from our user object:

 {
   section: 'contact',
   fields:
   [  {factory: IPA.multivalued_text_widget, name:'mail'},
      {factory: IPA.multivalued_text_widget, name:'telephonenumber'},
      {factory: IPA.multivalued_text_widget, name:'pager'},
      {factory: IPA.multivalued_text_widget, name:'mobile'},
      {factory: IPA.multivalued_text_widget, name:'facsimiletelephonenumber'}
   ]
},

A section is a portion of a details facet that can expand and collapse. The contacts section is composed of fields like telephone, and email that are one to many relations with the entity. A given user might have two different email addresses, a cell phone, a desk phone, and a fax. The multivalued widget supports add, delete, and multiple line reset.

Notice that the function used to construct the widget is passed in as a part of the declaration. Most of our Widgets take a dictionary object, or spec, as the sole parameter in the constructor. We use the declaration as that spec, and extract the factory object from it.

The builder can also provide additional context required by setting additional keys in the spec when composing the new object, prior to calling the factory. For example, the section object actually has a ‘name’ property, but we use the key ‘section’ in the declarative approach as it better documents the intention. Here’s the builder code:

 factory = field.factory;
 field.factory = null;
 field.name = field.section;
 field.section = null;
 current_dialog.add_section(factory(field));

So when I state that I am avoiding a domain model, really what I mean is that I consider my domain to be visual components. The domain of FreeIPA is managed with a very strong domain model already, and I do not wish to complete with it. The two models provide a complementary approach. The declarative approach to entity view definition provides the glue between reusable visual component and our domain model.

I’m going to append Endi’s objection to the end here, as this whole essay grew out of design discussions we’ve been having:

The UI ‘domain model’ is different than server domain model. IPA server is a service oriented application. Currently the service is available via XML or JSON RPC interface. The UI domain model is simply an abstraction or a language binding for this interface. It provides a native API for the UI instead of forcing us to deal with the RPC protocol. The language binding doesn’t contain any business logic, so it doesn’t compete with server’s domain model.

We actualy already have an abstraction with IPA.cmd(). The problem with this interface is that it is procedural instead of object oriented. It is also very low level, everywhere it’s invoked you have to know the method name, the parameter names, and the structures of JSON objects. It’s becoming a maintenance problem as the UI grows more complex. What if someday we decide to replace JSON with another RPC mechanism? The impact on UI will be extensive.

A higher level interface will encapsulate this problem from the rest of the UI application. This interface will be object oriented with classes and methods that mimic the server domain model which is more intuitive without making any assumption about the protocol.

There are many examples for this. The one that I just saw is Selenium language bindings:
http://seleniumhq.org/docs/05_selenium_rc.html#programming-your-test

I then suggested Autogenerating the domain logic, which is probably my preference, at least to start:

Auto generating language binding is separate issue. Sometimes it’s possible (e.g. Apache Axis), but I don’t know if our metadata contains enough info to auto generate JS API. Also, I don’t know if there are tools that can help us with this. It’s possible that auto generating will take much more effort than manually generating the language binding. It could be a completely separate project.

Auto generating or not, I think UI domain model/language binding is a necessary abstraction in the long run.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.