However, what is the right abstractions for your application?
Sadly, the choice of abstractions really comes from our choice of framework. Frameworks are basically abstract solutions that we extend to solve our problem.
Unfortunately frameworks, like Spring Boot, come opinionated about the threading models you use, interfaces you need to extend, possibly the data repositories applicable and various other assumptions about your problem space. That's a lot of restrictions before I've even written my first line of code.
What we really want to do is explore the problem space first. This is what test driven design is all about. We write tests to define what is successful code. Then we implement code to pass those tests. As we go along writing tests to cover off requirements, we subsequently churn out working code for the application. In time we get enough working code to release as the application.
So this leads me to ask, when do we test the choice of framework?
Opinionated frameworks force abstractions too early in the development process
Well, I guess we pay very experienced senior people to make this choice. So this choice must be correct. It would not be for reasons like:- I (or our company) only know this framework, so we are using it
- New shiny with lots of buzz words, we must use it
- My CVs a little old, let's try something new
- This one is cheaper
- Architecture believed what it says on the tin
Sadly, even if you test with the most risky aspects, finding out the framework decision is wrong can lead to a lot of wasted code. This arguably wastes is a lot of money for the business and can lead to failing projects.
For example, say we choose Spring Reactive. Yay, we can make concurrent asynchronous calls out to various micro-services. We can also use the latest in NoSQL data stores. This was all a great decision. However, over time we realise we have a small amount of data where integrity of the data is very important. We find we want to use a relational database to solve this, and then incorporate JPA on this database for easier interaction. However, our choice of Spring Reactive has disallowed this because it requires all I/O to be asynchronous (JPA is synchronous database calls). Ok, yes, we can use Schedulers, but I seem to be continually doing work arounds for lack of transactions. The data consistency issues are starting to mount up and we're missing deadlines. I'm now in a position of do I throw out all the Reactive code, or do I keep making work arounds hoping it might all hang together. I definitely need to swap jobs before this hits production and we start supporting it. In my next job, I've learnt to use Spring Servlets for this type of problem.
The flip side of this could also be easily the case. We start out wanting Spring Servlet for JPA interaction with a database. However, over time we realise the database interaction is mostly read-only. What we really wanted was asynchronous I/O from Spring Reactive to collect data from multiple micro-services and data stores concurrently. Unfortunately, with our up front Spring Servlet choice, the data collection is just too slow. Our work around is to use async Servlets and spawn threads to make concurrent requests. This worked initially, but over time the load increased. This significantly increased thread counts, resulting in thread scheduling starvation, which resulted in timeouts. I've really got no way to fix this without significant rewrites of the application. In my next job, I've learnt to use Spring Reactive for this type of problem.
So can look to test the framework without having to throw out all our code?
Inverting framework control
Dependency Injection went a long way in inverting control. When I write my Servlet handling method, I no longer need to pass in all my dependent objects. I would define dependencies, via @Inject, to have the framework make them available. The framework, subsequently, no longer dictates what objects my implementation can depend on.However, there is a lot more to a framework than just the objects. Frameworks will impose some threading model and require me to extend certain methods. While dependency injection provides references to objects, the framework still has to call the methods on the objects to do anything useful. For example, Spring goes along way to make the methods flexible, but still couples you to Reactive or Servlet coding by the required return type from the method.
As I need the Spring framework to undertake Dependency Injection for my tests, I'm coupled to the particular Spring Servlet/Reactive abstractions before I even write my first line of code. An upfront choice that could be quite costly to change if I get wrong!
What I really want to do is:
- Write tests for my implementations (as we are always test driven, of course)
- Write my implementations
- Wire up my implementations together to become the application
- Write tests calling a method passing in mock objects
- Write implementation of the method to pass the test
The Inversion of (Coupling) Control (IoC) provides this facade over the method via the ManagedFunction. The ManagedFunction interface does not indicate what thread to use, what parameters/return types are required, nor what exceptions may be thrown. This is all specified by the contained method implementation. The coupling is inverted so the implementation specifies what it requires.
This inversion of coupling allows framework decisions to be deferred. As I can have all my methods invoked in a consistent way, I can go ahead and start writing implementations. These implementations may require Reactive coding to undertake asynchronous calls out to different micro-services. Some of these implementations may require using JPA to write to relational databases. I really should not care at the start of building the system. I'm tackling the concrete problems to gain a better understanding of the real problem space. I know my methods can be invoked by the framework via wrapping them in a ManagedFunction. We can deal with determining the right framework later on, once we know more.
Therefore, it is no longer the framework being opinionated. It is your developer code that is allowed to be opinionated.
This then allows your implementations to be opinionated about the most appropriate framework to use. No longer do you have to guess the framework based on vague understanding of the problem space. You can see what abstractions your implementations require and make a more informed choice of framework.
In effect, IoC has deferred choice of the framework to much later in the development process. This is so you can can make the decision much more confidently. And isn't this what Agile says, defer the commitment until the last responsible moment.
Summary
In summary, why be forced to make too many up front decisions about your application? In choosing the framework, you are making some significant choices is solving your problem space. As frameworks are opinionated, they impose a lot of coupling on your solution.Rather, why can't I just start writing solutions to concrete problems and worry about how they fit together later on? This allows me to make choices regarding the appropriate abstractions (and subsequently framework) when I know a lot more about the problem space.
Inversion of (Coupling) Control gives this ability to defer abstraction and framework choices to much later in the development process, when you are more informed to make the decision correctly.