Breaking Down Object-Oriented Ruby

Understanding the relationships between models in your app

Ariel Jakubowski
Jun 18, 2019 · 6 min read
Image for post
Image for post
Zacharie Grossen on Wikimedia Commons

Breaking code into more manageable pieces is one of the most important parts of coding. It also can be one of the most challenging. Object-oriented programming (OOP) in Ruby is no exception. My general process for breaking down OO Ruby is as follows:

Step 1

Before you even touch your computer, you need to understand the relationships between your objects. For the purposes of this guide, we will be looking at a group of three objects. Let’s take startups as an example. A startup can have many venture capitalists, and a venture capitalist can back many startups. This is called a many-to-many relationship.

Image for post
Image for post
Many-to-many relationship

These startups and venture capitalists are connected by funding rounds. A startup and a venture capitalist can each have many funding rounds, but when a venture capitalist backs a startup, it happens through an individual funding round. The relationship between these three classes is called a has-many-through relationship.

Image for post
Image for post
Has-many-through relationship

Because the many-to-many relationship between a startup and a venture capitalist exists through a funding round, the funding round is the joiner.

Step 2

Now that we have identified the joiner, we can start building our classes. When building classes, the joiner is always built last. We will start by declaring the class for our startup as follows:

class Startup
end

Next, we’ll add some basic information and the initialize method. A startup is initialized with a name, a founder, and a domain so our class will contain the following:

class Startup
@@all = []
attr_accessor :name
attr_reader :founder, :domain
def initialize(name, founder, domain)
@name = name
@founder = founder
@domain = domain
@@all << self
end
end

Note that the name of a startup can change, but its domain and founder cannot. The name will be written with an attr_accessor, and the founder and domain will be written with an attr_reader. Also, the class is declared with an empty @@all array, and the initialize method adds each class instance to this array. This allows us to keep track of all of the instances we create.

Now that we have our basic information for the startup class, let’s build the basic information for the venture-capitalist class. A venture-capitalist instance is initialized with a name and a total_worth, both of which can change so our basic venture-capitalist class will look as follows:

class VentureCapitalist 
@@all = []
attr_accessor :name, :total_worth

def initialize(name, total_worth)
@name = name
@total_worth = total_worth
@@all << self
end
end

Step 3

Now that we’ve built the basics for our other classes, let’s do the same for the joiner.

The joiner class is initialized with a type and an investment, both of which can change. The joiner class must know about the classes it is joining so it will be initialized with instance variables for each of the other classes.

When a venture capitalist backs a startup, they can invest as much as they want, but they cannot take money from the startup. To make sure that an investment is initialized correctly, we will include an if statement that will automatically ensure that an investment will not be less than zero.

The basics for the joiner class will be as follows:

class FundingRound 
@@all = []
attr_accessor :type, :investment
attr_reader :startup, :venture_capitalist
def initialize(startup, venture_capitalist, type, investment)
@startup = startup
@venture_capitalist = venture_capitalist
@type = type
if investment < 0
@investment = 0
else
@investment = investment.to_f
end
@@all << self
end
end

Step 4

Now that we have the basic information for our classes, we can start making methods to use this information. I recommend writing all of the methods that involve just one class first, then writing methods that involve multiple classes. The first method we will write is as follows:

def self.all        
@@all
end

This is an important method that will allow us to access all instances of a class, which will be useful in building other methods. We will give each of our classes this method.

Let’s say we want to find a startup with its founder’s name. To do this, we will iterate through the @@all array until we find the startup with the founder that matches our founder name. That method will look as follows:

def self.find_by_founder(founder_name)        
self.all.find do |startup|
startup.founder == founder_name
end
end

Now let’s say we want to get all of the domains of our startups. To do this, we will iterate through the @@all array and use map to make an array of startup domains.

def self.domains        
self.all.map do |startup|
startup.domain
end
end

Step 5

Now we’ll learn how to handle multiple classes.

Let’s say we want to know all of the funds that a startup has received. The startup class is not tracking that information. Instead, it’s the funding round class that is tracking this information, so to get the startup’s total funds, we need to look at its funding rounds.

Before we can figure out the funds that the startup has received, we need to know all of the funding rounds belonging to the startup. This is an important piece of information, and it can help us get even more information about the startup’s funding rounds and about the venture capitalists who have invested in it.

To get this, we will iterate through all of the funding rounds and select the instances where the startup matches the name of the startup we are looking for. Next, we will iterate through the resultant array to determine the investment amount for each funding round a startup has. We will then sum all of these investments that have been made in our startup to get the startup’s total funds. Our helper method and total_funds method are as follows:

def funding_rounds 
FundingRound.all.select do |round|
round.startup == self
end
def total_funds
investments = funding_rounds.map do |round|
round.investment
end
investments.sum
end

Now let’s say we want to know all of the venture capitalists who have backed the startup. The startup does not track venture capitalists who have backed it, but because each funding round tracks both the startup and the venture capitalist, we can use funding rounds to get this information. For this, we will create a helper function again, but this time when we iterate through all of the funding rounds for the startup, we will map the venture capitalists. This method is as follows:

def investors        
funding_rounds.map do |round|
round.venture_capitalist
end.uniq
end

Now let’s say our venture capitalists want to know about the startups they have backed. The venture-capitalist class does not track information about its startups, but because the funding-round class tracks both the startups and the venture capitalists, we can use funding rounds to get this information.

For this, we will start by building a helper method similar to the funding_rounds helper method that we wrote for the startup. This time, instead of finding funding rounds that match a startup, we will get all of the funding rounds that match a venture capitalist. We will then use this helper method to map the startups that a venture capitalist backed with their funding rounds. These methods are written as follows:

def funding_rounds        
FundingRound.all.select do |round|
round.venture_capitalist == self
end
end
def portfolio
self.funding_rounds.map do |round|
round.startup
end.uniq
end

Step 6

Now that we have all three of our classes built out, we can get information about each class and we can get information about related classes. The last thing we have to do is test to make sure we have created everything correctly.

To do this, we start by creating seed data, a bunch of examples of class instances we can experiment with. To test our code, we can simply go to the terminal and run our file.

We can then call our seed data and call our methods on our classes and on the seed data we made. If testing the data and methods produces the results we expect, we know that we have written our code correctly.

Better Programming

Advice for programmers.

Sign up for The Best of Better Programming

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Ariel Jakubowski

Written by

Full Stack Software Engineer/ Web Developer and former Mechanical Engineer https://www.linkedin.com/in/ariel-jakubowski/

Better Programming

Advice for programmers.

Ariel Jakubowski

Written by

Full Stack Software Engineer/ Web Developer and former Mechanical Engineer https://www.linkedin.com/in/ariel-jakubowski/

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store