In software engineering, a framework is an abstraction in which software that provides generic functionality can be modified by implementation-specific code in order to result in application-specific software.
A good framework adheres to the SOLID principles of object-oriented design. These principles are:
- Single responsibility principle. A class should do just one thing (and do it well).
- Open/close principle. A class should be open for extension but be closed for modification.
- Liskov substitution principle. Objects in a program should be replaceable with instances of their subtypes without breaking the application. See: polymorphism.
- Interface segregation principle. Many client-specific interfaces are better than fewer general-purpose interfaces.
- Dependency inversion principle. Depend upon abstractions, not concretions. See: interfaces or protocols.
These principles are mostly self-explanatory, though I think the open/close one is a little more ambiguous in that deciding to which degree a class should be open or closed is often a matter of circumstance and of opinion.
Personally, when dealing with the access modifiers such as private, protected, internal or public (which may be named differently or which may not even exist at all depending on the language in question), I make very few methods and properties private to the class itself because it’s impossible to be certain that they won’t need to be accessible or be extended in a subclass at some point down the line. So by default I tend to use whichever access modifier makes the method or property accessible to the class that defines it as well as to its descendants – but which prevents access from outside.
Of course there are occasions when private is more appropriate. But few things are more infuriating than trying to extend a supposedly extensible framework class and discovering that a method or property that is required for this new functionality is inaccessible. Deciding between duplicating the inaccessible functionality or forcing access to it with a hack is unpleasant and shouldn’t be necessary.
One other notable point about the SOLID principles is that, in general, they are numbered according to their importance and to the impact that they will have on a codebase. That is, with every principle that is adhered to, each subsequent principle becomes increasingly easy to adhere to; similarly, if any principle is not adhered to then subsequent principles will become increasingly difficult to adhere to.
With that in mind, it is imperative to have ‘single responsibility’ at the forefront of your mind when designing your framework, because if that principle is adhered to correctly then the others almost fall into place by themselves.
When it all goes wrong
Recently I was working on a project that was based on a framework that was developed by an internal team. This framework had been extracted from a previous product which is always a tricky proposition, but if the SOLID principles above are adhered to then it is possible to extract a usable framework in this way.
However, what was painfully clear on this project was that these principles had not been adhered to at all: classes were often enormous with multiple responsibilities; they were difficult to extend with the methods and properties that would be useful to a subclass often being private; subclasses would often disregard their supertypes’ contracts; and interfaces were like gold dust with the vast majority of the framework written for concrete implementations.
On top of all that, the framework also contained lots of implementation-specific code that the framework team must have felt was so intrinsic to the company’s applications that having it written into the framework was acceptable. But in the real world where a client’s prerogative is to make unexpected (and sometimes illogical!) change requests, the result was that the application was sometimes forced to extend classes that performed unwanted work, only to have to undo that work itself immediately afterwards. Needless to say, this was wasteful.
In some cases, the framework’s classes performed so much unwanted work that – in the interest of application performance – it was actually necessary to exploit the order in which packages were loaded by the compiler to override the framework’s class with a local application version, just so that all that unnecessary work could be stripped out. And yes, this also meant that any of the framework class’ code that we wanted to maintain had to be duplicated. Why not simply substitute the class? Because as mentioned above, everything was written for concrete implementations.
Unsurprisingly, this framework felt more like a monster that had to be wrestled with than a helpful tool. The development of every new application feature was tedious, laborious, frustrating and time-consuming. But a rewrite wasn’t possible because the framework was too large and was used by too many applications.
Keep it simple
As well as adhering to the above SOLID principles, it is important to remember that the whole point of a framework is to provide an abstract foundation onto which an application developer can build their application-specific software with their implementation-specific code: a framework should therefore NEVER contain implementation-specific code itself!
This is not to say that a framework developer is prohibited from providing their application developer colleagues with functionality that is common to their company’s applications – they may indeed do so, but the correct approach is not by polluting the framework with it because that should remain abstract: instead, this common functionality should be placed in a separate package, allowing the application developer to use that functionality if they want to and not because they have to.