Skip to content

Instantly share code, notes, and snippets.

@IanWhitney
Last active May 14, 2017 04:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IanWhitney/6d8d777659896ff9e20d to your computer and use it in GitHub Desktop.
Save IanWhitney/6d8d777659896ff9e20d to your computer and use it in GitHub Desktop.

For my blog post I'm taking a simple problem from exercism.io and refactoring my first solution using East Orientation. Below I've put the exercise, my initial, dumb, solution and the refactored one. I think I've followed the rules of EO...but, honestly, the refactored version seems very confusing to me. Maybe it's just because I'm not used to the approach. Or maybe it's because I went totally off the rails.Your thoughts?

Some things I wonder about.

  • For methods that return strings, I find myself inheriting from String so that I can follow the rule of returning self. That seems weird.
  • SoundFactory both a) sounds like a 90s dance band and b) isn't exactly a factory. It's really more of a value object that I'm using for lookup. But splitting it off in this way seemed to be my best choice.
  • I really dislike the whole fallback thing in RainSounds (again, these names are terrible), but I'd hit my mental limit for the day.

Oh, and yes I'm intentionally explicitly returning self even though I don't actually have to. It's for illustrative purposes.

Raindrops

Write a program that converts a number to a string, the contents of which depends on the number's prime factors.

  • If the number contains 3 as a prime factor, output 'Pling'.
  • If the number contains 5 as a prime factor, output 'Plang'.
  • If the number contains 7 as a prime factor, output 'Plong'.
  • If the number does not contain 3, 5, or 7 as a prime factor, just pass the number's digits straight through.

Examples

  • 28's prime-factorization is 2, 2, 7.
    • In raindrop-speak, this would be a simple "Plong".
  • 1755 prime-factorization is 3, 3, 3, 5, 13.
    • In raindrop-speak, this would be a "PlingPlang".
  • The prime factors of 34 are 2 and 17.
    • Raindrop-speak doesn't know what to make of that, so it just goes with the straightforward "34".

Source

A variation on a famous interview question intended to weed out potential candidates. view source

require 'prime'
class Raindrops
def self.convert(number)
factors = Prime.prime_division(number).map {|x| x[0]}
ret = ""
ret << "Pling" if factors.include?(3)
ret << "Plang" if factors.include?(5)
ret << "Plong" if factors.include?(7)
ret.empty? ? number.to_s : ret
end
end
require 'prime'
# 1) Always return self
# 2) Objects can query themselves
# 3) Factories are exempt
class Raindrops < String
def self.convert(number)
self.new(number)
end
def initialize(number)
super(RainSounds.new(Factors.new(number), number))
self
end
end
class Factors
include Enumerable
def initialize(number)
@members = Prime.prime_division(number).map {|x| x[0]}
self
end
def each &block
@members.each{|member| block.call(member)}
end
end
class RainSounds < String
def initialize(numbers, fallback)
sounds = numbers.inject("") { |ret, n| ret << SoundFactory.build(n)}
if sounds.empty?
super(fallback.to_s)
else
super(sounds)
end
self
end
end
class SoundFactory
def self.build(number)
{
3 => "Pling",
5 => "Plang",
7 => "Plong",
}[number].to_s
end
end
@saturnflyer
Copy link

Given that the goal of East-orientation is to tell objects what to do, this forces you to consider who needs to do what.

After some experimentation with this problem, I found that 1 object was responsible for determining the factors, and another responsible for outputting the sound.

I used BasicObject below just to limit the public API of the object as much as possible. This isn't really necessary. I kept the convert method on Raindrops but it's merely a pass-through for the initializer.
The data for Raindrops is marked as private.

require 'prime'
require 'delegate'

#1) Always return self
#2) Objects can query themselves
#3) Factories are exempt
class Raindrops < BasicObject
  # Factory. Rule 3
  def self.convert(number)
    self.new(number)
  end

  # internal Ruby method used in the 'new' factory. returning 'self' is implicit, but this is Rule 3
  def initialize(number)
    @number = number
    @drips = ::Prime.prime_division(number).map {|x| x.first}
  end

  # Private data
  attr_reader :number, :drips
  private :number, :drips

  # The only public method. Designed to interact with the template
  # Rule 1
  def drip(template)
    template.hit(number, drips)
    self
  end
end

If you initialize Raindrops and tell it to drip on a template, the object you'll get back is the Raindrops.

The object which makes the sounds I called "TinRoof".
Its job is to determine what to do with the drops. If the drops fit within it's own understanding of what makes a sound, then it makes that sound. Otherwise it will output the number itself.

This template uses DelegateClass which comes from the 'delegate' library and merely defines all the methods one would expect from an IO object (such as STDOUT) to forward to that object. Any additional things we need our template to do will be defined in the class (like hit and sounds).

class TinRoof < DelegateClass(IO)
  # Rule 1
  # And Rule 2
  def hit(number, drops)
    if (drops & sounds.keys).empty?
      puts number
    else
      puts drops.map{|drop| sounds.fetch(drop, '') }.join
    end
    self
  end

  private

  # Private data
  def sounds
    {
      3 => 'Pling',
      5 => 'Plang',
      7 => 'Plong'
    }
  end
end

To make this work, the main object (the running script) just needs to initialize the Raindrops, the template, and tell the raindrops to drip on the template.

tin_roof = TinRoof.new(STDOUT)

Raindrops.convert(28).drip(tin_roof)
Raindrops.convert(1755).drip(tin_roof)
Raindrops.convert(34).drip(tin_roof)

The result of each of those commands (convert and drip) is a Raindrops object. The main script merely tells them what to do and the tin_roof does what it does.

If you change the template to something with different rules, you'd merely need to have Raindrops tell that template to do the same thing:

class WoodenPorch < TinRoof
  def sounds
    {
      2 => 'Dink',
      13 => 'Dank',
      71 => 'Donk'
    }
  end
end

wooden_porch = WoodenPorch.new(STDOUT)

Raindrops.convert(17156).drip(wooden_porch)

The rules for what makes a sound stay with the object making the sound (the template). The rules for determining the drops stay with the object making that decision (the raindrops). If you change the way you calculate the drops, the templates don't care; they merely apply their sounds rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment