Contrasting Three Projects
Three projects stand out in my memory as vivid examples of how dramatically domain design
practice can affect development results. Although all three projects delivered useful software, only
one achieved its ambitious objectives and produced complex software that continued to evolve to
meet the ongoing needs of the organization.
I watched one project get out of the gate fast, by delivering a useful, simple Web-based trading
system. Developers were flying by the seat of their pants, but this didn't hinder them because
simple software can be written with little attention to design. As a result of this initial success,
expectations for future development were sky-high. That is when I was asked to work on the
second version. When I took a close look, I saw that they lacked a domain model, or even a
common language on the project, and were saddled with an unstructured design. The project
leaders did not agree with my assessment, and I declined the job. A year later, the team found
itself bogged down and unable to deliver a second version. Although their use of technology was
not exemplary, it was the business logic that over-came them. Their first release had ossified
prematurely into a high-maintenance legacy.
Lifting this ceiling on complexity calls for a more serious approach to the design of domain logic.
Early in my career, I was fortunate to end up on a project that did emphasize domain design. This
project, in a domain at least as complex as the first one, also started with a modest initial success,
delivering a simple application for institutional traders. But in this case, the initial delivery was
followed up with successive accelerations of development. Each iteration opened exciting new
options for integrating and elaborating the functionality of the previous release. The team was able
to respond to the needs of the traders with flexibility and expanding capability. This upward
trajectory was directly attributable to an incisive domain model, repeatedly refined and expressed
in code. As the team gained new insight into the domain, the model deepened. The quality of
communication improved not only among developers but also between developers and domain
experts, and the design—far from imposing an ever-heavier maintenance burden—became easier
to modify and extend.
Unfortunately, projects don't arrive at such a virtuous cycle just by taking models seriously. One
project from my past started with lofty aspirations to build a global enterprise system based on a
domain model, but after years of disappointment, it lowered its sights and settled into
conventionality. The team had good tools and a good understanding of the business, and it gave
careful attention to modeling. But a poorly chosen separation of developer roles disconnected
modeling from implementation, so that the design did not reflect the deep analysis that was going
on. In any case, the design of detailed business objects was not rigorous enough to support
combining them in elaborate applications. Repeated iteration produced no improvement in the
code, due to uneven skill levels among developers, who had no awareness of the informal body of
style and technique for creating model-based objects that also function as practical, running
software. As months rolled by, development work became mired in complexity and the team lost
its cohesive vision of the system. After years of effort, the project did produce modest, useful
software, but the team had given up its early ambitions along with the model focus.
Story of Eric Evans from his book http://dddcommunity.org/book/evans_2003/
The Registration Problem
Consider this: you want customers to register with your site. When they do, a number of things need to happen:
In a typical scenario, there's probably more - but let's use this for now.
This is some code that you might see in a Customers module:
This code is not only hideous-looking, it's also synchronous and a nightmare to maintain. Node allows you to do this much better with EventEmitters. Let's see how to use Events to clean this code up.
There are two ways to do this: encapsulate the eventing, or make your entire object an EventEmitter through inheritance. I'll do the latter.
The first thing to do is reference Node's event module and the util module as well - it has some helpers we'll need. Then we rewire the module to handle the events - I'll explain in a second, but here's the final code:
So what's going on here? Well first - there are no more callbacks - we don't need them! We have events to listen to.
Node helps you with this using the "util" library. On line 42 we're telling the util to push the prototype from events.EventEmitter onto our Customer function. Notice that this is a function, not an instance of a function as I had in the first example above.
Next, on line 7, I had to invoke the "base" constructor to be sure that I don't miss any internal instancing or setting of values. Turns out for EventEmitters you don't need to do that and you can omit this line - but it's safe to just do it, no matter what.
In the body of each method I'm simply "emitting" an event to all listeners (there's obviously some code missing here - pretend that I have an insert routine and so on). I can emit an event for whatever happens along the way - a successful validation fires "validated", a failure might fire "validationFailed". This frees up our code to do what it needs to do and no more, making it much cleaner and clearer.
On line 29 I've added a final event trigger if everything works out: "successfulRegistration". This is what calling code will really be interested in the most - either that or "failedRegistration" - and we pass along the customer record (more on that in a second).
On lines 35 through 38 we've implemented a bit of workflow. This doesn't need to be inside the Customer function - you can arrange these events wherever and however you like. The calling code can remove every event listener and replace it with its own if it wanted to reorganize the flow here.Speaking of calling code, here's what it might look like:
You hook into the events, then run register() and respond as needed.
All Over The Place
Node is built on top of EventEmitters - you'll find them everywhere. Understanding them is key to writing cleaner code that's more functional and maintainable - and it also helps keep things asynchronous.
n the first example, we had a synchronous drop all the way down - even though we were using callbacks. Our code above isn't synchronous at all - we've hooked into an event and when Node is ready, it will process the emitted event callback.