So you're writing a three- (or more) tier application. This is probably the
most common application architecture, so you're in good (or at least well-populated) company. What I'm going to advocate now is the "shared API" pattern.
Let's get the most obvious objection out of the way first. "But," you say, "surely in a service-orientated architecture we want loose coupling? Doesn't a shared API break that principle?"
The answer is yes and no. Yes, a shared API does create tighter coupling at compile time
, but provided the service contract doesn't change (and it shouldn't, after a public release), you are still going to be loosely-coupled at run-time. That is to say, you don't have to update the client software every time you rebuild the service layer, and vice versa.
This is really the best of both worlds. You want
tight coupling at compile time, to prevent your domain model from being (a) duplicated and hence (b) susceptible to client/server drift. Conversely, at run-time you really want the coupling to be loose, to prevent your users from getting frustrated with having to upgrade their software all the time.
There are other objections to shared APIs, but I believe that they stem principally from bad design and implementation. The basic idea of a shared API is that you have to define your service contract up-front
; so arguing against it on the grounds that it makes life difficult for programmers when they want to just make a little change to the domain model or service contract is, in fact, rather perverse. After all, isn't defining your domain model first one of the most rudimentary design principles?
So now the question becomes, what does a shared-API architecture look like? I'll use WCF to illustrate. There are really two parts to a shared API: the service/data contract and the model.
Let's start with the model. You can do it the other way around, but that sometimes leads to problems later on. Step one is to create a set of interfaces (or abstract classes if you like) that represent your system:
The purpose of this exercise is to get the model right. At this point we are not considering efficiency or implementation constraints. The only concern is with the informational content of and structural relationships between the entities.
Entity relationships can be subtle things. Everybody's heard of "one-to-one", "one-to-many" and "many-to-many", but there's much more to the story. One important question to answer is that of ownership. An object may link to a set of sub-objects, but it doesn't necessarily "own" them. If a sub-object might be shared between two parents, it's almost certainly not an "owning" relationship. The other question is that of visibility. Is the child aware of its parent? (Why?) Does the parent know about its children? Answering these questions is often more of an art than a science, and volumes could be written about the different approaches that are taken.
At any rate, there are generally three types of relationship: parent contains an array of children, parent points to child(ren), and child points to parent. Each implies different ownership and visibility semantics.
Having satisfied ourselves that the domain model is correct, we now turn our attention to the use cases. Each step in a use case could potentially involve the invocation of a service or services. The question that must be answered is: "what data must be passed in which direction at this point?" This is often easy, as in the case of data that is specified to appear on a particular screen, but it can be very much more complicated - as in, what are the services required to update a form or document?
Transactions must also be considered at this point, not only in terms of the guarantees your system must make, but also in terms of the architecture you intend to use. For example, if you use MSDTC to manage your transactions, then outbound transaction flow must be enabled on all the client machines. This could be an administrative and/or security problem, in which case you might be better off changing your service model to explicitly include Begin/Commit/Rollback operations.
Once you've identified the services you require, and consolidated them as appropriate ("as simple as possible, but not simpler"), you're ready to write your service contract. There is generally an intersection between your domain model and your service contract. Try not to focus too much on this! Remember that the service model is a completely different thing from the domain model. Services operate in a specific context, whereas domain entities are abstract and generalized.
Here's an example of a service contract for the domain I described above:
Next time I'll talk about the implementation aspects of shared APIs as they relate to their client and server consumers, and also about the limitations of object orientated languages in this respect. Those familiar with WCF will probably have noticed some problems with the code above, and can therefore anticipate what is coming...
Labels: architecture, best practices, coupling, SOA, WCF