Frameworks and design patterns
Software development is a cornucopia of frameworks: J2EE, Spring Boot, Django, React, Tailwind, the list is endless. They exist because there is a lot of common behaviour between applications that can be captured in a framework.
Software engineers have design patterns: common approaches to writing software which flexibly solve various problems. Sometimes design patterns are implemented on the same problem enough times that they evolve into a framework. Whereas patterns are an idea, frameworks are a concrete implementation applying a pattern to a specific type of problem.
Frameworks can be great because they allow you to get a lot of work done very quickly. There is a great, slightly famous demo from David Heinemeier Hansson (DHH) in which he shows the power of Ruby on Rails; a real-time demonstration creating a blog in 15 minutes.
The price of a framework
These benefits do not come for free and can often be a Faustian bargain. An initial short-term boost in productivity can grow into a burden that hinders future development.
Frameworks are often opinionated. This does not mean they have something sassy to say when the application starts. Rather, the framework’s solution to this problem makes assumptions about how the application will work. They can become difficult to use when your application needs a feature that does not align with these assumptions.
At best, breaking a framework’s assumptions may mean forgoing some of the benefits that the framework provides. I once used Bootstrap—a CSS framework that preceded Flexbox—on an old project to help to format the front end of my application. I filled most
<div/> tags with variations of
col-md-6, and the like. Bootstrap splits the document into 12 columns and lets you choose how many a div takes for a given screen size. Unfortunately, unless you want to split your page into clean factors of 12 you are constantly fighting Bootstrap to get the result you need. I had to use alternate methods or CSS hacks to get the result I needed, often forgoing much of the benefit of Bootstrap. Worse still, on other projects using other CSS frameworks which style the entire website, I have found myself making very liberal use of
!important in order to get my desired outcome.
Frameworks often insert themselves into your code when they require you to inherit from and extend classes from the framework. This often means that some data types, methods, etc. are opaque. This is how these frameworks are supposed to work. The opaque boundaries are supposed to create abstractions that make the frameworks useful.
What happens when the inherited behaviour of these objects needs to be changed? You are now stuck having to intercept the out-of-the-box features of the framework; modify the framework’s code, breaking the abstraction that it is supposed to provide; or otherwise implement your own functionality on top of the framework.
Testing code that makes heavy use of a framework can be difficult. Tests should be quick and easy to run. This will mean that they are run more often, providing the most benefit during development.
A framework encumbers your tests, making them slower and more difficult to run. A framework will provide an API, usually in the form of functions, objects, or data that your code will use. These represent added complexity that needs to be managed as part of testing. This could mean additional modules that need to be imported during tests, running an additional server, database, etc. All of this makes your tests slower and more cumbersome. They will be run less often and provide less value to the application.
You are always vulnerable to any framework to which you bind your application. If the framework changes, your code which depends on that framework will break and have to change accordingly. This might be a new API, different behaviour of framework objects, or any other change to the framework.
The tighter that your code is coupled to a framework the greater your exposure to changes in that framework. If you inherit classes from the framework and use them throughout your code a change in such a class would mean correspondingly large changes.
As Bob Martin notes in the linked video, this dependency is only one way. A framework change is felt by all users of that framework. Changes in the applications that use a framework cause no affect on the framework itself. There is no change that the author of an application that uses Spring Boot could make to their application that would change the Spring Boot framework, or affect any other application that makes use of Spring Boot.
The solution to all this as presented by Bob Martin is based on ideas described in Jacobson’s book. Clear boundaries demarcate an application. Anything within those boundaries is clean of any frameworks. The application’s interactions with the outside world occur outside of that boundary and it is only outside of these boundaries that frameworks may be present. They might be used to send and receive data from a web client, a database, or other types of IO, but not for any of the application’s core activities.
This model does not present a rigorous definition of an application but does insist that boundaries be clearly demarcated with a clear and definite separation between what is inside and outside. There are then several components that work together to implement the important activities of the application.
These are the objects that describe the behaviour of what you are modelling. In an object oriented language these fit nicely into a class. Importantly, these are plain classes. They are not Django models, nor EJBs, nor any other special class that inherits from a 3rd party framework.
These entities describe the data and behaviour at the core of your application, following business logic. If working in the business domain they model the behaviour of the real life object that they are designed to model. Each class does not need to know anything about whether it is in a web application, what type of database is used, whether this code will run on-prem, in the cloud, or in a mobile device.
Messages between entities and the outside world flow through the interactor. The interactor contains all of the business rules specific to this application.
Interactors and entities may seem similar but they are distinct and different components. Entities might fit into multiple applications as-is, whereas interactors are specific to an application. For example, imagine multiple video processing applications in a suite. These would all use video files, so would all make use of code that would describe the video codec, length, etc. of a video file. However, a video editing application would have application specific functionality: cut out some parts of a video, join together different parts into a file, etc. None of this functionality to manipulate video files is needed by a video playing application.
The application interacts with the outside world through boundaries. These are an interface that the application implements to support a type of boundary.
A boundary will receive a request model and process that into something suitable for the interactor. These request models are not specific to any type of delivery mechanism. They are not
HttpRequest objects, not GUI click event objects, nor anything of this sort. They are unaware of the delivery mechanism they support and so can be tested in isolation of that mechanism.
This idea is bigger than just the View from the traditional MVC pattern. These boundaries could be bridging the application to any of the external domains of the sort described just above: web, GUI, database, hardware, etc.
The entities and interactors encode the important behaviour of the application. This is all done with regular code available in the programming language, unencumbered by any framework. Work proceeds on this core part of the application without any concern to make it fit the behaviour of a framework.
It is clearly important that the data and logic at the heart of an application behave correctly. When separated from a framework there is less surface area for errors and security vulnerabilities. Fewer mocks are a sign of a healthy test suite. There is less to mock when eschewing frameworks. This creates code that is simpler, more straightforward, leading to simpler and more reliable tests. They are also easier and faster to run, thus more useful to the application.
It may still be the case that frameworks are used outside of the application boundary. However, with the application less tightly coupled to these frameworks they are less problematic. If they are used simply at the boundary, only to interact with the world via delivery mechanisms—or persist data in a specific data store—the application is less vulnerable to changes in their behaviour.