Strategically Thinking Through Coding Exercises

Recognizing the untold stories behind the clean code we admire

Featured on Hashnode

Coding can be intimidating. If you do it on your own, it can be overwhelming! Often budding software developers see code produced by others and immediately feel inadequate. I'll never be able to write code like that! is a typical lamentation. You've likely experienced moments like this yourself. Next time this happens, keep in mind that what you see doesn't tell the story of how the author of that code got there. It only shares the final product. In this article, I'm going to share the full story of how I approached, worked through, and solved the SpaceAge exercise in the Ruby track on Exercism.org.

Before beginning, I'd like to point something out. I am a full-time professional software engineer. In the context of Exercism's Ruby track, The SpaceAge problem I work on here is labeled Easy. I didn't choose to work through this exercise to produce this article alone. I decided to work through it because I want to improve writing code with Object-Oriented Programming design principles. By deliberately decreasing the cognitive challenge of technical problem solving, I increased my bandwidth for critically thinking about software design decisions. Just because we reach certain levels of experience and mastery doesn't mean that immense value can't still be found in the "easy" exercises.

With that idea clarified, let's get started.

Exercise Instructions

Given an age in seconds, calculate how old someone would be on:

Mercury: orbital period 0.2408467 Earth years
Venus: orbital period 0.61519726 Earth years
Earth: orbital period 1.0 Earth years, 365.25 Earth days, or 31557600 seconds
Mars: orbital period 1.8808158 Earth years
Jupiter: orbital period 11.862615 Earth years
Saturn: orbital period 29.447498 Earth years
Uranus: orbital period 84.016846 Earth years
Neptune: orbital period 164.79132 Earth years

So if you were told someone was 1,000,000,000 seconds old, you should be able to say that they're 31.69 Earth-years old.

Below is a sample of the tests all solutions to the SpaceAge problem will be checked against (note: in the actual test file on Exercism, there is a test for each planet):

class SpaceAgeTest < Minitest::Test
  # assert_in_delta will pass if the difference
  # between the values being compared is less
  # than the allowed delta
  DELTA = 0.01

  def test_age_on_earth
    age = SpaceAge.new(1_000_000_000)
    assert_in_delta 31.69, age.on_earth, DELTA
  end

  def test_age_on_mercury
    age = SpaceAge.new(2_134_835_688)
    assert_in_delta 280.88, age.on_mercury, DELTA
  end

  def test_age_on_venus
    age = SpaceAge.new(189_839_836)
    assert_in_delta 9.78, age.on_venus, DELTA
  end
...
end

Source: exercism.org/tracks/ruby/exercises/space-age

Step 1: Clarify the problem and expected behavior of a valid solution

I've learned over time that one of the most valuable steps in technical problem solving is to take the time before writing a single line of code to make sure I understand the problem and what is expected of my solution. After reading the instructions, my understanding is:

I will be given a value representing age in seconds. I must write a program that can take in that value and, given a specific planet and orbital period, calculate the equivalent age of a being on that planet on earth using earth years as the unit of measurement. Return that value represented as a Float with the fractional part (or "mantissa") to the hundredths-place.

Step 2: Write down the steps I think I will need to take to solve the problem

Confident that I understand what I'm dealing with and what a good solution should look like and do, I still refrain from writing any actual code. Instead, I use human language to write down the steps I think I'll need to take to produce a solution. The goal here is not clairvoyance. My focus is on mapping out my thoughts. For the SpaceAge problem, I anticipate needing to do the following:

# Create a class called SpaceAge
# Allow the SpaceAge class to take in a single argument of type Integer
# Divide the argument by the number of seconds in 1 earth year, and assign the value to a variable
# Create a way to multiply the orbital period of a given planet with the value assigned to the variable to get the product of the two in earth years
# Round the product to the nearest hundredths-place and return

Step 3: Translate the human-readable steps into working code

As with a generic instructional manual, I can now start assembling a solution programmatically by translating each step of the instruction manual I created above one at a time.

Create a class called SpaceAge

# Create a class called SpaceAge
class SpaceAge

end
# Allow the SpaceAge class to take in a single argument of type Integer
class SpaceAge
   attr_reader :age_in_seconds

   def initialize(age_in_seconds)
     @age_in_seconds = age_in_seconds
   end
end
# Divide the argument by the number of seconds in 1 earth year, and assign the value to a variable
class SpaceAge
   attr_reader :age_in_seconds

   def initialize(age_in_seconds)
     @age_in_seconds = age_in_seconds
   end

   def seconds_in_earth_year
     age_in_seconds / 60 / 60 / 24 / 365.25
   end
end
# Create a way to multiply the orbital period of a given planet with the value assigned to the variable to get the product of the two in earth years
class SpaceAge
   attr_reader :age_in_seconds

   def initialize(age_in_seconds)
     @age_in_seconds = age_in_seconds
   end

   def seconds_in_earth_year
     age_in_seconds / 60 / 60 / 24 / 365.25
   end

  def on_earth
    earth_years = seconds_in_earth_year / 1.0
  end

  def on_mercury
    earth_years = seconds_in_earth_year / 0.2408467
  end

  def on_venus
    earth_years = seconds_in_earth_year / 0.61519726
  end

  def on_mars
    earth_years = seconds_in_earth_year / 1.8808158
  end

  def on_jupiter
    earth_years = seconds_in_earth_year / 11.862615
  end

  def on_saturn
    earth_years = seconds_in_earth_year / 29.447498
  end

  def on_uranus
    earth_years = seconds_in_earth_year / 84.016846
  end

  def on_neptune
    earth_years = seconds_in_earth_year / 164.79132
  end
end
# Round the product to the nearest hundredths-place and return
class SpaceAge
  attr_reader :age_in_seconds

  def initialize(age_in_seconds)
    @age_in_seconds = age_in_seconds
  end

  def seconds_in_earth_year
    age_in_seconds / 60 / 60 / 24 / 365.25
  end

  def on_earth
    earth_years = seconds_in_earth_year / 1.0
    earth_years.round(2)
  end

  def on_mercury
    earth_years = seconds_in_earth_year / 0.2408467
    earth_years.round(2)
  end

  def on_venus
    earth_years = seconds_in_earth_year / 0.61519726
    earth_years.round(2)
  end

  def on_mars
    earth_years = seconds_in_earth_year / 1.8808158
    earth_years.round(2)
  end

  def on_jupiter
    earth_years = seconds_in_earth_year / 11.862615
    earth_years.round(2)
  end

  def on_saturn
    earth_years = seconds_in_earth_year / 29.447498
    earth_years.round(2)
  end

  def on_uranus
    earth_years = seconds_in_earth_year / 84.016846
    earth_years.round(2)
  end

  def on_neptune
    earth_years = seconds_in_earth_year / 164.79132
    earth_years.round(2)
  end
end

Running this code against the provided tests shows that this solution works! If our main goal for this exercise is to solve the problem, I could be done. But as I pointed out at the beginning of this article, I chose an easy exercise because I want to "get better at writing code with Object-Oriented Programming design principles in mind." At this point, it's time to do some refactoring!

Step 4: Refactor working code into clean code

Looking over my working solution, I notice that I repeat myself a lot. Each of my planet methods shares a common naming pattern. Each of them divides its unique orbital period value by the seconds_in_earth_year instance method, assigning the quotient to the same earth_years variable. And each of them calls the .round method on the earth_years variable and passes in the same integer as an argument.

It's pretty evident that my working solution violates the DRY principle of software. DRY stands for don't repeat yourself and aims to reduce the repetition of patterns and code duplication in favor of abstractions and avoid redundancy.

The first repeated pattern I notice is using the seconds_in_earth_year instance method for division in each on_* planetary method. Looking back at the seconds_in_earth_year method definition, I see that the value being returned from converting the age_in_seconds into equivalent earth_years will never change for a given instance of the SpaceAge class. The only dynamic part of this method is the age_in_seconds value passed to the SpaceAge class through the .new method when an instance of this class is created.

This sounds like an excellent opportunity to move the seconds_in_earth_year method into the initialize method so that the value assigned to the earth_years variable is calculated whenever I create a new SpaceAge instance. In fact, since the only place I reference the @age_in_seconds instance variable is when I call the reader method of the same name in the seconds_in_earth_year method, by moving that method into the initialize method, I can remove the explicit assignment of that parameter to an instance variable entirely in favor of passing it directly to the seconds_in_earth method itself.

class SpaceAge
  attr_reader :earth_years

  def initialize(age_in_seconds)
    @earth_years = seconds_in_earth_year(age_in_seconds)
  end

  def seconds_in_earth_year(seconds)
    seconds /60 / 60 / 24 / 365.25
  end

  def on_earth
    (earth_years / 1.0).round(2)
  end

  def on_mercury
    (earth_years / 0.2408467).round(2)
  end

  def on_venus
    (earth_years / 0.61519726).round(2)
  end

  def on_mars
    (earth_years / 1.8808158).round(2)
  end

  def on_jupiter
    (earth_years / 11.862615).round(2)
  end

  def on_saturn
    (earth_years / 29.447498).round(2)
  end

  def on_uranus
    (earth_years / 84.016846).round(2)
  end

  def on_neptune
    (earth_years / 164.79132).round(2)
  end
end

That's looking cleaner. But it's still not quite DRY.

Though not identical, the way I've named the def on_* planetary method definitions is a repeating pattern. From a serpentining Google search, I discovered a strategy I was not familiar with which is appropriate for my use case here: Ruby's define_method method.

From the Ruby docs, I learned that define_method can be used to define an instance method on the receiver, which in my case is the SpaceAge class.

Knowing this new tool is available to me, I had the idea of creating a hash that maps each planet name as a key that points to the planet's orbital value.

Perhaps I could then iterate through this hash and, by using string interpolation, dynamically create the on_* instance methods for each planet without unnecessarily repeating any patterns.

Check it out!

class SpaceAge
  attr_reader :earth_years

  def initialize(age_in_seconds)
    @earth_years = seconds_in_earth_year(age_in_seconds)
  end

  def seconds_in_earth_year(seconds)
    seconds / 60 / 60 / 24 / 365.25
  end

  PLANETARY_HASH = {
    "earth": 1.0,
    "mercury": 0.2408467,
    "venus": 0.61519726,
    "mars": 1.8808158,
    "jupiter": 11.862615,
    "saturn": 29.447498,
    "uranus": 84.016846,
    "neptune": 164.79132
  }

  PLANETARY_HASH.each do |planet, orbital_period|
    define_method("on_#{planet}") do
      (earth_years / orbital_period).round(2)
    end
  end
end

To be honest, I wasn't certain this would work (this was my first time experimenting with the define_method method). Imagine my pleasant surprise when all of the exercise tests passed!

Conclusion

Hopefully, you now see that the code we find ourselves impressed by has such an effect on us as a result of an inelegant story of iteration, experimentation, and discovery. As with every great story, however, there are archetypes to be followed. My archetype includes:

  1. Clarify the problem and expected behavior of a valid solution
  2. Write down the steps I think I will need to take to solve the problem
  3. Translate the human-readable steps into working code, and
  4. Refactor working code into clean code

It is one of many. Perhaps you have your own! My archetype led me to a valid solution I feel satisfied with. But it may not be the best solution possible. That's something I love about coding. As I continue learning, studying, and deliberately practicing various techniques and principles of software design, what once seemed amazing can later seem basic. What once seemed impossible can later become common practice. Remember that the next time someone else's code shakes your own confidence. Rather than viewing it as a reflection of your relative inadequacy, acknowledge it for what it is, the potential that awaits you in the not-so-distant future.