# Objective

To learn how to apply basic object oriented design principles

# Problem

Write a program that:

1. Loads a set of employee records from a flat file
2. Sends a greetings email to all employees whose birthday is today

The flat file is a sequence of records, separated by newlines; these are the first few lines:

``````last_name, first_name, date_of_birth, email
Doe, John, 1982/10/08, john.doe@foobar.com
Ann, Mary, 1975/09/11, mary.ann@foobar.com
``````

The greetings email contains the following text:

``````Subject: Happy birthday!

Happy birthday, dear <first_name>!

where first_name is the place holder for first name.
``````

# Object Oriented Design Basic Principles

We will apply the following three basic principles:

1. Separate things that change from things that stays the same. Encapsulate what varies behind a well-defined interface.
2. Program to interfaces, not implementations. This exploits polymorphism.
3. Depend on abstractions. Do not depend on concrete classes.

# High Level Steps

2. Check if date of birth is today
3. Send greetings email if today is the person's birthday

Let's read a CSV file that contains data.

``````file_name = "data.txt"

text = open(file_name)

``````

Refine the step 1.

``````1. Read data.txt CSV file
2. Check if date of birth is today
3. Send greetings email if today is birthday
``````

Here is our simple program that we can start playing with:

``````require 'csv'

file_name = "#{Dir.pwd}/data.txt"

data.each do |x|
p x
end
``````

Refine the step 2

``````1. Read data.txt CSV file
2. Check if date of birth is today
Retrieve the third column
Remove the spaces at the ends
Check if month and date is the same as today's month and date
If yes, return the person's first name
3. Send greetings email if today is birthday
``````

Refine the step 3

``````1. Read data.txt CSV file
2. Check if date of birth is today
Retrieve the third column
Remove the spaces at the ends
Check if month and date is the same as today's month and date
If yes, return the person's first name
3. Send greetings email
Use Gmail, Sendgrid, Pony etc.
``````

# Before

The program written that does not apply the 3 design principles looks like this:

``````require 'csv'

file_name = "#{Dir.pwd}/data.txt"

data.each do |x|
birth_date = x[2].strip!
month = birth_date[5..6]
day = birth_date[8..9]

if (month.to_i == Date.today.month) and (day.to_i == Date.today.day)
p x[1].strip

email = <<EMAIL_TEXT
Subject: Happy birthday!

Happy birthday, dear #{x[1].strip}!
EMAIL_TEXT

p email
end
end
``````

# Analysis

This program has the following responsibilities:

1. Parsing CSV file
2. Checking if today is the birthday of a person
3. Sending email

Let's make a list of things that can change.

• Input data source
• How greetings is sent

Things that stays the same are:

• Logic to find if someone's birthday is today.

# Step 1

The PersonFileStore class will have records method that will return a list of Person objects. The code that applies what we did in analysis now looks like this:

``````require 'csv'

# This is a domain object
# This object has no dependency on other objects. It is agnostic to storage mechanism
class Person

def initialize(first_name, last_name, date_of_birth, email)
@first_name = first_name
@last_name = last_name
@date_of_birth = date_of_birth
@email = email
end

def birth_day
@date_of_birth[8..9]
end

def birth_month
@date_of_birth[5..6]
end
end

# This class knows how to parse the CSV file to create Person objects
# The direction of dependency is from PersonFileStore to the domain object
class PersonFileStore
def initialize(file)
@file = file
end

def records
result = []
data.each do |x|
person = Person.new(x[1], x[0], x[2].strip!, x[3])

result << person
end
result
end
end

# This section of the code is not yet cleaned up.
pfs = PersonFileStore.new("#{Dir.pwd}/data.txt")
records = pfs.records

records.each do |person|
month = person.birth_month
day = person.birth_day

if (month.to_i == Date.today.month) and (day.to_i == Date.today.day)

email = <<EMAIL_TEXT
Subject: Happy birthday!

Happy birthday, dear #{person.first_name}!
EMAIL_TEXT

p email
end

end
``````

# Step 2

Let's extract the logic to find if anyone has a birthday today.

``````# Person and PersonFileStore classes is same as before.

# This class encapsulates the logic to find out if the birthday is today or not.
# It has no dependency on other objects
class BirthDay
def initialize(month, day)
@month = month
@day = day
end

def today?
(@month.to_i == Date.today.month) and (@day.to_i == Date.today.day)
end
end

pfs = PersonFileStore.new("#{Dir.pwd}/data.txt")
records = pfs.records

records.each do |person|
month = person.birth_month
day = person.birth_day

birth_day = BirthDay.new(month, day)

if birth_day.today?

email = <<EMAIL_TEXT
Subject: Happy birthday!

Happy Birthday, Dear #{person.first_name}!
EMAIL_TEXT

p email
end

end
``````

# Step 3

Let's extract sending greeting.

``````# Person, PersonFileStore and Birthday classes is same as before.

# Sending email to the console output is encapsulated within the send interface
class GreetingConsole
def initialize(message, email)
@message = message
@email = email
end

def send
p "Sending email to : #{email}"
p @message
end
end

# The following code is the client code
pfs = PersonFileStore.new("#{Dir.pwd}/data.txt")
records = pfs.records

records.each do |person|
month = person.birth_month
day = person.birth_day

birth_day = BirthDay.new(month, day)

if birth_day.today?
message = <<EMAIL_TEXT
Subject: Happy Birthday!

Happy Birthday, Dear #{person.first_name}!
EMAIL_TEXT
# Client is tied to a specific implementation of sending an email message
# This needs to change to GreetingEmail.new(message), greeting.send to send email greeting
greeting = GreetingConsole.new(message)
greeting.send
end

end
``````

# Step 4

Let's add an in-memory data source and make it work.

``````class PersonMemoryStore

def records
result = []

person = Person.new('Bugs', 'Bunny', '1982/10/06', 'bbunny@rubyplus.com')
result << person

person = Person.new('Daffy', 'Duck', '1975/09/11', 'dduck@rubyplus.com')
result << person

result
end

end

# The following code is the client code
pfs = PersonMemoryStore.new
records = pfs.records

records.each do |person|
month = person.birth_month
day = person.birth_day

birth_day = BirthDay.new(month, day)

if birth_day.today?
message = <<EMAIL_TEXT
Subject: Happy Birthday!

Happy Birthday, Dear #{person.first_name}!
EMAIL_TEXT
# Client is tied to a specific implementation of sending an email message
# This needs to change to GreetingEmail.new(message), greeting.send to send email greeting
greeting = GreetingConsole.new(message)
greeting.send
end

end
``````

Notice that the PersonMemoryStore has the same interface as the PersonFileStore class. In a real project, we could use SQLite in-memory database.

# Step 5

Let's add a different way to send email by using Pony gem.

``````require 'pony'

# Sending a real email using Pony gem
class GreetingPony
def initialize(message, email)
@message = message
@email = email
end

def send
Pony.mail(:to => @email, :from => 'admin@rubyplus.com', :subject => 'Happy Birthday!', :body => @message)
end
end
``````

# After

The channel folder has `greeting_console.rb` and `greeting_pony.rb` classes. The GreetingConsole class looks like this:

``````# Sending email to the console output is encapsulated within the send interface
class GreetingConsole
def initialize(message, email)
@message = message
@email = email
end

def send
p "Sending email to : #{@email}"
p "Subject : Happy Birthday!"
p @message
end
end
``````

Here is the GreetingPony class:

``````require 'pony'

# Sending a real email using Pony gem
class GreetingPony
def initialize(message, email)
@message = message
@email = email
end

def send
Pony.mail(:to => @email, :from => 'admin@rubyplus.com', :subject => 'Happy Birthday!', :body => @message)
end
end
``````

The domain folder contains the BirthDay and Person classes. Here is the BirthDay class:

``````require 'date'

# This class encapsulates the logic to find out if the birthday is today or not.
# It has no dependency on other objects
class BirthDay
def initialize(month, day)
@month = month
@day = day
end

def today?
(@month.to_i == Date.today.month) and (@day.to_i == Date.today.day)
end
end
``````

Here is the Person class:

``````# This is a domain object
# This object has no dependency on other objects. It is agnostic to storage mechanism
class Person

def initialize(first_name, last_name, date_of_birth, email)
@first_name = first_name
@last_name = last_name
@date_of_birth = date_of_birth
@email = email
end

def birth_day
@date_of_birth[8..9]
end

def birth_month
@date_of_birth[5..6]
end
end
``````

The source folder contains `person_file_store.rb` and `person_memory_store.rb`.

``````require 'csv'
require_relative '../domain/person'

# This class knows how to parse the CSV file to create Person objects
# The direction of dependency is from PersonFileStore to the domain object
class PersonFileStore
def initialize(file)
@file = file
end

def records
result = []
data.each do |x|
person = Person.new(x[1], x[0], x[2].strip!, x[3])

result << person
end
result
end
end
``````

You can see that we need the require_relative statement, since it has dependency on the Person domain object. The PersonMemoryStore class looks like this:

``````require_relative '../domain/person'

#  This class provides in-memory implementation of the data source interface
#  Useful in writing tests
class PersonMemoryStore

def records
result = []

person = Person.new('Bugs', 'Bunny', '1982/10/08', 'bbunny@rubyplus.com')
result << person

person = Person.new('Daffy', 'Duck', '1975/09/11', 'dduck@rubyplus.com')
result << person

result
end

end
``````

# Visual Representation

You can download the final refactored code that has a better design here: Ruby Greeter

# Summary

We separated the input data source that can change into it's own source folder. We encapsulated it behind a well-defined interface. We did the same for different ways to send birthday greetings by moving all the relevant classes to the channel folder. We depend on the send method for greeting delivery and records method for data source, so we program to the interface. The glue code in main.rb that uses the classes in the channel, domain and source folders depends on concrete classes. You can use dependency injection and vary the input source and the channel to make them depend on abstractions instead of concrete classes.

# Reference

The birthday greetings kata

# Related Articles

Create your own user feedback survey