I sometimes hate software frameworks. More precisely, I hate their rigidity, and cookie cutter systems of mass production.
Personally, when someone tells me about a cool new feature of a framework, what I really hear is “look at the shine on those handcuffs.”
I’ve defended this position on many occasions. However, I really want to put it down on record so that I can simply point people at this blog post the next time they asks me to explain myself.
Why Use a Framework?
Frameworks are awesome things.
I do not dispute that.
Just from the top of my head, I can list the following reasons why you should use a framework
- A framework abstracts low level details.
- The community support is invaluable.
- The out of the box features enable you not to reinvent the wheel.
- A framework usually employs design patterns and “best practices” that enables team development
I’m sure that you can add even better reasons to this list.
So why would I not be such a big fan?
What Goes Wrong with Frameworks?
Just to reinforce the point, let me say that again: VENDOR LOCK-IN.
There is a natural power asymmetry between the designer/creators of a framework and the users of that framework.
Typically, the users of the framework are the slaves to the framework, and the framework designers are the masters. It doesn’t matter if it is designed by some open source community or large corporation.
In fact, this power asymmetry is why vendors and consultants can make tons of money on “free” software: you are really paying them for their specialized knowledge.
Once you enter into a vendor lock-in, the vendor, consulting company, or consultant can charge you any amount of money they want. Further, they can continue to increase their prices at astronomical rates, and you will have no choice but to pay it.
Further, the features of your application become more dependent on the framework as the system grows larger. This has the added effect of increasing the switching cost should you ever want to move to another framework.
I’ll use a simple thought experiment to demonstrate how and why this happens.
Define a module as any kind of organizational grouping of code. It could be a function, class, package, or namespace.
Suppose that you have two modules: module A and module B. Suppose that module B has features that module A needs; so, we make module A use module B.
In this case we can say that module A depends on module B, and we can visualize that dependency with the following picture.
Suppose that we introduce another module C, and we make module B use module C. This makes module B depend on module C, and consequently makes module A depend on module C.
Suppose that I created some module D that uses module A. That would make module D depend on module A, module B, and module C.
This demonstrates that dependencies are transitive, and every new module we add to the system will progressively make the transitive dependencies worse.
“What’s so bad about that?” you might ask.
Suppose that we make a modification to module C. We could inadvertently break module D, module A, and module B since they depend on module C.
Now, you might argue that a good test suite would prevent that … and you would be right.
However, consider the amount of work to maintain your test suite.
If you changed module C then you would likely need to add test cases for it, and since dependencies are transitive you would likely need to alter/add test cases for all the dependent modules.
That is an exponential growth of complexity; so, good luck with that. (See my post “Testing software is (computationally) hard” for an explanation).
It gets worse, though.
The transitive dependencies make it nearly impossible to reuse your modules.
Suppose you wanted to reuse module A. Well, you would also need to reuse module B and module C since module A depends on them. However, what if module B and module C don’t do anything that you really need or even worse do something that impedes you.
You have forced a situation where you can’t use the valuable parts of the code without also using the worthless parts of the code.
When faced with this situation, it is often easier to create an entirely new module with the same features of module A.
In my experience, this happens more often than not.
There is a module in this chain of dependencies that does not suffer this problem: module C.
Module C does not depend on anything; so, it is very easy to reuse. Since it is so easy to reuse, everyone has the incentive to reuse it. Eventually you will have a system that looks like this.
Guess what that center module is: THE FRAMEWORK.
This is a classic example of code immobility.
Immobile code signals that the architecture of the system doesn’t support decoupling.
You can argue that the framework has so much reusable components that we ought to couple directly to it.
Ultimately, we value code reuse because we want to save time and resource, and reduce redundancy by taking advantage of already existing components.
Isn’t that exactly what the framework gave us?
Well, yes and no.
It depends on what you want to reuse.
Are you trying to reuse infrastructure code? By all means, please use a framework for that.
Are you trying to reuse domain specific business logic? You probably don’t want to couple your business logic directly to a framework.
An example of coupling your business logic directly to a framework is your typical MVC-ORM design. I’ve already explained this in my blog post “MongoDB, MySQL, and ORMS: when and where to use them”; so, I will not elaborate on it, here.
The Best of Both Worlds
So it seems that we are at an impasse: we want to reuse infrastructure code from a framework, but we also want our business logic to be independent of it.
Can’t we have it both?
Actually, we can.
The direction of the arrow in our dependency graph makes our code immobile.
For example, if module A depends on module B then we have to use module B every time we use module A.
However, if we inverted the dependencies such that module A and module B both depended on a common module — call it module C — then we could reuse module C since it has no dependencies. This makes module C a reusable component; so, this is where you would place your reusable code.
The Dependency Inversion Principle exists to enable this pattern.
The typical repository pattern is the perfect example of how inverting dependencies can decouple your business logic from a framework. I have already talked about it in my post “Contract Tests: or, How I Learned To Stop Worrying and Love the Liskov Substitution Principle”; so, I won’t elaborate on it, here.
So how would you structure your application to support the inverted dependencies?
Well, there are multiple ways you can legitimately do it, but I use the strategy of placing the bulk of the applications code in external libraries.
Under this model, the framework’s views and controllers only serve as a delivery mechanism for the application.
Consequently, we can easily move our code to another framework because the components do not depend on the framework in any way.
As an added benefit, we can test the bulk of the application independent of the framework.
In fact, this is your typical Component Oriented Architecture.
For example, standard Component Oriented Architecture will break large C++ applications into multiple dll files, large JAVA applications into multiple jar files, or large .NET applications into multiple assemblies.
There exists rules about how you structure these packages, however.
You could argue that we would spend too much time with up front design when designing this type of system.
I agree that there is some up-front cost, but that cost is easily off-set by the time savings on maintenance, running tests, adding features, and anything else that we want to do to the system in the future.
We should view writing mobile code as an investment that will pay big dividends over time.
I plan to create a very simple demo application that showcases this design in the near future. Stay tuned.