Attention: We are retiring the ASP.NET Community Blogs. Learn more >

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!

17 Comments

  • So you're basically creating a separate class for each query? Please expand on this.

  • 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!

  • "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.

  • 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...


  • 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.





  • "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?





  • 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.

  • "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.

  • OK, yes.





    That would easily be solved with a wrapper around the DataSet.

  • We managed to reach consensus :)

  • 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"





  • 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.

  • 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.

  • 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.

  • 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.





  • 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.

  • Yes, of course.


Comments have been disabled for this content.