DEV Community

David J. Davis
David J. Davis

Posted on

Plain Ole Ruby Objects

image

TRIGGER WARNING && TL;DR
I think that service objects are sometimes over engineered in Rails, when in fact a plain old ruby object would potentially be better. If you are using a gem to manage services, presenters, library modules, etc, then maybe you should reconsider and weight what benefits it provides.

During the Digital Rails Conf this year I watched a video called the The Missing guide to Service Objects;
To me this was an opinionated guide to service objects, but he did discuss multiple ways of handling those service objects and some ways to accomplish it for newcomers to Rails. While it was a great talk I think that my thought process would be refactoring that talk into a "Lets talk about POROs".

To me the best rails setups do the following:

  • Rails is best used when all things in the Models folder are strictly for setting up and modeling your data, meaning that it directly deals with database data.
  • The controllers are simple and reference business logic or models directly.
  • Business Logic is stored in the appropriate name format and is away from the controller logic.

In order to do this you may think, "I need need: #{insert_one}". Service objects, presenters, components, monkey-patches, flying-monkeys, pokémon, modular objects, and widgets.

You might, but most likely they are going to be plain old ruby objects anyway, so call them whatever the hell you want, just focus on a design pattern that works for you and your team.

Why PORO?

To establish why, examine the following code. A simple class that takes a parameter and runs a perform method. Putting it simply this is a service object, but it is also a plain old ruby object.

class Num::SquaredService
  def initialize(num)
    @num = num
  end 

  def call
    @num * @num  
  end 
  alias_method :peform, :call
end 
Enter fullscreen mode Exit fullscreen mode

Looking at this file inspector. This assumes that you have access to multiple classes, it determines a type based on a matcher method inside each class. This file inspector can be a factory, it could be in the lib folder, or references as any other name.

class FileInspector::Type
  TYPES = [
    FileInspector::Image,
    FileInspector::Audio,
    FileInspector::Video,
    FileInspector::Text,
    FileInspector::Pdf, 
    FileInspector::Other
  ].freeze

  # Sets up mime type instance
  def initialize(file)
    @mime = `file --b --mime-type #{file}`.strip
  end

  # Determines the Type to be used in another object.
  def inspect
   TYPES.find { |type| type.matches?(@mime) }.name.demodulize
  end
end
Enter fullscreen mode Exit fullscreen mode

With a few keystrokes this PORO can look a lot like a service object, lets do that.

class FileInspector::TypeService
  TYPES = [
    FileInspector::Image,
    FileInspector::Audio,
    FileInspector::Video,
    FileInspector::Text,
    FileInspector::Pdf, 
    FileInspector::Other
  ].freeze

  # Sets up mime type instance
  def initialize(file)
    @mime = `file --b --mime-type #{file}`.strip
  end

  # Determines the Type to be used in another object.
  def call
   TYPES.find { |type| type.matches?(@mime) }.name.demodulize
  end
  alias_method :peform, :call
end
Enter fullscreen mode Exit fullscreen mode

With that, we now have a service object. With a few more changes and some HTML we could turn this into a presenter. My point is that we should be thinking more about the functionality of the object; not the name, or the location of the object in a directory structure.

The reason I lean towards keeping them as POROs is to keep everything simple and keep all classes focusing on a single responsibility. Breaking down big problems to small problems that are easily testable.

Top comments (0)