I'm a Floridian web engineer who planted his roots in Paris. Some of my favorite past-times are analyzing and optimizing development processes, building solid APIs, mentoring junior devs, and long walks on the beach.

STI Models and a Misconception of DRY 02-25-2017

One of the biggest problems in programming is how to handle and simplify the logic that shows up in your application. Logic which shows up uniformly throughout a class tends to be a smell which indicates the need to represent multiple different concepts. Here is an example of how this can manifest:

Example:

class ThingController < ApplicationController
  def create
    if creation_params[:kind] == 'First'
      First.create(creation_params)
    else
      Second.create(creation_params)
    end
  end

  private

  def creation_params
    params.require(:thing).permit!
  end
end

What's happened here is that we have two separate classes, here First and Second. If you are familiar with STI then you probably have seen this sort of thing often. The problem is that the objects share a data schema, but they will have potentially different views, potentially different validations and they will do potentially different things, ie they have different domains.

Keep it DRY

DRY

People often wrongly cite that they are staying DRY by merging two domains or by combining two functions. But the concept is actually not referring to code. The principal is referring to having a single representation knowledge or a single source. So an example of keeping code dry is to not have a constant show up in all of your view code.

.header
  %p Default circle size is 300 mm.
  .blurb
    Space remaining:
    %span = circle.size - 300

Instead you would store the size as a constant or a method in a class.

class Circle
  def self.max_size
    300
  end
end

.header
  %p= "Default circle size is #{Circle.max_size} mm."
  .blurb
    Space remaining:
    %span= circle.size - Circle.max_size

Manifestation of Excess Logic with STI:

If we look back to our original controller example, we see that there are two separate domains that are being handled by one single controller and that while the total amount of code is less(potentially), we haven't actually simplified our lives. As we add more subclasses to the STI model we will see an unhappy pattern emerge. Potentially we will have a case-statement to select our object and to have them do different things. We will have to define functions like this outside of our models that will then manage the models.

The solution is to not be afraid of having more files, not thinking that less code is necessarily more manageable. Treating the two models as separate resources will make maintenance easier from our routes to our views.

The problem with putting two separate models in the same controller is that there is a branch that starts in the route, and goes all the way to the view. The only argument that I have seen is that you can save on the amount of code you write by putting them in the same controller and giving them the same route. Doing so however makes all of the code associated more complicated as there is now a branch for every reference to the model. If there really is no divergent domain code, then there is no reason for STI. STI is not a way for you to treat two separate things as the same, it is a way for you to treat two things with the same data as different.

Tags: dry STI Rails