We use frameworks all the time. Some are part of third party libraries, others are self made. What options do we have if requirements change, especially for the custom frameworks?
What I mean with “framework”
The term framework has many different definitions. To get misunderstandings and confusion out of the way, here is what I consider a framework,at least in the scope of this blog post:
- A framework is a set of functions and/or classes that together implement a general use functionality that is used throughout a larger part of a project.
A framework may consist of only a single class or it might be a whole library. The key point is the widespread use. Another point is that classes and functions that do not serve the same functionality are not part of the framework.
An obvious example would be a GUI library. But also `std::shared_ptr`, `std::make_shared` and `std::weak_ptr` form a framework of sorts.
Implications on framework design
The widespread use of a framework means that changes to the behavior and/or interface of the framework may affect large parts of a code base and cause necessary changes in a lot of places.
In other words, the usual agile approach to add and refactor functionality when it’s needed, but not earlier does not quite cut it when it comes to frameworks. Therefore framework should be designed with every functionality in mind that it will have to support, because later changes can be costly.
That does not mean that functionality should be implemented that is not needed yet. It also does not mean you should prepare for anything that some day might be needed. Over-generalization is a bad thing.
It means you should make a proper assessment what is likely to be needed in the framework and design it in a way that makes those changes possible in a way that does affect the clients as little as possible.
No matter how good the design of a framework is, the day will come when there are needs that can not be met directly. There are several options that should be considered.
Bend the code
Due to the widespread effect that a framework change could have, it is often an option to tweak the client code in order to fit into the framework. It is sometimes an awkward position to be in, because you might have to break a design principle, violate a coding guideline or commit another minor crime against a clean coding style.
Up to a certain degree, such tradeoffs are acceptable, but if they get too harsh or happen too often, it is better to consider other options.
Change the framework
This is the opposite extreme to changing the code and letting the framework untouched. Switching to a different framework, be it self made or third party, has several that go beyond changing all the calls to a new interface.
Especially if you have a third party framework, the underlying philosophy might be slightly different, meaning that seemingly similar usage of the framework has different semantics. You have to understand those small differences in order to use the new framework correctly and effectively.
That brings us to the next cost: Learning the framework. Even if it consists only of a handful of functions or a single class it is still a cost you have to consider. The developers in your team might be quite comfortable with the old framework, and it will take some time until they get used to the new thing that suddenly pops up all over their code.
Consider writing a wrapper or adapter for the functionality the old and new framework provide. That way you can make the transition to the new framework significantly smoother.
Layering the framework
If the behavior you seek is already achievable within the framework, for example by bending the code as described above, then another layer can make the usage of that behavior more natural than e.g. a repeating series of function calls.
Adding a layer does not really change the framework and can be considered cleaner, but it is not always a possibility to solve the problem you have. You have to consider if you want to have all calls to the framework changed to the new layer or if the old framework and new layer should coexist in the code.
For the sake of simplicity I would advice against using both access possibilities in parallel On the other hand, finding and changing all calls from the original framework to the new layer can take a lot of time.
Extending the framework
If the change you want to apply to the framework has been considered in the first design this means just implementing a function that was already there in your mind. The impact on the code base should be minimal.
Anything beyond that has to be done with extreme caution. By adding functionality that has not been planned before means you effectively change the whole framework. Any code that called it before the change is developed unaware of the new functionality.
New functionality sometimes adds new semantics and can slightly change the perceived meaning of existing code. You might even want to revisit all call sites to make sure that the way they use the framework is still appropriate.
Whenever you add functionality, you should keep the framework as a whole in mind. The whole picture should be consistent. Especially, don’t circumvent or violate any assumptions or invariants the framework had before.
Breaking the framework
Frameworks are not very flexible. Trying to bend a framework to your needs and violating its original design breaks it, making it a pain to use and hard to replace.
I have seen a framework that added intrusive reference counting to a class hierarchy. Any object of that hierarchy could “use” another object, increasing its reference count. It then could “free” the object again. The last “free” would delete the object.
Today this would have been done with `std::shared_ptr` or other similar smart pointers. It would have been easy to replace the original framework with such smart pointers if its original design had been left intact.
At some point in time there seems to have been a problem with mismatched use/free calls causing double deletes. The fix at that time was to sometimes not delete the object on the last “free”. The non-deleting free got used more and more, leaving some memory leaks, but not fixing the potential for the spurious double delete once in a while.
With that addition and a few more minor changes,such a framework can no longer be easily replaced by `std::shared_ptr` or other common smart pointer alternatives.
Frameworks are the hard rock foundation of your applications. Choose them with care and treat them carefully. If you break them, your whole application might come tumbling down.