Designing an OO Backend

As follow-up for my 'Impetus for an OO Backend' post from a couple
weeks ago, I wanted to talk about some approach designing an OO
backend. Again, I'm going to attempt brevity and undoubtedly end up
being long winded, and I'm going to again intentionally skip
implementation details. Finally, there will be almost no mention of UIs
at all; I'm talking exclusively about the backed of an application. UIs
will use the exposed APIs to do "stuff", based on user input. How they
do it is a totally different discussion.

For the sake of having a consistent example, I'm going to be talking
about some unnamed app that deals with users. No need for more specific
than that, I don't think, as pretty much every CF developer has had to
build such functionality for one app or another, or at the very least,
had to use such an application.

To jump into the meat of things, the first concepts are really the
central tenets of good OO design: abstraction and encapsulation.
Abstraction refers to the separation of a thing's interface from it's
implementation. In other words, someone can be told what this thing
does without having to know how it does it. Encapsulation refers to
the state of being self-contained; not depending on other things. The
"black box" on an commercial airliner is a good example of both.
Everyone knows what it does (records everything about the plane), but
most people don't know how (for instance, what's the recording media?),
and it's also totally self contained so that if the plane completely
breaks, it'll keep functioning. These two core concepts are the central ideas of almost
every good OO design.

But on to our user management app. Pretty simple, right? Add a user,
delete a user, edit a user, and get a list of users in the system.
First, where does abstraction fit in? Well, I just told you what the
app needs to do, but I haven't told you anything about how it does it,
so I'd say we've found it. But how do we implement the abstraction? For that, we
need a service object.

Service objects are objects that expose "business operations" to
something. I listed the four core business operations of our app above
(add, edit, delete, and list users), so that'll be what's exposed in
our service object. Services are also singletons, which means that
there should never be more than a single instance of them within an
application. Since they're singletons, that also means that they must
be thread safe, but again, that's a whole different topic.

So now that we've got our service (named UserService, of course),
how do we use it? The easiest answer is to do <cfset application.userservice = createObject("component", "userservice").init(application.dsn) /> during application
startup (in Application.cfc or Application.cfm), modified
to reflect whatever specific parameters the service needs. Now the
whole application can access the service (via the application scope),
and since the createObject call only happens at application startup,
we're [mostly] assured that there will only be a single instance of the
component. As a note, this isn't my recommended technique for larger
apps, but for this scenario, it's perfectly adequate.

Now it doesn't seem like we've gotten much out of packaging the
business operations up in this fashion. We have a nice object with
everything in it, sure, but that just makes it harder, because it's a
single large file, right? Those are valid concerns (which can be dealt
with later), but that arrangement brings a very important benefit that
isn't quite so obvious. Your UI knows nothing about the business logic
of the app, except that a given method of application.userservice, when
passed a certain set of parameters, will do a certain thing.  I.e., the what but not the how.  That's a
ridiculously beneficial thing to be able to say, because it means the
implementation of the methods can change at will, and the UI needn't
care at all, as long as the end result is the same. If you don't see
why that's a wonderful trait to have, keep reading.

Lets say we've got our app running in production, and then the
managers come back with a set of changes, as they always do. But
they're not just little tweaks, they're some pretty massive changes to
core functionality. So you go off and basically gut your UserService
object to implement these changes. But while you're doing, you're
careful not to change any of the cffunction and cfargument tags. When
you're done, guess what? You're done. Since the methods didn't change,
your UI needn't change, so you don't have any more coding to do, and
your testing should be equally streamlined. That, my friends, is the
power of abstraction.

So what have we learned? Having a clearly defined API (exposed via
service objects) that your UI connects to can save you enormous amounts
of work as changes happen over time. That same API also lets you
redesign the backend, as well as change the business logic, without affecting anything else.

Now lets say you're working along, as it comes time to add some new
functionality that doesn't tie directly to users (lets say document
creation). Time for another service object: DocumentService, which will
have the corresponding effect of expanding our application's API. Same
rules apply, except now our UserService isn't the entire backend, so
we're not longer completely encapsulated. It's possible, likely even,
that the DocumentService will need to call methods on the UserService.
This is different then the UI calling methods, since the API the UI
uses doesn't know about implementation, but the guts of the
DocumentService does (the guts, after all, are the implementation).  We also don't want the service objects talking to the application scope, because there's no guarentee that application.userservice will always be there.  It might be server.userservice, or application.services.user.

We still have our abstraction, just like before, but the
encapsulation needs some work. Enter a factory. Not literally, but the
Factory design pattern (you may all now cringe and shudder in fear).
The Factory design pattern simply says "instead of instantiating
objects directly, create an object whose sole purpose is to create
those instances for you." Again, not very earth-shattering at first
blush, but digging deeper will reveal some great advantages. The more important question, however, is
how do we design it?

Remember where we called createObject to instantiate the UserService
directly into the application scope? Well instead, we'll create a
factory that has a getUserService method, and instatiate the factory
into the application scope, and then we'll immediately call that method
and assign the result to application.userservice. This second step
isn't necessary (or desirable, even), but it's important, because our
app currently depends on application.userservice being available, and
we don't want to have to change all the references to it right now,
though that's something that should be done.

Inside the factory, the getUserService method does about what you'd expect, except that it
caches the instance it creates in the factory's variables scope and uses
that cached instance for subsequent requests so that the instances are
true singletons.

Now back to our original problem: how does the DocumentService call
methods on the UserService? Well, both services, in addition to their
"normal" constructor arguments should be changed to accept a reference
to the factory as well. So now either service can easily obtain a
reference to any other service in the backend. The factory is allowing
the backend as a whole to remain encapsulated, and it's also allowing
each individual service to be abstracted from the others. Note that
this means the service objects are now exposing two APIs, one for the
UIs to use, and one for other services to use. The former is almost
certainly a subset of the latter, so it doesn't really appear to be two
different APIs, but it is.

Ok, quick recap to this point. We have a factory singleton
instantiated into the application scope of the app. It can be asked for
singleton instances of the application's service objects. Each of those
service objects also has a reference to the factory, so they can
request instance of other services for complex business operations. The
backend as a whole is fully abstracted from the UI and fully
encapsulated, and each individual service object is also fully
abstracted and mostly encapsulated from the other service objects. I
used the 'mostly' hedge because it's impossible to get fully
encapsulated within the backend (otherwise it'd be multiple separate
backends), but we want each piece to be as isolated as is reasonable to
reduce the maintenance headache.

Once we have this arrangement, the possibilities are nearly
limitless. We can wantonly reimplement any of our service objects
without affecting anything else. So when you're ready to give using
entity objects (business objects, entity beans, etc.) and their
corresponding DAOs a whirl, you can do it with a minimum of fuss. As
long as everything remains encapsulated behind the service objects, no
other pieces of the application (both the backend and the UI) need care.

It's probably worth a bit more discussion of encapsulation at this
point. Encapsulation isn't a wall around an object. It's a wall around
a system, where a system can be quite large or very small. All but the
simplest systems are made up of smaller subsystems, each with their own
wall. A system can't see outside it's wall, but it can see inside it's
subsystem's walls. Note that I'm talking only about encapsulation here,
not abstraction. A system can see it's subsystems' interfaces , but not
their implementations, but a system can't even see the interface of
it's parent system.

Two more concepts to mention (by name) before I go: coupling and cohesion.

Coupling is when objects are tightly bound together in some way.
This is pretty obviously a Bad Thing, as it means that encapsulation
and abstraction are both missing to some degree or another. In dollars
and cents, it means that any changes to one thing have a great chance
of the "ripple effect", which usually means a lot more wild goose
chases across your codebase fixing bugs, and at the very least,
requires a longer testing and QA cycle for every code change.

Cohesion is the idea that a given thing (be it a system, and object,
or a method) does a single, clearly definable thing. This has the
benefit of making your code easier to follow, and it also reduces the
possibility that a method will semantically change, and therefore
require an API adjustment, which reduces the ripple effect, and again
saves you money with reduced maintenance costs.  Finally, it
promotes code reuse, since small atomic blocks are easier to reuse then
larger blocks.

Two scary terms, but speaking from experience, you don't have to
think about them much. If you've done your homework and designed a system that is both
abstracted and encapsulated, you'll usually end up with loose
coupling and tight cohesion without having to think about it. I only
mention them because they're helpful checks along the way while
designing a system. When you get stuck, ask yourself which alternative
is more cohesive and more loosely coupled, and that's usually the right
way to go.

In conclusion (if it's a conclusion at all), the APIs you build into
your backend are of utmost importance.  The rest of the design is
pretty much meaningless without this piece.  If it's present,
however, your developers will be enormously more productive and able to
meet changing needs without massive costs to rework the application as
a whole.

Good OO design is difficult, almost always requiring an
iterative approach, but using the coupling and cohesion tests along the
way can help you avoid potential pitfalls.  But most of all, it
just takes a lot thoughtful practice and experimentation to learn what
works and what doesn't.

So I'm again signing off after more typing than I intended. I
realize now that I didn't offer up a whiskey break breather in the middle,
but hopefully you'll forgive me.

6 responses to “Designing an OO Backend”

  1. Scott Stroz

    Barney –

    Between you and Joe Rinehart, I may actaully 'get' OOP after a while.

    Great job!

  2. Raymond Camden

    Not to put more work on your plate, but you should offer a simple example of this as well, in real code. I think it would be useful. Of course, I don't mean a full implementation, ie, addUser should be empty, but just the shell.

  3. Joe Rinehart

    Hey man,

    This rocks. Ever thought about submitting it to Simon Horwith for a CFDJ article?

    -Joe

  4. Steve Bryant

    Barney,

    Great post! I agree with Joe that it would make a great CFDJ article.

    You mentioned that don't recommend using application-scoped service objects for large applications. That is exactly what I am doing right now, so I would love to know what the pitfalls of that approach are and any suggestions you might have.

    I would love to see more information on how you use service factories.

    One thing I am having a hard time getting my head around is how you call objects from the service factory without breaking encapsulation. Right now, I pass in any component that I need to a component that uses it. This makes the dependency clear. It seems like those dependencies would become much less clear when calling service objects from the factory.

    Sorry if I overloaded you with too many questions.

    Thanks for the great post!

  5. Barney

    Steve,

    The main pitfall with having all the services directly in the application scope is that you end up having to pass a lot of object references around your application. Yes, it's more clear in the sense that the object coupling is explicitly laid, but it's also really cumbersome to deal with.

    In the main app I've got, most of my services only require references to one or two other services (there are perhaps 40 services total), but there are a few that require 10-15 other service references, and one that requires more than 30. I could pass those around individually, but passing a single factory object and allowing the object to get what it needs, when it needs it, makes for much easier to maintain code.

    The other advantage of a service factory (or any sort of factory, really), is that it lets you encapsulate the initialization of the objects. Right now, you initialize into the application scope, and pass references (or use the directly), because you can't initialize them any other way. With a factory, you can "initialize" a new instance at any time. The factory isn't actually going to create a new object (at least if it's a singleton), but there's no way for the calling code to know that.

    Bottom line, I'd say a factory is better encapsulated, even if the coupling between objects isn't as clearly documented but the constructor parameters.

  6. Tuggle

    I also have been raised on storing services in the application scope. I see what you mean about cumbersomeness, I guess it's a trade off between readability and maintainability (a real word?). It's pretty rare that those two things compete!

    This is a great article (even um, a year later) thanks Barney. :)