At the moment, I regularly have to develop Treegrid UI components. It’s been quite a lesson in API design and made me realize how design patterns exist to create great architecture, not a great API.
My recent work focuses on making treegrid components work with Crown’s backend. The backend contains a variety of data structures which, while being quite diverse, need to be manipulated through a common programmatic interface. To that end, I designed and implemented an adaptor, DDSTreeGrid
, around EasyUI’s Treegrid component and a declarative data represenation of our backend views (view
).
// Example adaptor interface
function DDSTreeGrid ($element, loader, mapper, view) {
this.getSelected = function () { ... }
this.selectAll = function () { ... }
this.deselectAll = function () { ... }
this.onSelectionChanged = function () { ... }
this.getEntries = function { ... }
// ...
}
// Example view structure
var view = {
label: "Users",
url: "api/users.json",
apiParams: { showfriends: "false" },
mappings: {
[ field: "Username", apiField: "uname" ],
[ field: "Full Name",
via: function (apiResponse) {
return apiResponse.forename + apiResponse.surname;
},
sortable: true
],
[ field: "Email", apiField: "email" ],
[ field: "Dummy column", always: "dummy value" ]
}
};
My code (below) parses view
structures to generate both AJAX loaders, via generateLoader
, and backend-to-frontend mappers, via generateMapper
. To increase flexibility, The resulting loaders and mappers are dependency injected into DDSTreeGrid
. This allows loaders and mappers to be decided at runtime:
var view = { /* see above */ };
var $treegrid = $("#registered-users-treegrid");
var loader = generateLoader(view);
var mapper = generateMapper(view);
var treegrid = new DDSTreeGrid($treegrid, loader, mapper, view);
Dependency injection sure is useful. This implementation was sucessfully deployed in a contact list, administrator panels, and clinical data views. Dependency injection does come with one annoying cosequence though: something further up the hierarchy now has the responsibility of resolving the dependencies. Ignoring dependency injection containers, that “something” is usually the developer.
As a consequence of dependency injection, any code wanting to use a DDSTreeGrid
need to construct a well-formed view
, a mapping function, and a loading function. While that’s logical to me, other developers are going to find it annoying. Worse, even I’ll probably forget how it works later down the line.
I’ve found that expecting developers, including myself, to study complex annoying APIs is unrealistic - there’s too much code in the world to comprehend it all. So, with my embarassment-averse future-self in mind, I present an engineering bodge, .ofView
. ofView
is a simple factory helper method on the DDSTreeGrid
class that internally injects the most commonly used dependencies:
// In the implementation:
DDSTreeGrid.ofView = function($element, viewName) {
// Get a view definition via its name by doing a
// schema lookup on the server. This is more rigid
// than bespokely coding a view but less hassle.
var view = DDSView.fromViewName(viewName);
var loader = generateLoader(view);
var mapper = generateMapper(view);
return new DDSTreeGrid($element, loader, mapper, view);
};
// In library user's code:
var $element = $("#my-treegrid");
var treeGrid = DDSTreeGrid.ofView($element, "BackendViewName");
.ofView
might seem like a cheap hack to circumvent dependency injection but it covers 90 % of use cases. DDSTreeGrid
’s constructor still covers the remaining 10 %. .ofView
helps in those “I need to get a TreeGrid showing quickly oh crap oh crap what dependencies did I need again?” situations that tend to crop up just before a critical deadline.
I’ve spent many long evenings coding “perfect” classes with full dependency injection, perfect parameters, and a perfectly pure outer-interface but, over time, I’ve come to appreciate the usefulness of a few “quick n’ dirty” helper methods to cover the “get the job done” situations. So that’s how I design code now - I design an “ideal” architecture on which easier-to-comprehend methods sit.
Perhaps I should publish this as a new design pattern - the “hide the underlying design patterns” design pattern. It’s a pattern that simplifies the otherwise modular nature of the codebase. A facade of simplicity, if you will. Oh god, wait a minute…