Frameworks are awesome. Web frameworks such as Rails, .NET MVC, Django, Node, Flask, Sinatra are powerful accelerators of software development. They provide scaffolding that you can use, but you don’t necessarily have to write or maintain.
Frameworks are alluring. They often focus on providing features that are geared to accelerate development and are eminently demoable. Developers are encouraged to use all features everywhere. Those demos never seem to discuss the problems that could arise by using the feature as the system grows over the years. The demos do not discuss how to protect the system using them against how the feature may change.
My experience over the years while working on large and small systems has correlated extensive coupling to a framework across the whole code base to almost always being a bad idea. For a small enough system it can work. But systems seem to rarely stay small without a lot of premeditated effort.
Systems make it easier or harder to do the right thing. A system that makes it easy to do the right thing allows its developers to fall into the pit of success, no matter the strength of the developer. Frameworks far too often encourage designs that are tightly coupled to the framework. As a result, better developers are needed in order to ensure a good, sustainable design.
An alternative design
An alternative design is to keep the framework at the edges of the system. This is similar to (but possibly not as hardcore as) the hexagonal architecture / clean architecture / screaming archicture. [https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html]
Controllers, in the Rails and .NET MVC sense, are about converting the web into API calls to your code. Views are about displaying the information that they need. Database objects – active record or inside a repository – are for accessing the database. The business logic that the controllers call, that gets presented to the view, that transforms inputs into models that could be persisted – that doesn’t need a framework. Any code written in your language that does not rely on your framework is not going to break when the framework is upgraded – or replaced.
When using a database, keep all the data access behind an API that returns single, or lists of, objects that you can use. Pass those objects to be manipulated with your system’s logic in non-framework code. Save the results back to the DB by passing objects (or hashes) to the database API.
Implementing the database code behind an interface means you can change it at will – even swapping out a database implementation if you must. It is a single seam for data access of the application. Keeping database access at the edges of your code allows optimising database calls in one place instead of having multiple calls spread throughout the business logic code. This may result in loading more data at one time initially, but deal with a memory problem when you have one, rather than smearing your logic and your framework code together across your code base.
// Get the parameters from the web firstname = params[:firstname] lastname = params[:lastname]
// Fetch objects from the DB contacts = Contacts.fetch_for_name( firstname, lastname)
// Do something with the objects results = ContactExportFormat.transform(contacts)
// Persist to the DB / export and assign the result for the view to display @result = ContactExportLog.persist(results) // send_file / send_data the export
Selectively break the rules, when you must
When you need to optimize some code due to speed or memory constraints, then potentially mix logic and database and other external accesses, but as a last resort. Lean towards enumerable objects that do lazy evaluation and yield to the business logic so that these dependencies are clear and separate. But start with clean, clear separations and muddle them up for a clear and obvious external business goal of performance.
Focused functionality provided by a framework is great. For example routing to a controller is a common pattern implemented by many web frameworks. It is something that is clear and contained and testable. The goodness does a specific thing and it can be tested and proven to work.
Unfocused, extensive framework usage is a problem. The understanding of the reason for its usage can be lost. Once it is lost, it becomes incredibly difficult (if not impossible) to get it safely under control again as no one is really sure what it is doing or why it is there.
Focused usage allows change to be isolated. If the framework makes a change to how params are dealt with, then if we know we only deal with params in controllers, then we know where to look to fix it. If we’ve allowed params to be used randomly throughout the system we’re in trouble. Isolate change. And make sure the behaviours that are expected are codified in tests so we can know if the framework changes that things still work.
The Framework Moves
Frameworks move at a steady pace. Bugs are fixed, security patches are applied, features are added and deprecated. All of this happens at a reasonable rate. For most code bases that live for more than a couple of years, the framework will be able to be upgraded multiple times. The less code that is written that is coupled to the framework, the less painful that upgrade will be.
If all your code inherits from a framework class, you are doing something wrong.
Limit frameworks to do well understood, localised and controllable things. These could then be replaced with some other version of the framework easily.
Frameworks that force you to design in a specific way and are smeared around your entire code base will probably cause a world of pain in the long term. (Or lets be honest, probably not you but the business who owns the code and the poor guy who now owns the code.)
It can be faster to just do it the framework way initially. In the short term. But it might not be responsible in the long term. The economics of short term gain over potential later cost are hard. But if you expect the code to be around in more than 2 years, they should stop being hard.
Frameworks are awesome.
Held at an arms length. Under your control.
But keep in mind, non-framework code will always survive a framework change.