Sunday, February 12, 2012

Web applications and the business logic

The MVC pattern is the foundation of every web project. It is not necessary to talk over its principles and advantages as they have repeatedly been discussed.
While several implementations have been provided during the last decades, how to structure and organize the model is still open to debate and will be at the core of this article.

Randy Stafford distinguishes between Domain logic and Application logic in the book Patterns of Enterprise Application Architecture.
Domain logic is described by all the business rules that govern the domain in use. In the case of a football league, the domain logic involves teams, players, matches and all the rules that inter-connect them.
Application logic contains all the rest. It is the logic that would not exist outside of the application. It is not specific to the domain but it is required by the application in order to fulfill its requirements. Some examples are exporting data to CSV, sending emails, and persisting data to the database.
The last remark is probably the most difficult to digest, but it is also the most important. The database is not part of the domain logic.
Going back to the football league example, the domain logic does not involve databases, tables and columns at all. The database exist only because the application needs it (maybe).
The database is just a way to store data, it is a detail. In the same way the HTTP is a detail. The HTTP is just the protocol the application uses to communicate with the external world. Same goes for CSV files, FTP servers, RMI, REST and so on.
They are all details. They are always for the application to interact with the outside environment. If you migrate from MySQL to MongoDB or Riak your domain does not change at all. What changes is something specific to the application, which is the way for the data to persist.

Business logic in the J2EE world

In the Java/J2EE world, the domain model is usually made of beans. These beans are mapped to database tables using Hibernate or equivalent. A bean is an object with an empty constructor, a set of private fields and a getter/setter pair of methods for each field. However, a bean does not contain any logic, it is a mere data structure.
On top of the beans there is the service layer. The service layer consist of a set of POJO's carrying all business logic. Services contains both domain and application logic. Hopefully, those handling domain logic are separated from the ones taking care of the application logic.
Usually, on top of these service POJO's there are service facades. A service facade set up a context for inter-services collaboration. It also takes care of cross-cutting concerns like transaction management, security and so on.
The whole point of Object Oriented Programming is to encapsulate and bundle together data with behavior.
This means that a standard J2EE setup violates this principle since it separates data (beans) from behavior (services).

Business logic in the Rails world

The Rails approach is simply the opposite in that models contain both data and logic, which is great.
This approach is well known in the Rails community as skinny controllers and fat models.
The problem is that models do not contain just domain logic. All application logic is to be found in them as well. This includes database mapping, callbacks and even controller specific concerns, like the to_param method.
This approach can lead to serious problems. As the business logic grows and grows, models end up being obese!
The Ruby community has faced the problem and has started looking for solutions. The most popular at the moment seems to be DCI, which stands for Data, Context and Interaction.
DCI can seem to be very elegant at first but it has his drawbacks. Many mention implications on performance. Others the fact that DCI can require a lot of boilerplate especially when the number of use cases is considerable.
In my opinion the problem with DCI is exactly the same of J2EE. Data and behavior are separated.
The model acts as data structure at the same way as java beans do. Roles hold logic but no data, same as Java services. The Context bundles together data and roles. Eventually, it adds cross cutting concerns like transaction management and security as well. Does it smell like J2EE service facades?

Separating the domain from the application logic

Both J2EE and Rails approach can work quite well, even though they both have pitfalls. But as soon as the number of use cases increases, they both don't scale very well and code will start to smell.
In those cases the simplest way to go is to just separate the domain model from the application model.
The domain model has to contain nothing else but domain logic. It does not have to persist anything to the database, it does not even have to know that a database exists!
The domain objects will bundle data and behavior together. This provides better information hiding, encapsulation and modularity: the three most important concepts in software engineering.
Services can still exist in the domain model. They will contain operations involving more than one domain object and that do not really belong to any of them.
The domain will be packaged and distributed. It can be a JAR file, a Ruby gem, it does not matter!
The same domain module can obviously be imported by several distinct applications, increasing re-usability and maintainability enormously.
The application model imports and uses the domain model. It also takes care of the interaction with the database, CSV exports, requests to external services and so on.
It does not matter whether you are working with a Java application, a Ruby application, an enterprise application or even a desktop one. What matters is that you solve a certain problem for a specific domain. Your focus has to be the domain you are working with. Forget about databases, HTTP, REST and company. Those are just details. You can change them whenever you want, but the domain will remain the same.
I got inspiration for this article from the talk of "Uncle" Bob Martin during the last Ruby Midwest Conference.
Of course this approach requires much more initial work and it could be unworthy for simple systems. However, as soon as the system grows, the time and the pain saved will be considerable.

4 comments:

  1. I like the spirit of this article.

    First, let me say I agree with you in some instances and disagree in others. I agree that skinny controllers, fat models is making our models obese. I agree that databases should not be a large factor when developing our applications and that domain logic should be broken into Roles, in the DCI case and domain classes in any other.

    The problem with implementation agnostic domain logic is optimization. The complexity of the application creeps to a point where you're in need of optimizations and you can't get them because you're so agnostic towards your database, HTTP service, etc. Models who are too dumb end up suffering from performance issues when denied multi-level selects, deep joins and database-level user-defined functions. If you're trying to squeeze every last drop out of your application performance, these details matter. If it's something that can be done in the model, awesome, but many times it's your domain logic that needs to know about specific data, extracted in a means that suites its needs.

    My belief is: Cross the optimization bridge when you get there. There's a troll who lingers below. Abstract out domain logic just like you address here and care nothing of how your data is stored. DCI is a fantastic means of abstracting domain logic from data store implementations. The performance implications are improving by approx. 350% from Ruby 1.9.2 to 1.9.3.

    ReplyDelete
    Replies
    1. Hi Mike,

      The problem I see with DCI is the separation between Data and Roles (which is the behavior).
      In particular, the Data is not an object but a data structure. The Data exposes its structure instead of hiding it. A change to the Data will require changes to the Roles that the Data itself acquires through the Context. Even tough the Role does not seem to be coupled to the Data, it actually is through the Context. The Context is the glue between Data and Role. The code in the Context results to not have any meaning since, it does not speak by itself. The Context prepares the field for the Roles to speak.
      As the number of use cases grows, the number of Contexts grows as well. This means more boilerplate and additional maintenance required.

      On the other hand, separating domain from application logic results in loosely coupled objects. They will hide their structure to the outside world and encapsulate the relative behavior. This results in code that is more re-usable and more expressive. Domain objects will naturally communicate with each other without the need for the Context to act as a glue between them.

      Regarding performance, you are completely right. However, I do not want the database to determine they way my domain behaves. I want it to the be the other way around. The domain has to lead, while the database and the rest have to follow. Premature optimization is the root of all evils :)
      However, as you say, performance can (hopefully) become an issue. When that time comes, I will have to change the way the data is stored but not the domain. This could mean switching from SQL to MongoDB or Redis for operations that do not need to be ACID. By separating domain from application logic this can be achieved in a quite simple and elegant way.

      Delete
  2. I'd love to see some more elaboration and code examples comparing the "DCI way" to your way. DCI has always seemed a little odd to me, and compared to the simplicity of the command pattern, I'm not sure it's worth its drawbacks.

    ReplyDelete
    Replies
    1. To be honest, I do not believe there exists a single way to design all web applications. This is because all applications are different, have different requirements and the design has to best fit those requirements. The problem I see with DCI is that DCI is an outside-in approach. It starts from an assumption, the DCI pattern, and the rest has to adapt to it. However, I see the same problem with the Command pattern. What I prefer is instead an inside-out approach. I start from the design of the application and only then I decide which patterns to apply in order to achieve the best design possible. These patterns can be DCI, the Command pattern or any other pattern that can make the application better.

      Delete