Andres Aguiar's Weblog

Just My Code

Layers, messages and domain models

In some previous posts I was trying to explain why I think is better to use a message-based approach for developing n-layer applications instead of using a domain model. In some posts I've focused on the DataSet, but its not really about the DataSet but about the way of exchanging data between layers (notice Im saying layers, not tiers). This time I'll try to explain myself better ;).

When I'm saying message what I mean is to gather the data you need for a specific task in a specific data structure. So, if I'm dealing with an Order and the order has master and detail, I want a data structure with 2 levels, even if an Order involves more than 2 classes or 2 tables. If you think a message is something different than this, please pretend you agree with me, it is not really important ;) in this context.

Lets think about an ASP.NET application that needs to display an Order that is stored in a relational database, with the following tables:

Order [Id (key), CustomerId]
OrderDetails [Id (key), OrderId, ProductId,  Quantity]
Customer [Id (key), Name]
Product [Id (key), Price]

If I map this to an object model, and I want to use the resulting classes in my ASP.NET application, I'll probably write something like this (in pseudo c#):

Order.Id
Order.Customer.Id

foreach (OrderDetail detail in Order.Details)
{
       detail.Product.Id
       detail.Product.Price
       detail.Quantity
}

Now, suppose I need to change my underlying schema. Instead of having one price for each product, I'll create Customers Categories, and I'll have a Product price for each Category. The tables will look like:

Order [Id (key), CustomerId]
OrderDetails [Id(key), OrderId, ProductId,  Quantity]
Customer [Id(key), Name, CategoryId]
Category [Id(key), Name]
Product [Id (key)]
ProductCategory [ProductId(key), CategoryId(key), Price]

The usual way to hide this change in a multilayer application is to change the way I map my tables to my classes, so the signature of the classes will be the same and I won't have to change my ASP.NET code. In this case, the problem is that I cannot (or perhaps I can but I dont realize how ;). I'll need to change my code to something like:

Order.Id
Order.Customer.Id

foreach (OrderDetail detail in Order.Details)
{
       detail.Product.Id
       detail.GetProductPrice(Order.Customer.Category.Id)
       detail.Quantity
}

This is because there is no way to obtain the price from the Product object, as it needs the Category, and the Category depends on the Invoices Customer.

If instead of using a domain model, I define a message with:

OrderId
CustomerId
OrderDetail[] lines

OrderDetail:
   ProductId
   ProductPrice
   Quantity

Ill write:

OrderId
CustomerId

foreach (OrderDetail detail in Order.Details)
{
       detail.ProductId
       detail.ProductPrice
       detail.Quantity
}

And this code will be valid with the first database schema and with the second one.  I just need to change the SQL statement that loads the structure.

Why does this happen? Because if I use my domain model from a different layer, Im exposing it. We build applications in layers because we want to isolate one layer from the changes in the other layers, and exposing a domain model does not let me do it. Its usually known as a bad practice to expose the database schema to the UI layer of an application. Exposing the object model is better, but it's still bad idea.

On the other hand, think on how complex is for the ASP.NET developer to deal with each model. If he uses the object model, he needs to know how to traverse it to find the data he needs. Also, he does not really know how many roundtrips it takes to access the Order, and how many columns will be retrieved (i.e., all the fields from the accessed tables will be probably retrieved even if not needed).

With a data structure designed to work with the Orders, we are giving the external layer programmer a much better interface with our middle layer. 

UPDATE: this post has been translated to French!

Comments

Paul Gielens said:

Exposing the object model is better, but its still bad idea: This is why we encourage to interface functionality via Service Layer architectures instead of feeding users with a fairly raw domain model. By reading your post I taste a lack of experience with these kind of systems (am I correct?). Complex systems surely benefit from rich Domain models where Table Module patterns seem to lack overtime. It's all about making the tradeoffs:
- Are we gone do severe maintenance?
- Are we supporting ADO.NET containers over different versions of the .NET Framework or isolate this problem by introducing custom entities and only maintaining the mapping layer?
- What's the payoff for databinding (do we need it at all?)
- Is my team capable of constructing a rich application upon the newly build domain model and service layers?
- Is any of my code or gained knowledge reusable? Does this affect our decision?
- Etc

My personal experience is domain model over ADO.NET any day. Traversing associations over looping through datastructures. Encapsulating business logic within my objects and their services. And the far most important, technology independance... with a good oo-design I can switch to any object oriented language I care for, so my customers solution isn't tied to specific technology. I can participate once my customer decides to try Java over .NET instead.

Further reading here: http://weblogs.asp.net/pgielens/posts/8843.aspx
# June 22, 2003 1:27 AM

Bo said:

So you're basically creating a separate class for each query? Please expand on this.
# June 22, 2003 1:32 AM

Paul Gielens said:

Bo: Model your problem, resulting in a domain model, and fix your customers problems. Then start worrying about mapping the model to a recourse manager. We need to stop thinking "database centric" and focus on REAL problem. You'll get highly rewarded for doing so!
# June 22, 2003 1:38 AM

Frans Bouma said:

My order class (domain model) has methods like GetSingleCustomer() (because it has a m:1 relation with customer), a property OrderDetails which calls GetMultiOrderDetails() (1:n) (and lazy loads it) which returns a collection with orderdetail entity classes. These classes have a relation with products. (m:1). Order also has a m:n relation with Products via orderdetails. I therefor also generate a method GetMultiProducts (and a property Products) which returns the products entities in a collection (typed).

Now, this is common sense OO programming. You CANT have it in another way then: there are entities in my system, they have relations over attributes, deal with it. Even in your application you are exposing relationships between entities over attributes: every column in your resultset has a relation with all the other columns in the resultset.

I don't see the problem with 'exposing' some kind of knowledge that has to be kept secret. Why? You NEED the information to maintain a consistent database anyway. You CANT add new rows to the database by using your dataset which has perhaps only subsets of several tables columns, you need datasets which have all rows of these tables in them to add new rows.

What you forget to mention is the real problem with datasets as the base block of building O/R mappers: the schema is in the dataset/datatable and not in the typed row. I can't give the datarow (typed even) to another class or ask another class to create a new datarow for me, it has to do that via a dataset, because you can't create an instance of a datarow (which is an entity in some respect) without a dataset with a schema.

Also, you don't talk about entities in your examples. Which will result in problems within teams which have database specialists and programmers who write the OO code: they can't talk about the same thing if you hide information for the other one.
# June 22, 2003 3:51 AM

Frans Bouma said:

"Exposing the object model is better, but its still bad idea: This is why we encourage to interface functionality via Service Layer architectures instead of feeding users with a fairly raw domain model"
Paul, you mean the developers of gui's should work with f.e. 'OrderManager' which controls the order maintainance tasks by using order classes ? Agreed, it that's true.

I wonder why Adres is so focussed on explaining some kind of difference that is supposed to be there, where the real difference is quite small: in an application you connect the gui to logic which can feed the gui with the right info plus can consume the info from the gui in a 1:1 manner, f.e. by offering the gui 1 method which does several DML actions at once (f.e. through an 'OrderManager' class). Now, if you do that, there is no real difference: the ordermanager works as a black box for the gui and in Andres' case works with dataset based objects, and in other cases (like mine) works with domain objects.

So it depends on what kind of code do you want to write in that 'ordermanager' class. For 'exposing' secrets which should have been hidden inside another layer, it's not important, since the ordermanager is typical code which can be found in the middle tier/layer.

In other words: it doesn't matter much what you'll use, when focussing on exposing secrets to other layers.
# June 22, 2003 3:59 AM

Andres Aguiar said:

OK, I think we finally agree.

If you always add a service layer between your domain model and the UI then that's it. That's what I'm suggesting: to not expose your db schema or your object model to the UI layer, add a layer.

We can argue if it's better to have a database-based design or a full OO design, but it is really a quite religious subject, so I don't want to discuss it...
# June 22, 2003 4:09 AM

Andres Aguiar said:

I did not mention DataSets or .NET in this example. I can use the same approach in any architecture/platform.

Going back to Fowler, I'm saying to add a service layer that returns Data Transfer Object.

Also, on the issue of being architecture independent. From Fowler: 'Don't try to separate the layers into discrete processes unles you absolutely have to. Doing that will both degrade performance and add complexity, as you have to add thinkgs like Remote Facades and Data Transfer Objects'.

He means that if I use the raw domain model, I'm really choosing an 'inproc' architecture. I don't want to do that. If I have my service layer and my Data Transfer Object, I can use it inproc and out of proc, so I can make that decision whenever I want.

# June 22, 2003 4:31 AM

Andres Aguiar said:

"What you forget to mention is the real problem with datasets as the base block of building O/R mappers: the schema is in the dataset/datatable and not in the typed row. I can't give the datarow (typed even) to another class or ask another class to create a new datarow for me, it has to do that via a dataset, because you can't create an instance of a datarow (which is an entity in some respect) without a dataset with a schema."

I have a typed DataRow class, and I have to ask the typed DataSet for an instance of it. I don't see what's the problem with that, can you help me?

# June 22, 2003 4:45 AM

Frans Bouma said:

Ok :)

About the data transfer object (DTO) aspects: if you set it up with DTO's being passed to persistence oriented logic (f.e. adapters or DAO's) it's really a matter of taste: encapsulate the logic inside your domain class (domain class passes along the DTO to a DAO) or place it outside the domain class (you pass the DTO to the DAO / adapter). The first (encapsulation) has f.e. 'save' and 'fetch' methods exposed. The latter doesn't. I think it's a matter of how much your object travels through your application. Yesterday a nice discussion about this topic was held on the DOTNET-CLR mailing list at developer.discuss.com. When your object travels a lot (f.e. among layers or among webservices) it's perhaps wise to choose the external persisting logic method, however the opposite is also logical in some way.

The service layer is key IMHO, since it makes GUI development very easy plus makes implementing different gui's also easy and creates code re-use. I think the main focus on explaining good software design to people should be based on that: create abstraction between those groups of functionality when these groups of functionality can be seen as an atomic unit from the POV of the functionality itself, like 'gui', 'data centric logic' 'business logic' but perhaps also 'customer oriented business logic' to create separations inside a larger, more abstract separation of 'business logic'.

And we're back again with the top-down approach of Yourdon cs. :) People should read more of Yourdon's work. He's the real hero people should read, not mr. Fowler.
# June 22, 2003 4:47 AM

Frans Bouma said:

"I have a typed DataRow class, and I have to ask the typed DataSet for an instance of it. I don't see what's the problem with that, can you help me? "

Well, if I want a given instance of a customer, say customer "ABC", I can do in a domain model:

CustomerEntity customer = new CustomerEntity("ABC");

and I have my customer. This is the way you instantiate ordinairy classes every day. I can't however do that in a dataset oriented world. I have to ask a dataset to create an instance. Which in a way is weird to me, since I have to instantiate the instance in a different way than my other objects in my program. THis can be weird for a developer if he doesn't know the underlying structure of the O/R mapper he's using.
# June 22, 2003 4:50 AM

Andres Aguiar said:

OK, yes.

That would easily be solved with a wrapper around the DataSet.
# June 22, 2003 4:53 AM

Paul Gielens said:

We managed to reach consensus :)
# June 22, 2003 4:53 AM

Andres Aguiar said:

Just to make sure we agree in what we had just agreed ;):

"You should never use your object model from the UI layer, you need to add a service layer"

# June 22, 2003 10:38 AM

Jesse Ezell said:

The problem is that your domain model was flawed to begin with :-). A line item should NEVER get its price directly from a product. The reason for this is pretty simple, prices change rapidly, so if you do this, you will seriously jack up your invoices every time you change a product price. As a result, the LineItem class (or order details here), should have a member called something like UnitCost (actually, our line items contain a lot more, because of discounts, promotions, etc.... but for the sake of simplicity, UnitCost will work here). This is set when the line item is created, so you merely have to update the business logic to pull the category price instead of the product price. Yes, you have to change your business logic layer, but that makes perfect sense because this is a change in the business rules (either way, you would have to make some modifications, the goal here is not eliminating them, but minimizing them). In any case, the portions of your code in question here would continue to work if you had properly designed your domain model in the first place.
# June 22, 2003 2:25 PM

Jesse Ezell said:

PS: I should mention that your service layer suffers from a flaw here that will break it if your domain model gets more complex and can't be fixed by simply changing up your sql code. Your order details should have a total member as well, because the total might not be unitCost*quantity if you should decide to support discounts in the future or some type of cost modifiers. So, again, in the end it comes down to properly designing your system. If you design it wrong in the first place, it doesn't matter whether you are using a service layer or you have a single spagetti code layer.
# June 22, 2003 2:36 PM

Andres Aguiar said:

Jesse, you focusing in the sample and not in the core issue.

I agree that you don't want to use the 'real' product price to calculate the invoice total, but is just an example.

If instead of an invoice it's a 'price request' from the customer, I could have the same structure with the same problem. I can think of more examples if you need them ;)

I don't understand why my service layer will break if the total needs to be calculated in a different way. I could add an 'InvoiceTotal' field to my Data Access Object and fill it in the proper way.
# June 22, 2003 3:15 PM

Jesse Ezell said:

The point is that you are in one of two positions:

a) You knew you needed that field in the first place. In which case, you don't need to build the extra layer, because you can put the field in the first layer.

b) You didn't know you needed that field. Your solution will break, regardless of whether you are using your business objects, or this 2nd data layer on top of your business objects. Adding the InvoiceTotal field does not work after the fact, it has to be there in the first place, or people won't build their solutions using it.

Again, it all comes down to proper design. Using a second data layer as an excuse for not designing your first layer properly is a ludicrous idea. You must anticipate future changes in order to build a solid solution. Adding an extra layer on top of your objects might make you feel like you are safer, but it is a false sense of security.

That said, there are places where you need this second layer, mainly when dealing with SOAs--which, despite what you seem to be implying, are no a silver bullet or the best solution for every problem. The reason for this additional layer with your SOAs though is mainly due to the fact that:

a) business entities passed through web services are "dumb" entities. They contain data, but no logic. So, if your business entities are more than just data, the automatically generated entities supplied to the webservice consumer via the WSDL/SOAP are not going to match your business objects. This, of course, could create a potential integration nightmare.

b) some times, your business entities contain a lot more data than you want visible to the outside world. A lot of fields might only be used for internal processes, and would be inefficient to pass around (not to mention, confuse the hell out of those consuming your web service).

c) the people consuming your data need a consistant view of the data. Changes to the schema should not break the service. If the potential exists that your object model will undergo substantial change during the lifetime of the service, you should consider making some standard data structures.

Still, you should not use option c as an excuse to throw together a crappy BLL, or to suggest that all BLL's are crappy and, therefore, all projects require a special abstraction layer.

In any case, I think what you are getting at has nothing to do with designing layers, as the body of the post suggests, but more to do with whether or not you should use SOAs, and the obvious things you need to consider if you are using SOAs (I really don't think these things apply much for most non SOA applications).
# June 22, 2003 4:50 PM

Andres Aguiar said:

I don't get why I can't add the field if I found it's needed after the fact.

In this case I'm not saying you don't need to design your first layer first. You can build it. But the external layers should not use it.

# June 22, 2003 8:51 PM

Jesse Ezell said:

The problem is that implementations built on the first version will break, because they will all have a quantity*cost calculation hard coded instead of referencing the total field. There are some things you just can't fix if they weren't done properly to begin with.
# June 23, 2003 6:11 PM

Andres Aguiar said:

Yes, of course.
# June 24, 2003 9:32 PM

TrackBack said:

# July 2, 2004 3:30 PM