Skip to main content

Lab One - Onboarding with Ruby

· 23 min read

Lab Goals and Culture

  • After finishing your courses in Programming Principles, Programming 1, Programming with C++ and Data structures and Algorithm, You applied for an internship at a software startup company called NSBT Futures and you got hired as an engineering intern after successfully passing the interview process.
  • NSBT Futures is small company with a small team that working together from all the software development cycle and may have person A working on System Analysis => System Design => System Implementation => System Testing => System Deployment => System Maintainance.
  • The Engineering Manager called Promise, realized you took NBIT208 or DIFT206: System Analysis and System Design course and this your unique background convinced him to add you to not only System Analysis and Design Team but also the Core Product Software Engineering Team and DevOps and Site Reliabity Engineer Team.
  • The The Core Product Software Engineering Team mostly uses a language and framework called ruby and ruby on rails which you are not familiar with and you are force to learn it on the job.
  • which is not a big deal because you know programming already
  • Student will learn to appreciate system analysis or requirement engineering and system design in system software development with ruby and it's object oriented programming paradiagm and how to collobrate with other team members like product owner or product manager or software architect or ux designer etc

Setup

1. Objects, Classes, and Instances

Starting this Lab I assume you have rvm installed and have ruby installed and pry installed which is optional. if you are using mac or linux you must have done the following on your terminal.

$ rvm install 3.2.2
$ rvm use 3.2.2
$ gem install pry

Learning Goals

  • Describe the difference between a class and an instance of that class
  • Define a class
  • Store state in instance variables defined in initialize
  • Provide access to state using attr_readers and attr_accessors
  • Use methods to provide behaviors to instances of a class
  • Create a new instance of a class and call methods on that instance

Vocabulary

  • Class
  • Object
  • Instance
  • State
  • Attribute
  • Instance Variable
  • Behavior
  • Method

Warm Up

  • In your notebook brainstorm a type of object and specific instances of that object. Then brainstorm 3 different attributes for those objects and 3 different behaviors of those objects.

For example:

  • Type of object: Student
  • Specific instances: Level 100 Student, Level 200 Student, Level 300 Student
  • Attributes: Level, Age, Name, Email, ID, GPA
  • Behaviors: register courses, change courses, check gpa

Classes in Ruby

Overview

In programming, a Class is something that models:

  • State
  • Behavior

State is what something is. Behavior is what something does. In the warm up, our Class was Student. We modeled the state of a Student by defining the attributes Age, ID, email, Name and GPA. We modeled the behavior of a Student by defining the methods register courses, change courses, check gpa.

An Instance or Object is a concrete representation of a Class. In the previous activity, Level 100 Student is a specific Instance of the Student Class. We can also say that Level 100 Student is a Student Object. Do not get confused by the terms Instance and Object. They mean the exact same thing. Think of playing with your own shadow, you full human body is the Class(main Object) but you can cast(clone) instances(objects) of your shadow at any where.

Think of a Class like a blueprint for a house and an Instance as an actual house. The blueprint is a just an idea of how the house should be built, and the house is the realization of that blueprint.

Syntax

  • The syntax for defining a class is as follows:
class NameOfClass
end

So, for example, if we wanted to create a Student class, we could do the following:

class Student
end
  • Notice the use of UpperCamelCase for the class name.

  • Generally we will want to put more information in our classes to make them useful to us, but those two lines even with no other information will create a class.

Example - Class and Instance Syntax

  • Let’s follow an example with a Employee class. I will create a directory in the NBIT208-DIFT208-2S23 directory called objects_classes_and_instances. Within that directory, I’ll create an employee.rb file, and put the following information into that file. (You can do what ever you like if you know what you are doing).
cd NBIT208-DIFT208-2S23 
mkdir objects_classes_and_instances
cd objects_classes_and_instances
touch employee.rb
class Employee 
end
  • Notice that class is lowercase while NameOfClass is UpperCamelCased
  • In the same objects_instances_and_classes directory, let’s create a main.rb file and put the code below into that
touch main.rb
require './employee'

paul = Employee.new
patrick = Employee.new

require 'pry'; binding.pry
  • We can run the main.rb file from the command line if we are inside of our objects_classes_and_instances directory by typing the following: ruby main.rb on the terminal.
pwd
ruby main.rb
  • When we run this file, our terminal should open up a pry session when it reads the line: binding.pry. Inside of that pry session, we’ll type paul and hit return to see what the variable paul is holding. Then, we’ll type patrick to see what that variable is holding.

Reflection

  • How are those two things the same?
  • How are they different?

Attributes in Ruby Classes

  • Above we created an Enployee class and then also created specific instances of the Employee class that we held in the variables paul and patrick. Generally the objects we create will come from the same template, but each will be a unique object.

Take a look at these Employee.

Each one is different in important ways. For example, each one has its own:

  • Name
  • ID
  • Age

We can model these attributes in code by using instance variables. Generally we define these instance variables in a special method called initialize that is run every time a new instance of a class is created. Thus called constructor in other language like Java.

Initialize

When we run Employee.new in Ruby, what actually happens? We can see from the last example that different Employee objects (or instances) are created. Other than that, nothing happens. If we want some specific code to run when we first create a new Employee, we need to tell Ruby what should happen when a new Employee instance (or object) is created. We do this with the initialize method.

class Employee 
def def initialize
# any code here will run each time a new instance is created
end
end
  • This method is run once and only once during an Object’s lifetime, when we call new.
  • Other than that, initialize is like any other method where we can put Ruby code
class Employee 
def def initialize
puts "An Employee class implementation"
end
end

Modeling State with Attributes

  • The instances of the classes we have defined so far are basically useless. Aside from their object_id, there is nothing unique about these instances.
  • Remember, a class models State and Behavior. Let’s give our Employee some state.

Example - Attributes

  • Let’s add some attributes to the Employee class.
  • The @ symbol before a variable name indicates that it is an Attribute or Instance Variable. These two terms mean the exact same thing.
class Employee 
def def initialize(id, name, age)
@id = id
@name = name
@age = age
end
end
  • Because attributes are something we want to persist throughout an object’s lifetime, we typically define them inside the initialize method because we want them to exist as soon as the object is created.

  • We have now created a method class that will allow us to create many different instances of Employee, each one slightly different from the last.

  • How do we do that in practice? Let’s update the main.rb file so that it includes the following:

require './employee'

paul = Employee.new(1, "Paul", 34)
patrick = Employee.new(2, "Patrick", 26)
eric = Employee.new(3, "Eric", 45)

require 'pry'; binding.pry
  • When we include the arguments to .new, Ruby will pass those arguments to the initialize method for us. Note that the arguments that we pass to new are order dependent. So, in the first example when we pass 1 as the first argument, we are saying that the id of the Employee we are creating is 1. When we pass in 2 the second time we call new we are saying that the Employee that we created must have an id 2.

  • What we have just done is a very common pattern. We gave our initialize method some arguments and we saved those arguments to instance variables. While this is a strong pattern, it is not a rule. For instance, you may want to set a variable in your initialize that has a default value that isn’t set using an argument.

class Employee 
def def initialize(id, name, age)
@id = id
@name = name
@age = age
@retirement_age = 70
end
end

Practice

  • Create an objects_classes_and_instances directory, then touch a person.rb file and a run_person.rb file. Define a Person class in it and create instances of that class in your runner file.

  • Give your Person class some attributes that are set using arguments to initialize and some attributes that have default values. Make some instances of your Person class, and run you runner file.

Accessing Attributes

  • That’s all well and good, but what can we do with all these attributes that we’ve created They’re no good to us if we can’t use them.

  • Generally, the way that we access information stored in a class is by sending it messages or calling methods on that class. We do that using . syntax.

  • Let’s run our main file again and check to see what this returns

require './employee'

paul = Employee.new(1, "Paul", 34)
patrick = Employee.new(2, "Patrick", 26)
eric = Employee.new(3, "Eric", 45)

require 'pry'; binding.pry

paul.id
  • We should get an error that says something about the method .id not existing (a no method error). The syntax here is correct, but we haven’t told our Employee class how to respond when it receives the message id.

  • We can do that with methods like the ones we’ve seen before, but attributes stored as instance variables are special. We can tell our class to provide access to them using attribute readers. Let’s do that now.

Example - Accessing Attributes

  • Let’s update our Employee class to include the lines below.
class Employee 
def initialize(id, name, age)
@id = id
@name = name
@age = age
@retirement_age = 70
end

def id
@id
end

def name
@name
end

def age
@age
end

def retire_age
@retirement_age
end
end
  • Let’s run our runner file again and see if you can now call paul.id.
  • type paul.id paul.name paul.age
require './employee'

paul = Employee.new(1, "Paul", 34)
patrick = Employee.new(2, "Patrick", 26)
eric = Employee.new(3, "Eric", 45)


require 'pry'; binding.pry

paul.id
  • Now, I should be able to call paul.id and get back whatever was stored in the instance variable. But wow, this class is suddenly lengthy, harder to read, and has a lot of similar work happening. A method called id returns @id, name returns @name, etc. There’s a cleaner way to do the same thing:
class Employee
attr_reader :id, :name, :age, :retirement_age

def initialize(id, name, age)
@id = id
@name = name
@age = age
@retirement_age = 70
end

end
  • Let’s run our runner file again and see if you can still call paul.id and the other attributes.

  • An important thing to remember is that although there is a special syntax for creating attr_readers, they are still just methods. Remember the error we got earlier was a no method error for name.

Practice

  • Create attr_readers for the attributes in your Person class.
  • Practice explaining to your what is happening under the hood with the attr_readers

Other Methods

  • We can also create other methods that will allow us to send other messages to our Employee class. For example, let’s say we wanted to add a increase salary to our Employee class. Let’s do that by creating a method called increase_salary that will add to the previous salary.
class Employee
attr_reader :id, :name, :age, :retirement_age

def initialize(id, name, age)
@id = id
@name = name
@age = age
@retirement_age = 70
@salary = 500
end

def increase_salary(amount)
@salary + amount
end

end

Let’s update our main file so that you:

  • Create a new instance of Employee.
  • Print the salary of that Employee.
  • Add an increase in salary
  • Print the new salary of the Employe.
class Employee
attr_reader :id, :name, :age, :retirement_age, :salary

def initialize(id, name, age)
@id = id
@name = name
@age = age
@retirement_age = 70
@salary = 500
end

def increase_salary(amount)
@salary += amount
end

end

Practice

  • Create a have_birthday method for your Person class. This should increase the age of that person by 1.
  • Update your run_person file in a similar fashion to steps 1-4 for your Person class.

Object Interaction

  • When we build more complex programs, we typically have many classes, and the instances of those classes interact in some way.

Example - Object Interaction

  • Employee can be working at different Location, so we can have a location class.
class Location 
attr_reader :country, :state, :city
def initialize(country, state, city)
@country = country
@state = state
@city = city
end
end

Practice

Create a Book Class

  • Create a book class. Make sure that your book class with title, author, and genre attributes.

  • Once you’ve created your class, create a run_book file that creates three separate instances of book and saves them to variables.

  • Check in with your partner that you’re in a similar place. Discuss an differences you have in your code.

Create a Library Class

  • Create a Library class. Add attributes as you wish, but the be sure to include a @collection instance variable that starts as an empty array.

  • Check in with your partner that you’re in a similar place. Discuss an differences you have in your code.

If you have time:

  • Add a add_book method that takes an instance of book and adds it to your collection.

  • Add a titles method that iterates over your collection of books and returns only their titles.

  • Add an authors method that iterates over your collection of books and returns the authors for each book. Can you make it so that it does not return any duplicate authors?

  • Pretty print: add a method that prints a table of books and authors that the library has. This will require some string manipulation to get a table to print with columns that line up.

  • Update your run_book file to create a new library, add some books to the library, and print information about their collections.

2. Intro to Testing

Learning Goals

  • Understand why we use tests
  • Define the stages of a test
  • Define a RSpec test
  • Use a variety of assertion methods

Vocabulary

  • Gem
  • Test
  • Assertion

Warm Up

  • Think back of your practical solution in (1); how did you know if your program was working?
  • What are some potential drawbacks to this approach?

Test Etiquette

File Structure

  • Spec files live in their own spec directory
  • Implementation code files live in a sibling lib directory
  • Spec files should reflect the class they’re testing with _spec appended to the file name, e.g. spec/name_of_class_spec.rb
  • In your test, you’ll now require ./lib/name_of_class.rb
  • Run your spec files from the root of the project directory, e.g. rspec spec
  • If you want to run a specfic spec file you can append the location of that file to the rspec spec command. So rspec spec spec/name_of_file_spec.rb
.
├── lib
| └── name_of_class.rb
└── spec
└── name_of_class_spec.rb

RSpec Setup

  • RSpec is a framework used for automated testing. It helps us to do Test Driven Development TDD
gem install rspec
  • Require rspec - the easy and explicit way to run all your tests

RSpec Convention

  • At the top of every spec file: describe NameOfClass
  • describe ‘#name_of_method’
    • It is good practice to have another describe block for the name of method. That way we can group all assertions dealing with this method in this describe block.
  • We need to have an assertion at the end of every test
    • A lot of times we are going to compare if two values are equal to each other
    • We do that by writing expect(actual).to eq(expected) where actual is the result of the method call or object querying, and expected is the value we expect it to be.
  • RSpec Expectations Documentation

S.E.A.T

Each test that we create needs 4 components to be a properly built test.

  • Setup - The setup of a test is all of the lines of code that need to be executed in order to verify some behavior. Because each test is run individually, we often see the same setup being created multiple times.
  • Execution - The execution is the actual running of the method we are testing. This sometimes happens on the same line as the assertion, and sometimes happens prior to the assertion.
  • Assertion - The verification of the behavior we are expecting. This is really the main focus of the test; without the assertion, we have no test.
  • Teardown - After we complete a test, we want to delete all of our setup, and clear the scope for our next test. In RSpec this is done automatically!
cd objects_classes_and_instances
mkdir testing
cd testing
mkdir lib spec
cp ../person.rb lib
cd spec
touch person_spec.rb
cd ..
rspec spec
rspec spec spec/person_spec.rb
require 'rspec'
require './lib/person'

describe Person do
describe '#initialize' do
it 'is an instance of person' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person).to be_a Person
end

it 'has a first name' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person.first_name).to eq 'Paul'
end

it 'has a last name' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person.last_name).to eq 'Offei'
end

it 'has age' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person.age).to eq 44
end

it 'has a gender' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person.gender).to eq 'male'
end

it 'has status by default' do
person = Person.new("Paul", "Offei", 44, "male")
expect(person.status).to eq 'single'
end

it 'can have different name as well' do
person = Person.new("Esi", "Osei", 24, "female", "married")
expect(person.first_name).to eq 'Esi'
expect(person.last_name).to eq 'Osei'
expect(person.age).to eq 24
expect(person.gender).to eq 'female'
expect(person.status).to eq 'married'
end
end

describe '#have birthday' do
it 'add plus one to the person age' do
person = Person.new("Paul", "Offei", 44, "male")
person.have_birthday

expect(person.age).to eq 45
end
end

end

Practice

  • Apply the same techniques on book.rb employee.rb library.rb location.rb class

3. Flow Control

Conditional Statement in Ruby

Is a Boolean statement, which is a Declarative statement that is either true or false but not both

  • true
  • false
[1] pry(main)> statement1 = true
=> true
[2] pry(main)> statement2 = false
=> false
[3] pry(main)> statement3 = true
=> true
[4] pry(main)> statement4 = false
=> false

Comparason Operator in Ruby

Their result always return true or false

  • == (Equal to:)
  • = (greater than or equal to:)

  • <= (less than or equal to)
  • (greater than)

  • < (less than)
  • != (not equal to)
[5] pry(main)> 2 == 2
=> true
[6] pry(main)> 3 != 5
=> true
[7] pry(main)> 7 > 5
=> true
[8] pry(main)> 99 < 34
=> false
[9] pry(main)> 44 >= 55
=> false
[10] pry(main)> 44 <= 66
=> true

Relation Operators in Ruby

  • && (and) - only true when all of its statement are true
  • || (or) - only false when all of its statement are false
  • ! (not) - inverse of itself
[13] pry(main)> statement1 && statement2
=> false
[14] pry(main)> statement1 && statement3
=> true
[15] pry(main)> statement1 || statement1
=> true
[16] pry(main)> statement2 || statement4
=> false
[17] pry(main)> !statement1
=> false
[18] pry(main)> !statement2
=> true

Branching or Decision Making or Selection

  • if
  • if - else
  • if - elsif .... - else
[19] pry(main)> if statement1 
[19] pry(main)* puts "I Love System Analysis and Design"
[19] pry(main)* end
I Love System Analysis and Design
=> nil
[20] pry(main)> if statement2
[20] pry(main)* puts "I Love NSBT"
[20] pry(main)* else
[20] pry(main)* puts "Yeah NSBT is the best"
[20] pry(main)* end
Yeah NSBT is the best
=> nil
[21] pry(main)> if statement2
[21] pry(main)* puts "yes!"
[21] pry(main)* elsif statement4
[21] pry(main)* puts "yes yes!!"
[21] pry(main)* elsif statement1 && statement2
[21] pry(main)* puts "yes yes yes!!!!"
[21] pry(main)* elsif statement3 || statement4 && statement1
[21] pry(main)* puts "yeah yeah!!!!!"
[21] pry(main)* else
[21] pry(main)* puts "Hey hey!!"
[21] pry(main)* end
yeah yeah!!!!!
=> nil

Repeatition in Ruby with

  • Times
[22] pry(main)> 5.times do
[22] pry(main)* puts "I Love you"
[22] pry(main)* end
I Love you
I Love you
I Love you
I Love you
I Love you
=> 5
[23] pry(main)> 3.times do |any_name|
[23] pry(main)* puts any_name
[23] pry(main)* end
0
1
2
=> 3
  • while
[24] pry(main)> initialization = 0
=> 0
[25] pry(main)> while initialization < 5
[25] pry(main)* puts "I Like You"
[25] pry(main)* initialization += 1
[25] pry(main)* end
I Like You
I Like You
I Like You
I Like You
I Like You
=> nil
  • until (is the onverse of while)
[31] pry(main)> age = 5
=> 5
[32] pry(main)> until age > 10
[32] pry(main)* puts "I'm still a baby"
[32] pry(main)* age = age + 1
[32] pry(main)* end
I'm still a baby
I'm still a baby
I'm still a baby
I'm still a baby
I'm still a baby
I'm still a baby
=> nil
  • loop
[35] pry(main)> age = 10
=> 10
[36] pry(main)> loop do
[36] pry(main)* if age != 18
[36] pry(main)* puts "you are not eligible to vote"
[36] pry(main)* end
[36] pry(main)* if age % 2 == 0
[36] pry(main)* age += 2
[36] pry(main)* end
[36] pry(main)* if age == 18
[36] pry(main)* puts "woow I can vote"
[36] pry(main)* break
[36] pry(main)* end
[36] pry(main)* end
you are not eligible to vote
you are not eligible to vote
you are not eligible to vote
you are not eligible to vote
woow I can vote
=> nil

4. Scope

Scope is what you have access to and where you have access to it. Think of scope as a gateman and a house owner. House owner has her own room and the gateman to has his own room closer to the gate. both room are caled scope but the house owner can have access to the gateman room but the gateman don't have access to her room.

  • Local Variable: name fish_and_chips x_axis thx1138 _x _26
  • Instance Variable: @name @point1 @X @ @plan9
  • Class Variable: @@total @@symtab @@N @@x_pos @@SINGLE
  • Global Variable: $debug $CUSTOMER $_ $plan9 $Global
  • Class Name: String ActiveRecord MyClass
  • Constant Name: FEET_PER_MILE DEBUG

Global Scope

Example 1

x = 10
puts x
puts y

Example 2

x = 10
puts x
puts y
y = 20

Example 3

x = 10
def say_hello
puts "Hello World!"
end
puts x

Example 4

def print_variable
x = 4
puts x
end

x = 2
print_variable

Example 5

def print_variable
x = 4
end

x = 2
print_variable
puts x

Method Scope

Example 6

def print_variable
x = 10
puts x
end
print_variable
puts x

Example 7

def print_variable
x = 4
puts x
end

print_variable

Example 8

def print_variable
x = 4
puts x
end

x = 2
print_variable

Example 9

def print_variable
x = 4
end

x = 2
print_variable
puts x

Example 10

def print_variable(x)
puts x
end

print_variable(4)

Example 11

def print_variable(x)
puts x
end

x = 4
print_variable(x)

Example 12

def print_variable(x)
puts x
end

print_variable(2)
puts x

Example 13

def print_variable(x)
x = 4
puts x
end

print_variable(2)
puts x

Block Scope

Example 14

numbers = [1,2,3]
total = 0
numbers.each do |number|
total += number
end

p total

Example 15

numbers = [1,2,3]
total = 0
numbers.each do |number|
pizza = "yummy!"
total += number
end

p pizza

Example 15

numbers = [1,2,3]
total = 0
numbers.each do |number|
total += number
end

p number

Example 17

numbers = [1,2,3]
number = 0
numbers.each do |number|
puts number
end

Example 18

numbers = [1,2,3]
numbers.each do |number|
number = 0
puts number
end

Example 19

numbers = [1,2,3]
def number
0
end
numbers.each do |number|
puts number
end

Class Scope

  • refer to examples in (1)