Beginning OOP with Ruby

This post attempts to guide a beginner through object-oriented progamming in Ruby, showing them the thought process behind both designing and interacting with a library, and problem solving in general with Ruby.

To accomplish this, we will solve a simple geometry problem:

Breaking it down

The first thing we want to do to tackle this problem is to break apart the problem into a series of smaller, bite sized questions. “Good Ruby code” will read a lot like English - I think you’ll find the syntax we end up writing will almost look like English answers to these questions. You probably already know the answers to these (which is why I picked this as an example), but read them and pay attention to the language that I use.

1) What is perimeter?

Perimeter is the total distance around an object, in our case a 2D polygon with any number of sides.

2) What is a polygon?

A polygon is an object made up of lines.

3) What is a line?

A line is the shortest distance between two points in space. Since we’re dealing with finite lines, they have a start and an end.

4) What is a point?

A point is a location in space. For our purposes, this location is on a 2D plane, bound by coordinates.

In the next section, we’ll convert these concepts into Ruby code. Re-read this section once we’re done and see how what we said here is directly represented in the answer we come up with.

Concepts into code

From the previous section, you can see that we will need three objects to describe our problem. Using the class keyword, we can form a basic layout:

# A location in 2D space.
class Point
end

# A finite segment (or distance) between two points.
class Line
end

# A collection of lines that describe a 2D shape.
class Polygon
end

And from here, we can already describe how we’re going to solve our problem, by creating a #new instance of a Polygon, and calling #perimeter on it:

Polygon.new(___).perimeter

In Ruby, and in most OOP languages, a class describes a “template” for an object. An object is usually a collection of data, and a set of methods or functions that allow you to interact with that data or give it logic. We first describe the objects with a class, and create new instances of them with Thing.new.

In the next sections, we’ll work with one approach to solve this from the ground up, to figure out how to make a Polygon to get what we need, and explore some properties of Ruby along the way.

Objects

Point

Consider any kind of object around you. A pen, a car, your computer - they all have properties, or attributes. Ruby objects can be given attributes that let you describe and store data inside them. We do this with the @var syntax, like so, describing what defines a Point in 2D space:

# A location in 2D space.
class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
end

def defines a new method, or an instruction on how to do something with an object. initialize is a special method that is called whenever you call #new on an object to make a new instance of one.

We can now make a new Point like so:

point = Point.new(1.5, -3.0)
puts point.inspect #=> #<Point:0x2ae37f0 @x=1.5, @y=-3.0>

The attributes we’ve defined (@x, @y) can only be accessed inside of the class’s own methods. We’re going to need to use these attributes of a point in order to do our calculations. To do so, we can define methods that will return the values of our attributes:

# A location in 2D space.
class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  def x
    @x
  end

  def y
    @y
  end
end

Since this is such a common thing in Ruby, the language has shortcuts to quickly define such simple methods that reflect an objects attributes:

So, our Point can be described simply as:

# A location in 2D space.
class Point
  attr_reader :x

  attr_reader :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end

In our case, we will only need to ever read these attributes, so I just used attr_reader. attr_accessor would have also worked here.

Playing with our new readers:

point = Point.new(1.5, -3.0)
puts point.x #=> 1.5
puts point.y #=> -3.0

You can read more about attributes, readers, setters, and more here.

What more could we add to our Point class? With any two points, we can calculate the distance between them.

Ruby has a rich standard library, which includes some common mathematical functions. We need the pythagorean theorem here to calculate the distance between two points, and if we do some searching we can see Ruby already provides a method for us to calculate this instead of having to implement it ourselves:

https://ruby-doc.org/core-2.4.0/Math.html#method-c-hypot

Math.hypot(3, 4) #=> 5.0

Let’s implement it to our Point. (Note: I’ve omitted the rest of the class we already wrote to save space, ad nauseum)

# A location in 2D space.
class Point
  # Calculates the distance to another Point object
  def distance(other)
    x_difference = x - other.x
    y_difference = y - other.y

    Math.hypot x_difference, y_difference
  end
end

Here, the x in x_difference = x - other.x is actually calling this objects own attr_reader method internally that fetches its @x attribute. In another language, you may write something like self.x to refer to this value. You can do that here too in Ruby, but in most cases you can omit self., because Ruby understands what you mean.

Our new #distance method will take another Point object as an argument, read its @x and @y attributes, and do the maths to return our result implicitly by being the last line of the method.

Let’s try it out:

point_a = Point.new(1.5, -3.0)
point_b = Point.new(2, 1.0)

puts point_a.distance point_b #=> 4.031128874149275

We can now:

  1. Describe a Point.
  2. Measure the distance between any two Points.

Line

Let’s refer back to our definition of a Line.

# A finite segment (or distance) between two points.
class Line
end

From what we know, we can now give our Lines two attributes. We’ll call them start and stop.

I would use the word end here instead of stop, but this is a Ruby keyword - you cannot have Ruby keywords as variable names. You could be slightly more verbose if you wanted too, i.e., start_point and stop_point and this would also be acceptable.

# A finite segment (or distance) between two points.
class Line
  attr_reader :start

  attr_reader :stop

  def initialize(start, stop)
    @start = start
    @stop = stop
  end
end

Great. What other properties does a finite line have? Ah, right - length.

But - we’ve already implemented this! We already know the distance between any two Point objects.. let’s use it:

# A finite segment (or distance) between two points.
class Line
  def length
    # We use our attribute reader methods that we set before
    # to access this line's start and stop points
    start.distance stop
  end
end

I’ll use the same two points from before and test it:

point_a = Point.new(1.5, -3.0)
point_b = Point.new(2, 1.0)

line = Line.new(point_a, point_b)
puts line.length #=> 4.031128874149275

When talking about Ruby code, what we’ve done here is “wrapped” the #distance behavior of a Point object - or rather, we “abstracted” it - to give us the length of any kind of line we could describe.

Now we can:

  1. Describe a Point.
  2. Measure the distance between any two Points.
  3. Create a Line from any two Points.
  4. Measure the #length of that Line.

Take note of how we described the problem in simplest terms, and are slowly building up layers of objects and methods in order to craft our solution.

This leaves one last piece to our puzzle.

Polygon

We can now move on to describe a Polygon as a collection of Lines.

# A collection of lines that describe a 2D shape.
class Polygon
  attr_reader :segments

  def initialize(segments = [])
    @segments = segments
  end
end

Arguments for methods can be given a default value using the above syntax in def initialize. Here, I’m making segments default to an empty array.

We’re going to design Polygon to accept an Array of Line.

Our new Polygon can be instantiated like so..

Polygon.new [Line.new .., Line.new .., Line.new ..]

Thinking about Lines abstractly, we can use an enumerator called #reduce to help us calculate the perimeter. Ruby standard library to the rescue again!

You can go here for more examples and other iterators you can use. Learn them well! They’re the bread and butter of any rubyist!

Back to Polygon - let’s use #reduce to calculate the perimeter.

# A collection of lines that describe a 2D shape.
class Polygon
  def perimeter
    segments.map { |e| e.length }.reduce { |a, e| a + e }
  end
end

There is a shorter way to express this too. Since in both enums, we’re only calling a single method, you can pass a symbol that represents a method to call on all elements of that array. This could be written as segments.map(&:length).reduce(:+). Remember this handy shortcut for simple iterators - you can read more about it in the previously linked documentation for Enumerable.

Let’s put this all together and see how all of our objects and concepts we’ve discussed link up.

lines = [
  Line.new(Point.new(3.0, 1.0), Point.new(5.0, 1.25)),
  Line.new(Point.new(5.0, 1.25), Point.new(4.0, -1.25)),
  Line.new(Point.new(4.0, -1.25), Point.new(3.0, 1.0))
]

polygon = Polygon.new(lines)

puts polygon.perimeter #=> 7.170361291090916

We can now calculate the perimeter of a polygon with as many sides as we like!

In Review

So, we’ve come to a solution. Check out where we started this problem and observe the steps we took to solve it. You may have very well come to something different - and that’s okay if you see some other way to do this that I didn’t! That is a wonderful thing about Ruby and most other languages - there’s no single right answer, and there’s a lot of room to be creative. Programming is as much a science as it is an art.

Some takeaways:

Moving forward

There’s tons of cool things out there that you can do with Ruby, and maybe even problems in your own life that you can use to cook up Ruby solutions for.

Check out http://awesome-ruby.com/ for tons of community created packages - see what you can make with them!

Learn to read documentation like those on http://www.rubydoc.info/. Then you’ll be a real library ninja, and can answer a lot of your own questions! (Hopefully!)

When you’re stuck, there’s a lot of helpful ruby people out there. StackOverflow, IRC, etc. - or even me! I can be reached at wherever you’re viewing this post, or at my email, or on Discord - I’m z64#2639.

If you liked this article, you can see some others that my friends wrote.

Full code

# A location in 2D space.
class Point
  attr_reader :x

  attr_reader :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  # Calculates the distance to another Point object
  def distance(other)
    x_difference = x - other.x
    y_difference = y - other.y

    Math.hypot x_difference, y_difference
  end
end

# A finite segment (or distance) between two points.
class Line
  attr_reader :start

  attr_reader :stop

  def initialize(start, stop)
    @start = start
    @stop = stop
  end

  def length
    start.distance stop
  end
end

# A collection of lines that describe a 2D shape.
class Polygon
  attr_reader :segments

  def initialize(segments = [])
    @segments = segments
  end

  def perimeter
    segments.map(&:length).reduce(:+)
  end
end

lines = [
  Line.new(Point.new(3.0, 1.0), Point.new(5.0, 1.25)),
  Line.new(Point.new(5.0, 1.25), Point.new(4.0, -1.25)),
  Line.new(Point.new(4.0, -1.25), Point.new(3.0, 1.0))
]

polygon = Polygon.new(lines)

puts polygon.perimeter