A Look at Framework Coupling
A Look at Framework Coupling
You know that tightly coupling your code to underlying frameworks is bad, but how bad is it, exactly?
Join the DZone community and get the full member experience.Join For Free
Ready for feedback: How would you use Capital One’s Virtual Card Numbers API?
The first post that ever appeared on Tidy Java blog was about framework coupling. In the post, I laid out a vision of evil frameworks that destroy your project, your company, your career, etc. The catastrophic tone of that post was a result of my fascination with Uncle Bob's teaching and strict dependency management in general. Now, after eight months have passed, my views have changed a lot, my blog has changed a lot, and I think it's the right time to take another look at the topic.
What Is Framework Coupling?
Framework coupling is the degree to which your application is bound to the framework or technology stack that you're using. It would be at its lowest when all the code related to using the framework resides in separate files and the application has no knowledge of the framework being used. On the other side, the highest degree of framework coupling would mean that almost entire application logic is written using framework's capabilities, and if references to the framework were removed, the remaining code would not make any sense at all.
At the lower level, framework coupling appears as the direct usage of a framework's APIs, using framework's annotations, launching the framework in unit tests, and so on. Using framework's naming conventions can also be considered framework coupling, although it does not alter your application's behavior (hopefully).
Consequences of (High) Framework Coupling
High framework coupling, as defined above, comes with important consequences. Let's go through them one by one.
Shotgun Version Updates
(I named this consequence after Shotgun Surgery code smell.)
Whenever you want to use a newer version of the framework, you're exposed to the risk of modifying multiple source files because of retired or deprecated APIs. In good cases, the modifications are as straightforward as fixing the imports or changing the used class and method names. In severe cases, the whole philosophy behind a feature might have changed and you might end up reimplementing big chunks of your application.
This is directly related to the previous point. Whenever we update our project to use a newer framework's version, we're at risk of breaking existing application features. Such problems are most likely to surface when the tests are referencing the framework and we're updating to a new major version. In such case, both the tests and the production code might stop working, which leaves us with no safety net. Another type of problem might arise (and not be immediately spotted) when we rely on framework's undocumented behavior. Such stuff can be subject to change even if the public API remains untouched. In this case, we're left at the mercy of our test suite.
This is the last of the version-related consequences. Some features that we found crucial for our application's well-being might be deprecated or completely removed from future framework's version. This would imply at least rewriting the future and, at worst, rewriting the application. In general, it's important to understand that framework designers care about being cutting-edge and encouraging more people to use their creation. This might not go hand-in-hand with our application's needs.
When working with any code that has been already deployed (as a jar or the like), we cannot change it according to our testing needs. Thus, we must comply. The situation is no different when working with frameworks. As test-friendly as the framework is, as easy it will be to write tests for our application's code. Mocking tools are nowadays extremely powerful, so simulating a framework's behavior should not be a problem. Things are not as good when it comes to testing time and configuration. In heavily framework-coupled applications, a lot of tests require spinning up the whole framework machinery, which can dramatically slow testing time and put extra configuration burden on developers.
This should be pretty obvious. Too much coupling with a framework might mean that we'll never be able to change it because that would take huge amounts of time and put us at risk of breaking the currently working code. Another thing that might fall under lock-in might be an inability to use certain libraries or their version because of their framework incompatibility. In that case, we might be left with worse or less-suited framework-compatible replacements.
When you take a look at a framework-coupled system, you'll have no problem recognizing technologies and conventions being used. However, the system's heart, the business problems that it's supposed to solve, might be completely opaque. This might indicate suboptimal design decisions because the domain has been fit into the framework standards instead of the framework being used for the purpose of the domain.
Enough disadvantages. If things were so bad, nobody would use frameworks. Let's look at the bright side.
Idiomatic Framework Usage
A lot of common problems have idiomatic solutions prepared either by the framework designers or by its users, based on experience. By abandoning the dreams of fully framework-independent application logic, we're opening ourselves for the benefits of solutions like wide adoption, simplicity, lots of tutorials, and available community support.
As we'll see in a future post, actively avoiding framework coupling can introduce a lot of indirections to the project. Although they might seem beneficial, they may also cause confusion and force people to dig through extra source files to find out what's really happening. Using the framework directly solves the problem.
Less Tedious Coding
Wrapping every framework service and mapping each framework data structure to our own can be a boring, error-prone task. Coupling to the framework in some places can free us from the burden of excessive wrapping and mapping.
I could probably think of more, but the general picture should be clear at this point. We're basically trading purity and safety of our application classes for simple and direct solutions available for the framework being used.
Scale of the Problem
When talking about framework coupling and possible negative consequences, it's important to realize the impact range. In the original text, I stated that "microservices don't change anything" because of "no difference between having one huge poorly designed application and having 20 small poorly designed applications." While the argument itself makes sense, we need to realize that a poorly designed monolith and a poorly designed microservice can look completely different.
Framework Coupling and Monoliths
In the case of a monolith, each of the consequences of framework coupling will span across a lot of code. The person updating the framework's version does not only update it for his or her part of the project; he or she updates it for the entire thing. This means that multiple teams might have to be involved at the same time in updating framework's version. Also, the bigger the system is, the more tests there are and the more complex configurations have to be done for testing purposes. Therefore, the impact of slower testing time can be meaningful, often way above acceptable. As for framework lock-in, dependency management in big systems can be quite a challenge because every part of the system might have different requirements and, in different parts of the system, similar requirements may be fulfilled in different ways. Adding a heavy framework to the pile almost certainly won't help.
Framework Coupling and Microservices
Almost all of the negative consequences of framework coupling might become less significant when dealing with microservices. There's less code to update, less testing to be slowed down, and a smaller cost of rewriting significant portions of the application. There seems to be an interesting tension between first five consequences and the last one. The bigger the microservice is, the more burden will come due to the first five; the smaller the microservice is, the less domain logic it contains, which might then appear insignificant comparing to other classes. Obviously, we have to multiply the (smaller) problems by the amount of microservices. Therefore, I'd be particularly careful about things that might force me to "roll over" all my application's codebases, whatever these might be.
The text got a bit lengthy, so we'll stop there for now. In the next post about framework coupling, we'll have a look at techniques to limit its impact and find the sweet spot when working with frameworks.
Published at DZone with permission of Grzegorz Ziemoński , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.