Background
Poro: Plain Old Ruby Object
- An object which doesn’t depend or inherit from classes that are not part of ruby
- In the Rails world this means objects (classes) which are not derived from ActiveRecord (especially) or any of the other Rails framework classes
- Poros are easier to write and easier to test
- The discipline of desinging Poro’s almost automatically leads you to better isolation and separation of concerns.
- Poros are defined in standalone ruby files called, e.g. matcher.rb.
- Put them in any Rails directory under app. I suggest creating a new directory under app called lib/ but it’s not the only option
Service Objects
- The terminology here is currently muddled
- You will find ardent debates between terms like Service or Presenter or Facade classes
- In rails a very common use case is to make controllers simpler and better encapuslated
- Remember that Rails helpers are meant to be shorthand or macros for generating html in views.
Example: Matcher
- Imagine that you are writing a dating app where you want to match Members to each other
- Given a Member model or a Member object, it will compute the Member that best matches them.
- This can be a pretty complicated algorithm, taking as input one Member object as well as the complete list of available (unmatched) Members and producing as output another Member object, something like this:
@target_member = Member.find(i)
# .... complicared algortithm which ends with
@target_member.match = ...another member...
- Having this much code in the controller smells bad!
Instead try this
- Define a poro called Matcher which does not depend, itself or in the arguments it takes or the results it generates, on anything in Rails
class Matcher
def add_target_member id, species, genus
@target_species = species
@target_genus = genus
@target_id = id
end
def add_population members
# members is an array structured as follows:
# [[1, "Genus1", "SpeciesA"], [10, "Genus2", "SpeciesB"], ...]
@population = members
end
def find_best_match
# ... fancy algorithm
return best_match_id
end
end
Usage
- In the controller, or wherever you need it, you create an instance of the Matcher
- You retrieve the members from the database using ActiveRecord
- You convert them to non-ActiveRecord arrays
- You supply them to the matcher and then ask for the result
# Get the set of all members that don't have a match yet
unmatched_members =
Member.where(matched_member: nil).to_a.map(&:serializable_hash)
match = Matcher.new
# assume that the member object who we are trying to match up is "looking_member"
match.add_target_member_id( looking_member.id, looking_member.species, looking_member.genus)
match.add_population_members = unmatched_members
# Store the best match as in "looking_member"
looking.matched_member = Member.find(match.find_best_match)
Benefits
- You have pulled the logic for matching out of the controller into a separate class
- You can have several different matching algorithms and change them out cleanly by modifying the line match = Match.new to be for example match = FancyMatcher.new
- You can test class Match very simply, including generating randome combinations of data to see that it works correctly