Isolate dependencies while building object relationships in ruby - ruby

I'm practicing OOP for the first time by modeling my application domain (public high school) as objects, and I'm stuck on how to create relationships between the classes without introducing lots of external dependencies.
I have lots of relationships I want to construct, so in hopes of learning the general principle I'm giving two classes and sample objects here to illustrate the difficulty I'm having.
I have two classes Gradeand Transcript. Every instance of Transcript has an instance variable #mark, which right now is a string. I collected all the instances of each class a grades hash and a transcripts hash.
Question: How can I modify these classes so that #mark references the corresponding Grade instance?
(or, is that the wrong approach entirely?)
Grade has an instance for every possible final grade students can receive
class Grade
attr_accessor :mark, :alpha_equivalent, :numeric_range_low, :numeric_range_high, :numeric_qquivalent, :pass_fail_equivalent, :description
def initialize(args)
#mark = args["Mark"]
#alpha_equivalent = args["AlphaEquivalent"]
#numeric_range_low = args["NumericRangeLow"]
#numeric_range_high = args["NumericRangeHigh"]
#numeric_equivalent = args["NumericEquivalent"]
#pass_fail_equivalent = args["PassFailEquivalent"]
#description = args["Description"]
end
end
Sample object from the grades hash:
grades["100"] =>
#<Grade:0x007f9fcb077d68
#alpha_equivalent="100",
#description="100 out of 100",
#mark="100",
#numeric_equivalent="100",
#numeric_range_high="100",
#numeric_range_low="100",
#pass_fail_equivalent="P">
Transcript has instances for every final grade the student has ever received for all the courses they've studied
class Transcript
attr_accessor :student_id, :last_name, :first_name, :grade, :official_class, :school, :year, :term, :course, :course_title, :mark, :pass_fail, :credits
def initialize(args)
#student_id = args["StudentID"]
#last_name = args["LastName"]
#first_name = args["FirstName"]
#grade = args["Grade"]
#official_class = args["OffClass"]
#school = args["school"]
#year = args["Year"]
#term = args["Term"]
#course = args["Course"]
#course_title = args["Course Title"]
#mark = args["Mark"]
#credits = args["Credits"]
#grade_entry_cohort = args["GEC"]
end
end
Sample object from the transcripts hash:
transcripts["foobar-COURSE1-100"] =>
#<Transcript:0x007f9fce8786b8
#course="COURSE1",
#course_title="Example Course",
#credits="5",
#first_name="FOO",
#grade="100",
#grade_entry_cohort="V",
#last_name="BAR",
#mark="100",
#official_class="000",
#school="1",
#student_id="0123",
#term="1",
#year="2000">
I'm instantiating all the objects from CSV source files and then collecting them into a hash because I wanted to be able to address them directly.

Sounds like you need to want Transcript#grade to return a Grade instance. So let's make a method for that:
class Grade
def self.all
#all ||= {}
end
def self.find(mark)
all[mark]
end
end
Now, Grade.all needs to be populated. This could be achieved like this from your CSV:
grade_args = %w[alpha_equivalent description mark numeric_equivalent numeric_range_high numeric_range_low pass_fail_equivalent]
CSV.parse { |row| Grade.all.merge(csv['mark'] => Grade.new(row.slice(*grade_args)}
Now, we can modify Transcript like this:
class Transcript
def initialize(args)
#args = args
end
def grade
#grade ||= Grade.find(args['mark'])
end
private
attr_reader :args
end

Assuming that you've created the grades hash earlier:
# read args from csv
# id built from first, last, course, and grade
transcripts[id] = Transcript.
new(args.merge('Mark' => grades[args['Mark']])
It uses Hash#merge to extend args with an instance of Grade that was built earlier.

Related

Refactor with Strategy Pattern. In Ruby

Heads up! In the below example, using a pattern is probably overkill... however, if I were extending this to count genres, count the members in a given band, count the number of fans, count the number of venues played, count the number of records sold, count the number of downloads for a specific song etc... it seems like there could be a ton of stuff to count.
The Goal:
To create a new function that chooses the correct counting function based on the input.
The Example:
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count
self.songs.length
end
def artist_count
self.artists.length
end
end
P.S. If you are also a curious about this question, you may find this other question (unfortunately answered in C#) to be helpful as a supplemental context. Strategy or Command pattern? ...
In Ruby you can implement a strategy pattern quite easily using an (optional) block (assuming it's still unused).
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count(&strategy)
count_using_strategy(songs, &strategy)
end
def artist_count(&strategy)
count_using_strategy(artists, &strategy)
end
private
def count_using_strategy(collection, &strategy)
strategy ||= ->(collection) { collection.size }
strategy.call(collection)
end
end
The above code defaults to using the size strategy. If you ever want to use a specific strategy in a specific scenario you can simply provide the strategy alongside the call.
genre = Genre.last
genre.song_count # get the song_count using the default #size strategy
# or provide a custom stratigy
genre.song_count { |songs| songs.count } # get the song_count using #count
genre.song_count { |songs| songs.length } # get the song_count using #length
If you need to re-use some strategies more often you could save them in a constant or variable:
LENGTH_STRATEGY = ->(collection) { collection.length }
genre.artist_count(&LENGTH_STRATEGY)
Or create a specific class for them if they are more complicated (currently overkill):
class CollectionStrategy
def self.to_proc # called when providing the class as a block argument
->(collection) { new(collection).call }
end
attr_reader :collection
def initialize(collection)
#collection = collection
end
end
class LengthStrategy < CollectionStrategy
def call
collection.length
end
end
genre.artist_count(&LengthStrategy)

Ruby - Passing instance variables to a class from another

I have 3 classes: Invoice, Address and Customer (but for this problem, only the Invoice and Address class are relevant)
This is my Invoice class:
class Invoice
attr_reader :billing_address, :shipping_address, :order
def initialize(attributes = {})
#billing_address = attributes.values_at(:billing_address)
#shipping_address = attributes.values_at(:shipping_address)
#order = attributes.values_at(:order)
end
end
and this is my Address class:
class Address
attr_reader :zipcode, :full_address
def initialize(zipcode:)
#zipcode = zipcode
url = 'https://viacep.com.br/ws/' + zipcode.to_s + '/json/'
uri = URI(url)
status = Net::HTTP.get_response(uri)
if (status.code == "200")
response = Net::HTTP.get(uri)
full_address = JSON.parse(response)
#full_address = full_address
else
p "Houve um erro. API indisponível. Favor tentar novamente mais tarde."
#full_adress = nil
end
end
end
And this is my Customer class (not much relevant, but i'm showing for better explanation of the problem)
class Customer
attr_reader :name, :age, :email, :gender
def initialize(attributes = {})
#name = attributes.values_at(:name)
#age = attributes.values_at(:age)
#email = attributes.values_at(:email)
#gender = attributes.values_at(:gender)
end
end
As you can see, my Invoice class has 3 instance variables and my Address class has 2 instance variables.
So, if i test something like that:
cliente = Customer.new(name: "Lucas", age: 28, email: "abc#gmail.com", gender: "masculino")
endereco = Address.new(zipcode: 41701035)
entrega = Invoice.new(billing_address: endereco, shipping_address: endereco)
p endereco.instance_variables
[:#zipcode, :#full_address]
p entrega.shipping_address.instance_variables
[]
My instance variables can be acessed through the variable "endereco", that is an Address object, but can't be acessed through entrega.shipping_address that is also an Address object.
To be more precise, if a try this:
p entrega.shipping_address
I get this return:
[#<Address:0x00000001323d58 #zipcode=41701035, #full_address={"cep"=>"41701-035", "logradouro"=>"Rua Parati", "complemento"=>"", "bairro"=>"Alphaville I", "localidade"=>"Salvador", "uf"=>"BA", "unidade"=>"", "ibge"=>"2927408", "gia"=>""}>]
My full object are being returned, but i can't access the content of my #full_address instance variable.
If a do this:
p entrega.shipping_address.full_address
I get a NoMethodError:
solucao.rb:8:in `<main>': undefined method `full_address' for #<Array:0x000000012d25e8> (NoMethodError)
I'm trying to understand why i can't access the content inside my object if i have the full object. Maybe i'm trying to access in the wrong way, i don't know.
Can someone help ?
values_at returns an array of values (see https://apidock.com/ruby/Hash/values_at
for explanation)
Change
#shipping_address = attributes.values_at(:shipping_address)
into
#shipping_address = attributes[:shipping_address]
And that way #shipping_address will contain an Address object, not an array that contains an Address object
If you take a look at the error, it says
undefined method `full_address' for #<Array:0x000000012d25e8>
You're trying to call full_address on an array. So this means that entrega.shipping_address returns you an array (which it does, of course, take a closer look at the output).
If I were you, I'd look into how shipping_address is implemented. It's a simple attr_reader, so it's backed by an instance variable. Must be you initialize that instance variable to a wrong value (it gets an array instead of an address). Look closely at that initialization code and try to run it in IRB session. You should see the problem.

bad char after creating a Database from csv

I am trying to create a database using mongoid but it fails to find the create method. I am trying to create 2 databases based on csv files:
extract_data class:
class ExtractData
include Mongoid::Document
include Mongoid::Timestamps
def self.create_all_databases
#cbsa2msa = DbForCsv.import!('./share/private/csv/cbsa_to_msa.csv')
#zip2cbsa = DbForCsv.import!('./share/private/csv/zip_to_cbsa.csv')
end
def self.show_all_database
ap #cbsa2msa.all.to_a
ap #zip2cbsa.all.to_a
end
end
the class DbForCSV works as below:
class DbForCsv
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Attributes::Dynamic
def self.import!(file_path)
columns = []
instances = []
CSV.foreach(file_path, encoding: 'iso-8859-1:UTF-8') do |row|
if columns.empty?
# We dont want attributes with whitespaces
columns = row.collect { |c| c.downcase.gsub(' ', '_') }
next
end
instances << create!(build_attributes(row, columns))
end
instances
end
private
def self.build_attributes(row, columns)
attrs = {}
columns.each_with_index do |column, index|
attrs[column] = row[index]
end
ap attrs
attrs
end
end
I am not aware of all fields and it may change in time. that's why I have create database and generic mehtods.
I have also another issue after having fixed the 'create!' issue.
I am using the encoding to make sure only UTF8 char are handled but I still see:
{
"zip" => "71964",
"cbsa" => "31680",
"res_ratio" => "0.086511098",
"bus_ratio" => "0.012048193",
"oth_ratio" => "0.000000000",
"tot_ratio" => "0.082435345"
}
when doing 'ap attrs' in the code. how to make sure that 'zip' -> 'zip'
Thanks
create! is a class method but you're trying to call it as an instance method. Your import! method shouldn't be an instance method either, it should be a class method since it produces instances of your class:
def self.import!(file_path)
#-^^^^
# everything else would be the same...
end
You'd also make build_attributes a class method since it is just a helper method for another class method:
def self.build_attributes
#...
end
And then you don't need that odd looking new call when using import!:
def self.create_all_databases
#cbsa2msa = DbForCsv.import!('./share/private/csv/cbsa_to_msa.csv')
#zip2cbsa = DbForCsv.import!('./share/private/csv/zip_to_cbsa.csv')
end

Accessing instance variable array using IRB

I'm new to Ruby and am working on an exercise where a sports team (of up to 10 ppl) have to have at least 2 men and 2 women. I've created a Player class where the player's gender is determined and a Team class, where I add these players into an instance variable array of #team (which is created upon initialization of Team).
I have placed my full code towards the bottom of this request.
I'm hoping someone can please help me on the following:
(1) What do I type in IRB to be able to specifically recall/maniuplate the #team instance variable array (which stores all the players). I wish to later iterate over the array or extract the first item in the array (#team.first does not work)
(2) I'm having difficulty writing the code to determine if at least 2 male and female players are in the #team instance variable. The best code I came up in the Team class was the following - but it is reporting that #team is nilclass.
def gender_balance
#team.select{|player| player.male == true }
end
I have researched the internet and tried various combinations for an answer - with no success.
Directly below are the IRB commands that I have typed to create and add players to teams. Later below is my code for Team (which contains the method to assess whether it has the right gender mix) and Players.
irb(main):001:0> team = Team.new
=> #<Team:0x007fd8f21df858 #team=[]>
irb(main):002:0> p1 = Player.new
=> #<Player:0x007fd8f21d7b08 #male=true>
irb(main):003:0> p1.female_player
=> false
irb(main):004:0> p2 = Player.new
=> #<Player:0x007fd8f21bff58 #male=true>
irb(main):005:0> team.add_player(p1)
=> [#<Player:0x007fd8f21d7b08 #male=false>]
irb(main):006:0> team.add_player(p2)
=> [#<Player:0x007fd8f21d7b08 #male=false>, #<Player:0x007fd8f21bff58 #male=true>]
These two IRB lines are me trying unsucessfully to recall the contents of #team
irb(main):007:0> team
=> #<Team:0x007fd8f21df858 #team=[#<Player:0x007fd8f21d7b08 #male=false>, #<Player:0x007fd8f21bff58 #male=true>]>
irb(main):013:0> #team
=> nil
The code for both classes is below:
class Player
def initialize
#male = true
end
def female_player
#male = false
end
def male_player
#male
end
end
class Team
def initialize
#team = []
end
def add_player player
#team << player
end
def player_count
#team.count
end
def valid_team?
player_number_check
gender_balance
end
private
def player_number_check
player_count > 6 && player_count < 11
end
def gender_balance
#team.select{|player| player.male == true }
end
end
My github reference for this code is: https://github.com/elinnet/object-calisthenics-beach-volleyball-edition.git
Thank you.
Your Team class does not have an attribute for getting the #team instance variable.† So the only way you can extract its value is by using instance_variable_get:
irb(main):029:0> team = Team.new
=> #<Team:0x007fff4323fd58 #team=[]>
irb(main):030:0> team.instance_variable_get(:#team)
=> []
Please don't use instance_variable_get for actual production code though; it's a code smell. But for the purposes of inspecting instance variables in IRB, it's okay.
† You'd normally define one using either attr_accessor :team (read/write) or attr_reader :team (read-only) inside the class definition.

Assert_equal undefined local variable LRTHW ex52

Hi I made it to the lase exercise os Learn Ruby The Hard Way, and I come at the wall...
Here is the test code:
def test_gothon_map()
assert_equal(START.go('shoot!'), generic_death)
assert_equal(START.go('dodge!'), generic_death)
room = START.go("tell a joke")
assert_equal(room, laser_weapon_armory)
end
And here is the code of the file it should test:
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
#name = name
#description = description
#paths = {}
end
def ==(other)
self.name==other.name&&self.description==other.description&&self.paths==other.paths
end
def go(direction)
#paths[direction]
end
def add_paths(paths)
#paths.update(paths)
end
end
generic_death = Room.new("death", "You died.")
And when I try to launch the test file I get an error:
generic_death = Room.new("death", "You died.")
I tried to set the "generic_death = Room.new("death", "You died.")" in test_gothon_map method and it worked but the problem is that description of the next object is extremely long, so my questions are:
why assertion doesn't not respond to defined object?
can it be done different way then by putting whole object to testing method, since description of the next object is extremely long...
The nature of local variable is that they are, well, local. This means that they are not available outside the scope they were defined.
That's why ruby does not know what generic_death means in your test.
You can solve this in a couple of ways:
define rooms as constants in the Room class:
class Room
# ...
GENERIC_DEATH = Room.new("death", "You died.")
LASER_WEAPON_ARMORY = Room.new(...)
end
def test_gothon_map()
assert_equal(Room::START.go('shoot!'), Room::GENERIC_DEATH)
assert_equal(Room::START.go('dodge!'), Room::GENERIC_DEATH)
room = Room::START.go("tell a joke")
assert_equal(room, Room::LASER_WEAPON_ARMORY)
end
assert the room by its name, or some other identifier:
def test_gothon_map()
assert_equal(START.go('shoot!').name, "death")
assert_equal(START.go('dodge!').name, "death")
room = START.go("tell a joke")
assert_equal(room.name, "laser weapon armory")
end

Resources