The problem with the Ruby on Rails library,
compared to other libraries like PostgreSQL,
is that Rails is mostly trying to apply the DRY
principle across multiple web server projects,
whereas the PostgreSQL library
is making difficult algorithmic design decisions.
That PostgreSQL actually listens to a TCP socket
while Rails establishes a shared runtime with your
project is incidental to the argument I am making
in this blog post. Libraries that are predominantly concerned
with DRYing code disenfranchise the developers
who make use of them. They add a layer of hardship
because you typically don't have easy access to their
source code if and when the shit hits the fan somewhere
from within their complexity. Beyond the distanced access
to the code, they add a challenge because their complexity is daunting
even when your app is still relatively simple in scope (again,
when problems emerge from within them).
To join my side on this, you have to be able to hold
a B-Tree index implementation as distinct from a piece
of ORM code that merely generates an SQL Insert statement,
based on a struct instance and table schema it found somewhere.
Otherwises you're just going to counter that nobody
wants to write B-Tree indexing 10 times in their life
when they can just count on PostgreSQL to do it one
time for them (and everyone else).
If you wrote B-Tree indexing on two projects, and decided
to pull that logic out into a library that the two could
share, you would not call that "refactoring". "Encapsulation"
would be a better term.
I decided about a year ago that I wanted to write a web
app using Java. It would be my first time seriously using
Java to perform a task. I was coming from a 10+ year background
in Ruby on Rails. What were my goals?
I wanted my app to be simple. I started off by investigating
Spring, both the framework and the Spring Boot wrapper.
I investigated the ORM facilities in it and Hibernate.
I decided that I wanted my app to be simpler than a spring
project. I didn't even want to use dependency injection.
I started out with Spring Initializr and spring-boot-starter-web.
Slowly, I pulled out the Spring Boot wrapper, and eventually
even spring-beans. I pulled out the spring-webmvc support.
I pulled out Hibernate. "Simple" for me
meant that I wanted to spend a lot less time reading
documentation and more time writing code.
Without Spring, I needed to do some work to get to
what I thought was minimal infrastructure on which
I could start addressing project-specific requirements.
I determined that I wanted infrastructure for
at least these features:
- The concept of a MVC Controller
- The ability to test a controller action, e.g.
a GET request from start to response body.
- Asset fingerprinting
- Database migration support
- Basic SQL statement support
- SQL transaction support
- Simple app configuration
- The ability to launch the app as an executable
from the command line
- Related to 8, a systemd service unit for the app
- A way to store user sessions within a signed browser cookie
I also made use of some libraries to provide me
with some features. These included:
- Embedded Tomcat and the Jakarta API for the bottommost webserver
layer, and basic Request/Response encapsulation, respectively.
- Hikari for a database connection pool
- log4j2 for logging to STDOUT with log levels
- Thymeleaf for templating
- The Typescript, esbuild, and scss npm projects for building
frontend CSS and Javascript
- JUnit for unit testing, and Mockito for mocking support. I built
my own factories.
- Jackson for JSON help
None of the things on these two lists really had any instrinsic relation
to my application's requirements. They were very generic
needs. But, I couldn't start to work on my application until
these pieces were in place.
I spent what felt like a month migrating from a Spring boot
"hello world" project to a place where all of the above
was taken care of, without the use of Hibernate, spring-beans,
spring-webmvc, or spring-context.
The most obvious consequence of my approach was that writing
features took longer. There was one task that I estimated writing
in Rails would have taken 10 minutes, but which I spent 3 hours on.
It was very easy for me to forecast 5 hours for a feature that
I thought was kind of small, but here I was. I didn't have
the powerful ORM features at my disposal. I had to write SQL
for any database need I had. I had to write unit tests, both at
the model level and controller level, for every feature. And of course
I had to strongly type my app.
Another concern I have about Rails is that it is dynamically typed.
This means that there is no compiler to help you find type errors
at a compile phase, whereas with Java and Typescript, you do get
support with that. Yes, it takes a little longer to write your code
but at least you don't feel hopeless when it comes time to refactor
a big chunk of your project. Refactoring in Rails is a nightmare
and your unit tests become extremely important in that context.
You breathe far easier when you know the Java compiler is going to
see what wires you did not reattach correctly.
I do not feel like I escaped writing unit tests. There is the argument
that if you're just going to write unit tests anyway, then what is
the point of typing? Is it not redunant with the unit testing?
My response to this is that as a developer, you want to know in
the fastest amount of time that you've made a mistake, and unit
tests are not the fastest way to know that. The compiler is faster.
Even though the there was a greater expense of time on this project,
the result that I arrived at was more sturdy. If you were developing
a video game, wouldn't you want it to never crash? If so, why would
you not embrace static typing? Video games are silly wastes
of time, as everyone knows. But we still want them to work. Where
pride is concerned, I think you want your video game to work just
as much as you want code in the NASA Space Shuttle to work.
Aren't you willing to wait a longer priod of time for your code
to be done, then? This way your app is sturdy. When I say sturdy,
I particularly am referring to when refactoring time comes. You
can make adjustments and not feel so terrified that you're going
to break what was working yesterday.
I wrote above that libraries like Rails disenfranchise the developers
who work with them. On this project of mine, I went to use the Devise gem on a
toy Rails project while building a tour of some Ruby code. There is a step in the Devise
setup where you invoke a Rails generator. All the crypticness of Rails
came back to me when I invoked that. I had no idea what it was doing.
Why are there ~50 files in a Rails project when I haven't even
done anything yet? The Rails library is supposed to support the thousands
of Rails projects out there. When you use it, all of the if-statements
that have gone into supporting 9,999 other projects besides yours
suddenly become your responsibility. It doesn't seem that way,
but that's the reality, isn't it? You're responsible for what
your app does even if you choose to use Rails. But do you really feel
responsible for all of the code inside of Active Record? You're responsible
to your customers regardless of what library philosophy you choose.
If you choose Rails, you're inheriting responsibility for thousands
of lines of code that you know very little about. That places you
in an unfair position of powerlessness. And as I mentioned,
when it's time to debug that code, it's not in the Zeitwerk watch list,
so you have to restart your app every time you edit the outside source.
That's after you've navigated outside of your project folder and into
the gem sources to find it.
You could wonder how I feel about Sinatra. That's simple, right?
If I were to pursue the above philosophy with Rails, I would write
a Rack app without Sinatra. Rack would be where I draw the line.
I'd use rspec, still. I'd get controller infrastructure going on my own. I'd
use haml or erb for my views. But I wouldn't use Rails, Sinatra, or Active Record.
There is an enormous movement called convention over configuration. I think
it would be redundant for me to evaluate it here, given what I've already
written. I am much less of a fan of that movement as of today.