Most developers are familiar with the concept of N-Tier Architecture. It is a software pattern that separates components of an application into separate logical layers to establish code boundaries, promote flexibility, and allow reuse. Most commonly this is accomplished using 3 layers:
- User Interface Layer (UI): Where all presentation and user interaction takes place. Displays and receives data to and from the user.
- Business Logic Layer (BLL): Application processing. Coordinates data between the UI and DAL.
- Data Access Layer (DAL): Where data management occurs. Typically using a database or web service.
The UI and DAL are pretty easy to understand, but there is no clear definition for what a BLL should actually look like. As a result, many developers often omit an explicit business logic layer from their applications. That is why we see countless examples of UI code communicating directly with the DAL. How many times have you seen an ASP.NET MVC controller talk directly to Entity Framework? How many times have you seen an iOS view controller talk directly with CoreData or a web service? Even in Microsoft and Apple’s own documentation and samples! A “fat controller” is usually a code smell that your UI layer is doing too much.
Some people recommend designing your BLL by simply thinking of your application without a user interface. This is a cognitively difficult exercise. Instead, design your application to support multiple different user interfaces. For .NET, this could mean considering a WPF as well as an MVC front-end to your application. For iOS, it could mean considering both an iOS and OSX app (if possible). Sound easy? Go ahead, try it! Seriously. The need for common code will become immediately obvious as you find yourself literally copying identical code for each front-end. The code that you are able to share between them becomes your BLL! In general, this code will likely fall into one of the following categories:
- Processing commands (from the UI layer)
- Coordinating workflow
- Maintaining application state
- Accessing data (from the DAL)
- Making logical decisions
- Performing calculations
- Persisting data (to the DAL)
- Displaying data (to the UI layer)
The goal is to centralize as much common logic as possible in the BLL so that it is not coupled to any particular UI. In doing so, you are separating out the “core” of your application which, with good design, should support any number of front-end UI’s with a minimal amount of code duplication. As the code in your BLL grows, the code in your UI layer will necessarily get smaller. That is the idea behind the “thin controller” (or “dumb UI”) philosophy. But won’t the BLL become huge if we essentially move most of our application’s code into it? Yes, and that’s the point! If and when the BLL becomes too large or unwieldy, we can simply separate this layer further by moving from a 3-tier architecture to an n-tier one. This exercise also helps identify leaky abstractions, which are places where a logical boundary has been violated. For example, maybe you are relying on the built-in data validation in ASP.NET MVC. After trying to build a WPF front-end you will that this does not exist, and you will have to explicitly invoke object validators.
The same thinking applies for the BLL’s relationship to the DAL! Not sure where to draw the line between the two? Try the same exercise: design your application to support multiple different data stores. Seriously, try to do it. You will find that you probably have built-in assumptions throughout your application that your data comes from a database or a web service (Quick test: do you have the words “SQL” or “URL” in anywhere but the DAL?). The logic of your app (the BLL) should not care where it gets its data from. It should only care about the data itself. Therefore the relationship between the BLL and DAL needs to be designed to be just as datastore-agnostic as it is UI-agnostic.
Like many SOLID development practices, the beauty of a well-designed BLL is best appreciated not at first, but when an application has to be changed or updated. By separating the logic of an application from its user interface and data store, we isolate the code that needs to be fixed, and also make it easier to test. When less code changes, there is a lesser chance that something can break. That means a more stable, and ultimately better, product.