Or as Robert C. Martin says: “Don’t marry the framework”
Once upon a time, I used to learn frameworks and apply what I learned directly on projects without much interpretation.
While it was saving the day, it wasn’t doing any good for maintainability, testability, readability, DRY principles and whatsoever in the long run. I was wondering why it was so difficult to code a web app with reusable business logic components.
The typical tutorial code for doing a database operation on ASP.NET MVC looked like this:
I started coding like what the tutorials thought me. I guess this is what most newcomer programmers do for a long time. It’s OK to use this approach for some simple applications and learning new frameworks but a time will come when you will feel challenged facing the same problems again and again. The code becomes cluttered, business logic is duplicated around a lot of files, completely coupled with the framework (
ViewBagHttp Query String etc.).
Your core business logic should not be about HTTP requests, query strings and other web gimmicks. It should be all about the business requirements. The core business module should not be concerned about how data is input or how results are output. It can come from a web form, a query string, a windows form, a web service, a console application, or a VR interface if you are feeling adventurous. The solution for all of these problems is separating the business logic from the surrounding framework.
For a moment, just think about nature as an analogy. Fruits grow on trees with their peels and shells. They need their peels and shells to grow healthily and protect them from outer impacts and dangers. They do not expose their core to the outer world. Like a fruit, your application needs a shell to grow healthily.
When you directly embed business logic inside a controller, you are exposing the core of the fruit. But when you separate it into another layer, you are using the framework as the peel. You can always change the peel into a shell, or a plastic cover but the core will remain safe inside. You are future-proofing the core logic. Still, it’s not an easy undertaking.
You need to start thinking outside of the framework you are using (ASP.NET MVC in our case) and separate your business logic from your framework code. This makes your business logic decoupled from the framework you are using. You can design a service class that encapsulates the business logic that was in the controller. Of course, directly accessing a service class is not preferred. Instead, it is typically abstracted behind an interface to make it unit testable and interchangeable. Data is exchanged via data transfer objects or DTOs. DTOs are plain objects that don’t contain any business logic, only data that can be serialized.
Let’s try to separate the business logic from the earlier example:
The MVC controller code becomes this:
You can even call the same business logic from a command line interface:
It might seem pointless to use interfaces in these examples as we are constructing them manually and you are right. Because I omitted dependency injection for brevity.
You should absolutely use a dependency injection library to inject the implementations of the interfaces into the constructors instead of constructing them manually like the
new BookService(). It is critical for maintainability and testability and should be used in real-world applications. For example, this is how
BookController will look like with dependencies injected instead of manually created in place:
By depending on interfaces and DTOs instead of concrete classes and business logic objects, we are future-proofing our code. This way if we decide to break some of our services into another process like a web service or any other RPC method in the future, we can do it with ease:
Food For Thought
- We only abstracted away one layer of logic. This layer is still accessing the database directly through the entity framework, so it’s still tightly coupled to a framework. What will happen if you need to store data in a file, or in a database that the entity framework does not support? How can you overcome this problem?
- What will happen when a BusinessException is thrown? Do you need to handle it, and where should you handle it?
- Is leaking web constructs like HttpRequest or ViewBag directly into your service layer a good idea? What will happen if you want to use this business service in a Console app instead of a web app when you leak the web constructs?
This was a very general approach to a very broad topic. I highly recommend reading books like Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin to dive deeper into the subject. As Robert C. Martin says in the book: “Don’t marry the framework!”