Inheriting From Built-In Ruby Types

The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information.
— Alan Perlis

I was teaching a class on refactoring and
wanted a real-world example to demonstrate finding a class to
extract
. My students
were Rails developers, so I immediately knew I wanted to show an example of an
ActiveRecord model.

After skinny controllers and fat
models

became a Rails best practice ActiveRecord models have tended to grow without
bounds. It's easy to push code down into them, but developers seem to have
some kind of mental block causing them to think they can't create classes that
aren't more ActiveRecord models.

Though I've seen this often enough I couldn't use any client code. I scratched
my head a moment and thought of
Discourse, a new forum project. I
went directly to the User model because it's so commonly a god
object
. (Just to be clear, I'm not
trying to pick on Discourse. This is a fairly minor improvement for a very
common problem.)

I scrolled down past the usual mass of associations, validations, and hooks to
the method definitions. Here's the first eight:

def self.username_length
def self.suggest_username(name)
def self.create_for_email(email, options=())
def self.username_available?(username)
def self.username_valid?(username)
def enqueue_welcome_message(message_type)
def self.suggest_name(email)
def change_username(new_username)

From this quick glance there's a Username class waiting to be extracted.
It's in the name of several methods, it's the sole argument to many methods,
and most of the methods are class methods, implying they don't maintain any
state (class-level variables being rare in Ruby).

It's discouraging to think about extracting a Username. It's stored as a
varchar in the database and ActiveRecord will choke on validations if it
can't treat it as a string.

The solution is straightforward: Username should inherit from String.
Username is-a String, and keeping it in the built-in type is why this
code sprawls
.

I extracted the below class, changing as little as possible, mostly just
removing 'username' from the start of method names and using self where
appropriate. I left behind the unrelated create_for_email,
enqueue_welcome_message, suggest_name, and the tempting change_username,
which was about editing a User.

class Username < String
  def username_length
    3..15
  end

  def suggest
    name = self.dup
    if name =~ /([^@]+)@([^\.]+)/
      name = Regexp.last_match[1]

      # Special case, if it's me @ something, take the something.
      name = Regexp.last_match[2] if name == 'me'
    end

    name.gsub!(/^[^A-Za-z0-9]+/, "")
    name.gsub!(/[^A-Za-z0-9_]+$/, "")
    name.gsub!(/[^A-Za-z0-9_]+/, "_")

    # Pad the length with 1s
    missing_chars = username_length.begin - name.length
    name << ('1' * missing_chars) if missing_chars > 0

    # Trim extra length
    name = name[0..username_length.end-1]

    i = 1
    attempt = name
    while !attempt.available?
      suffix = i.to_s
      max_length = username_length.end - 1 - suffix.length
      attempt = "#{name[0..max_length]}#{suffix}"
      i+=1
    end
    attempt
  end

  def available?
    !User.where(username_lower: lower).exists?
  end

  # export a business name for this operation
  alias :lower :downcase
end

And it's used as:

u = Username.new 'eric_blair'
u.available?
new = u.suggest
User.new username: Username.new('Samuel Clemens').suggest

The User class got a lot simpler now that it doesn't know all the business
rules about usernames. I left the validation in User because it's the thing
being persisted to the database, though if it wasn't for the Active Record
pattern I'd want to move that over as well.

Now Username is a simple
immutable value
object
that's easier to reason about and
test. It adheres nicely to the
SOLID
principles, and it's an uncommon nice example of inheritance working well in Ruby.

One caveat is that User objects Rails instantiantes will return Strings on
calls to User#username. We'll need to write a getter to instantiate and return a
Username object, though Rails before 3.2 included
composed_of
for this:

class User < ActiveRecord::Base
  def username
    Username.new(read_attribute(:username))
  end

  # or, pre Rails 3.2:
  composed_of :username, converter: :new
end

(And to the curious: no, I haven't contributed a patch back to Discourse. They
have a Contributor License Agreement
to backdoor their ostensible open source licensing.)

DevMynd is custom software development company with practice areas in digital strategy, human-centered design, UI/UX, and web application and custom mobile development.