Primitive obsession

What is primitive obsession?

Primitive Obsession is using primitive data types to represent domain ideas. For example, we use a String to represent a message, an Integer to represent an amount of money, or a Struct/Dictionary/Hash to represent a specific object.

We’re all guilty of this because in general we’re all kinda lazy. It’s certainly not a “rookie mistake”. Have you ever done the following?

array_of_things << thing unless array_of_things.include?(thing)

Congratulations, you’ve just recreated Ruby’s Set class. That’s not a good thing. An Array is not a collection that enforces uniqueness. A reason why that’s not a good thing is that checking to see if an array includes an object is O(n) where checking to see if a Set includes an object is O(1). Let me clarify: at best and at worst an array is O(n) because it’s always O(n) for search. At best a Set, backed by a Hash Archive , is O(1) which is quite good, O(n) at worst.

I think that Ruby developers are lucky but burdened by Rails, specifically when it comes to “good OO”. ActiveSupport is a rad library, and I fondly remember reading the Rails book in 2006 and being blown away by the ability to do 2.days.from_now. One of the things I appreciate from Arel is that when you make a query you get back an instance of ActiveRecord_Relation not an Array. You can ask this relation questions you couldn’t, and shouldn’t, ask an Array. This brings me to something I don’t often see in Ruby… collection classes.

A collection class is a class that represents a collection of objects. Why might you use a collection class over an Array or a Hash, or even a Set? Most likely because you want to ask that collection questions that Array, Hash, and even Set have no idea about. We reach for these custom classes because a primitive is too simple, too basic, and we end up asking questions in the wrong places. I had to interact with a remote API that did not give back good error messages. An example of how this works:

class WidgetCollection
  include Enumerable

  attr_accessor :errors

  def initialize(widgets)
    @widgets = Set.new(widgets)
  end

  def each
    return enum_for(:each) unless block_given?

    @widgets.each do |widget|
      yield widget
    end

    self
  end

  def invalid_widget_present?
    return false unless errors
    !!/some pattern that we hope works for a long time/.match(errors.to_s)
  end
end

class Widget
  def self.fetch
    api_results = Fetch.call
    WidgetCollection.new(api_results).tap do |widgets|
      widgets.errors = api_results.fault_code # a string, could be anything :(
    end
  end
end

# elsewhere...

widgets = Widget.fetch
log(a_message) if widgets.invalid_widget_present?

It’s unfortunate that the remote API gives us a fault code like that, but at least we can hide it in a class and keep our code a bit cleaner. Granted, there are more moving parts in our production code, but this way of dealing with the remote API feels like the right way. Much better than using an Array and checking the status further out. That’s certainly not “good” OO! What would that code have looked like?

# elsewhere...

widgets = Widget.fetch
log(a_message) if WidgetCollection::ERROR_MESSAGE.match(widgets.errors)

That requires you to know way too much, and it’s a sign that you’re relying too heavily on primitives. Respecting the boundaries is an important part of practicing object oriented programming. When you don’t create classes, especially for collections, you push responsibility out instead of pulling it in.

Alexander M over 5 years ago
This website uses short-lived cookies to improve usability.
Accept or learn more