Tuesday, March 26, 2024

From Ruby on Rails to Relatively Plain Java

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:

  1. The concept of a MVC Controller
  2. The ability to test a controller action, e.g. a GET request from start to response body.
  3. Asset fingerprinting
  4. Database migration support
  5. Basic SQL statement support
  6. SQL transaction support
  7. Simple app configuration
  8. The ability to launch the app as an executable from the command line
  9. Related to 8, a systemd service unit for the app
  10. 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:

  1. Embedded Tomcat and the Jakarta API for the bottommost webserver layer, and basic Request/Response encapsulation, respectively.
  2. Hikari for a database connection pool
  3. log4j2 for logging to STDOUT with log levels
  4. Thymeleaf for templating
  5. The Typescript, esbuild, and scss npm projects for building frontend CSS and Javascript
  6. JUnit for unit testing, and Mockito for mocking support. I built my own factories.
  7. 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.

No comments:

Post a Comment