I have this classes (just a simplified example, not real ones)
class Shop
attr_accessor :name, :products
def initialize(name, products_names=%w(apple orange apple cucumber fennel))
self.name = name
self.products = products_names.map {|name| Product.new(self, name)}
end
end
class Product
attr_accessor :shop, :name
def initialize(shop, name)
self.shop = shop
self.name = name
end
def existing_products_count # problem_comes_here
shop.products #i need to process all products initialized for current shop
.select{|p| p.name==name}.size
end
def uniq_code
"#{name}_#{existing_products_count+1}"
end
end
And here is two questions:
Is this a good approach to pass self for Product instance initialization
and
How can i solve my case to process all already existing shop products for new product initialization
Thank you
UPDATE
all i invented for now is (at least it works like i need)
class Shop
attr_accessor :name, :products
def initialize(name, products_names=%w(apple orange apple cucumber fennel))
Product.class_variable_set(:##all, []) # << added
self.name = name
self.products = products_names.map {|name| Product.new(self, name)}
end
end
class Product
attr_accessor :shop, :name
def self.all # << added
##all
end
def initialize(shop, name)
self.shop = shop
self.name = name
self.class.all << self # << added
end
def existing_products_count # problem goes away here
self.class.all.products # << changed
.select{|p| p.name==name}.size
end
def uniq_code
"#{name}_#{existing_products_count+1}"
end
end
but i feel bad about this kind of solution (i don't know why) and will be appreciate for better one
I'm not sure if I understand the question, but here is one interpretation. Note how I've changed the method Product#uniq_code.
class Shop
attr_accessor :name, :products
def initialize(name, products_names=%w(apple orange apple cucumber fennel))
self.name = name
self.products = products_names.map {|name| Product.new(self, name)}
end
end
class Product
attr_accessor :shop, :name
def initialize(shop, name)
self.shop = shop
self.name = name
end
def uniq_code # problem_comes_here
puts "buy #{self.name} #{self.shop.name}"
end
end
Now let's have an example:
at_store = Shop.new("in store", ["bulldozer", "Ducati"])
online = Shop.new("online", ["can opener", "Makita router"])
arr = [at_store, online]
We can do this:
arr.flat_map { |s| s.products }.each { |p| p.send(:uniq_code) }
# buy bulldozer in store
# buy Ducati in store
# buy can opener online
# buy Makita router online
Is this roughly what you were looking for?
Edit: to save the product instances for a given shop instance:
product_instances = at_store.products
#=> [#<Product:0x007f93f984f938
# #shop=#<Shop:0x007f93f984f988 #name="in_store",
# #products=[...]>, #name="bulldozer">,
# #<Product:0x007f93f984f910
# #shop=#<Shop:0x007f93f984f988 #name="in_store",
# #products=[...]>, #name="Ducati">]
for use later:
product_instances.each { |p| p.uniq_code }
# buy bulldozer in_store
# buy Ducati in_store
Related
So when I run my lab i am getting the following error:
Associations — Song and Artist: Artist #add_song adds the song to the current artist's 'songs' collection
Failure/Error: expect(artist.songs).to include(song)
expected ["In the Aeroplane Over the Sea"] to include #<Song:0x0000000001496e88 #name="In the Aeroplane Over the Sea", #artist=#<Artist:0x0000000001496f50 #name="Neutral Milk Hotel", #songs=["In the Aeroplane Over the Sea"]>>
Diff:
## -1,2 +1,2 ##
-[#<Song:0x0000000001496e88 #name="In the Aeroplane Over the Sea", #artist=#<Artist:0x0000000001496f50 #name="Neutral Milk Hotel", #songs=["In the Aeroplane Over the Sea"]>>]
+["In the Aeroplane Over the Sea"]
But when I run my code through pry, I can see the song added into the array of the instance variable, #songs. I am working with two classes, a song class and artist class.
class Artist
attr_accessor :name
attr_reader :songs
##all = []
def initialize(name)
#name = name
save
#songs = []
end
def self.all
##all
end
def save
##all << self
end
def self.destroy_all
##all.clear
end
def self.create(name)
self.new(name)
end
def add_song(song)
if song.artist == nil
song.artist = self
end
#checks if the same song has already been added, otherwise adds new song
if #songs.include?(song.name) == false
#songs << song.name
end
binding.pry
end
end
class Song
attr_accessor :name, :artist
##all =[]
def initialize(name, artist = nil)
#name = name
#artist = artist
save
end
def self. all
##all
end
def save
##all << self
end
def self.destroy_all
##all.clear
end
def self.create(name)
self.new(name)
end
end
Your tests are expecting an aray of Song instances. But what they find is an array of strings.
To fix it you'd just need to change #songs << song.name to #songs << song.
I am sorry if I just gave you the answer instead of hinting you along to solve it yourself, but this isn't really the right platform for that kind of thing. Every question on StackOverflow can be searched by users and we want to keep things helpful to them by getting straight to the point.
However, I can give some advice. Practice reading errors. Try and understand what the error is telling you.
Here's code that works but I'm looking to make it as clean as possible, to get the output without having to build a hash.
class Person
attr_accessor :name, :age
def initialize(name, age)
#name = name
#age = age
end
def create
Report.create({name: #name, age: #age})
end
end
class Report < Person
def self.create(attributes)
puts "Hello, this is my report. I am #{attributes[:name]} and my age is #{attributes[:age]}."
end
end
me = Person.new("Andy", 34)
me.create # Hello, this is my report. I am Andy and my age is 34.
Here are my changes that didn't work, but is there a method that would?
def create
Report.create
end
and
def self.create(attributes)
puts "Hello, this is my report. I am #{:name} and my age is #{:age}."
end
but the output was "I am name and my age is age."
You could just pass the person, something like this:
class Person
attr_accessor :name, :age
def initialize(name, age)
#name = name
#age = age
end
def report
Report.new(self)
end
end
class Report
attr_accessor :person
def initialize(person)
#person = person
end
def to_s
"Hello, this is my report. I am #{person.name} and my age is #{person.age}."
end
end
me = Person.new("Andy", 34)
puts me.report
# Hello, this is my report. I am Andy and my age is 34.
Note that I've changed some details:
Report doesn't inherit from Person
Report instances are created via new
Person#create is now Person#report
Report uses to_s for the output (which is called by puts)
I am studying the adapter pattern implementation in ruby. I want to access an instance variable within the adapter module definition. Take a look at the following code:
module Adapter
module Dog
def self.speak
# I want to access the #name instance variable from my Animal instance
puts "#{name} says: woof!"
end
end
module Cat
def self.speak
# I want to access the #name instance variable from my Animal instance
puts "#{name} says: meow!"
end
end
end
class Animal
attr_accessor :name
def initialize(name)
#name = name
end
def speak
self.adapter.speak
end
def adapter
return #adapter if #adapter
self.adapter = :dog
#adapter
end
def adapter=(adapter)
#adapter = Adapter.const_get(adapter.to_s.capitalize)
end
end
To test it out I did the following:
animal = Animal.new("catdog")
animal.adapter = :cat
animal.speak
I want it to return the following:
catdog says: meow!
Instead it says:
Adapter::Cat says: meow!
Any tips on how I can get access to the Animal#name instance method from the adapter module? I think the issue is that my adapter methods are class-level methods.
Thanks!
You need to use your Module as a mixin and provide a way to keep track of which module is active, the methods don't seem to be overwritten by reincluding or reextending so I took the extend and remove methods I found here.
module Adapter
module Dog
def speak
puts "#{name} says: woof!"
end
end
module Cat
def speak
puts "#{name} says: meow!"
end
end
def extend mod
#ancestors ||= {}
return if #ancestors[mod]
mod_clone = mod.clone
#ancestors[mod] = mod_clone
super mod_clone
end
def remove mod
mod_clone = #ancestors[mod]
mod_clone.instance_methods.each {|m| mod_clone.module_eval {remove_method m } }
#ancestors[mod] = nil
end
end
class Animal
include Adapter
attr_accessor :name, :adapter
def initialize(name)
#name = name
#adapter = Adapter::Dog
extend Adapter::Dog
end
def adapter=(adapter)
remove #adapter
extend Adapter::const_get(adapter.capitalize)
#adapter = Adapter.const_get(adapter.capitalize)
end
end
animal = Animal.new("catdog")
animal.speak # catdog says: woof!
animal.adapter = :cat
animal.speak # catdog says: meow!
animal.adapter = :dog
animal.speak # catdog says: woof!
This is because name inside of the module context refers to something entirely different than the name you're expecting. The Animal class and the Cat module do not share data, they have no relationship. Coincidentally you're calling Module#name which happens to return Adapter::Cat as that's the name of the module.
In order to get around this you need to do one of two things. Either make your module a mix-in (remove self, then include it as necessary) or share the necessary data by passing it in as an argument to speak.
The first method looks like this:
module Adapter
module Dog
def self.speak(name)
puts "#{name} says: woof!"
end
end
end
class Animal
attr_accessor :name
attr_reader :adapter
def initialize(name)
#name = name
self.adapter = :dog
end
def speak
self.adapter.speak(#name)
end
def adapter=(adapter)
#adapter = Adapter.const_get(adapter.to_s.capitalize)
end
end
That doesn't seem as simple as it could be as they basically live in two different worlds. A more Ruby-esque way is this:
module Adapter
module Dog
def speak
puts "#{name} says: woof!"
end
end
end
class Animal
attr_accessor :name
attr_reader :adapter
def initialize(name)
#name = name
self.adapter = :dog
end
def adapter=(adapter)
#adapter = Adapter.const_get(adapter.to_s.capitalize)
extend(#adapter)
end
end
I am starting my studies with Ruby and OO and I have received a test to do about OO.
It is a store scenario where I have to create the classes, methods, calls ...
For now I have create the class Payment and Product classes (Physical, Membership, Digital)
class Payment
attr_reader :authorization_number, :amount, :invoice, :order, :payment_method, :paid_at, :shipping
def initialize(attributes = {})
#authorization_number, #amount = attributes.values_at(:authorization_number, :amount)
#invoice, #order = attributes.values_at(:invoice, :order)
#payment_method = attributes.values_at(:payment_method)
#shipping.extend Shipping
end
def pay(paid_at = Time.now)
#amount = order.total_amount
#authorization_number = Time.now.to_i
#invoice = Invoice.new(order.get_address, order.get_address, order)
#paid_at = paid_at
order.close(#paid_at)
extend
def paid?
!paid_at.nil?
end
def prepare_shipping(order)
???
end
end
class Product
attr_reader :name, :description, :type
def initialize(name, description, type)
#name = name
#description = description
#type = type
end
end
class Digital < Product
include DigitalProduct
def initialize(name, description, type)
super(name, description, type)
end
def matches?(query)
query=="digital"
end
end
class Physical < Product
def initialize(name, description, type)
super(name, description, type)
end
def matches?(query)
query=="physical"
end
end
class Membership < Product
attr_reader :membership_status
include DigitalProduct
def initialize(name, description, type)
super(name, description, type)
end
def matches?(query)
query=="membership"
end
def activate_membership()
#membership_status = true;
end
end
Here is my problem:
1.If the product is physical I have to generate a shipping label.
1.1 If its a book I have to notify that this product doesn't have taxes.
2.If the product is membership I have to activate the signature and notify via email the buyer.
If the product is digital I have to send a email for the buyer and give $10 discount in this product.
I am doing pretty good, but now I would like your opinion to know what is the best way following OO techniques (SOLID) to create the points above.
ps: I tried to create but my solution was really poor with many ifs.
I think there is a better way to do it with
I have other classes if you need more info tell me please.
Sorry about my poor english.
I have developed this solution, I think it is good ... maybe you can give me a feedback about it.
So to solve the problem about shipping and discount rules I created 2 modules Shipping and Discount
module Discount
def prepare_discount(order)
total_discount = 0
order.get_items.each do |item|
total_discount += item.product.discount
end
return total_discount
end
def discount
if matches?("physical")
return discount_for_physical_product
elsif matches?("digital")
return discount_for_digital_product
elsif matches?("membership")
return discount_for_membership_product
elsif matches?("default")
return discount_for_default_product
end
end
def discount_for_physical_product
price_discount_for_physical_product = 0
return price_discount_for_physical_product
end
def discount_for_digital_product
price_discount_for_digital_product = 10.00
return price_discount_for_digital_product
end
def discount_for_default_product
price_discount_for_digital_product = 10.00
return price_discount_for_digital_product
end
def discount_for_membership_product
price_discount_for_membership_product = 0
return price_discount_for_membership_product
end
end
module Shipping
def prepare_shipping(order)
order.get_items.each do |item|
item.product.shipping
end
end
def shipping
if matches?("physical")
shipping_for_physical_product
elsif matches?("digital")
shipping_for_digital_product
elsif matches?("membership")
shipping_for_membership_product
elsif matches?("default")
shipping_for_default_product
end
end
def shipping_for_physical_product
case #type
when :book
create_shipping_label
notify_buyer_product_without_taxes
else
create_shipping_label
end
end
def shipping_for_digital_product
notify_buyer_via_email
end
def shipping_for_membership_product
notify_buyer_via_email
activate_membership
end
def shipping_for_default_product
create_shipping_label
notify_buyer_via_email
end
def create_shipping_label
puts "Creating shipping label ..."
end
def notify_buyer_product_without_taxes
puts "Product exempt from taxes, as provided in the Constitution Art. 150, VI, d."
end
def notify_buyer_via_email
puts "Sending email ..."
end
end
This way Product and Order can extend both modules and implement the logic.
I have run into confusion tying together a test double and stubbing it. My question is - what is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
I have included the relevant code following code:
class PurchaseOrder
attr_reader :customer, :products
def initialize(customer)
#products = {}
#customer = customer
end
....some other methods
def add_product(product, quantity = 1)
#products[product] = (#products[product] ? #products[product] + quantity : quantity )
puts "You haved added #{quantity} #{product.title}'s to your purchase order"
end
def confirm_purchase_order
purchase_order_total
raise "Your PO appears to be empty! Add some products and try again." unless self.total.to_f.round(2) > 0
create_order
create_invoice
return "We have generated an Invoice and created an order."
end
def create_order
order = Order.new(customer)
order.products = #products.clone
end
def create_invoice
invoice = Invoice.new(customer)
invoice.products = #products.clone
end
end
class Order
attr_reader :customer
attr_accessor :status, :total, :products
def initialize(customer)
#products = {}
#status = :pending
#customer = customer
end
class Customer
attr_reader :name, :type
def initialize(name, type)
#name = name.to_s
#type = type.to_sym
end
class Invoice
attr_reader :customer, :products
attr_accessor :total
def initialize(customer, products)
#products = {}
#customer = customer
#payment_recieved = false
end
end
I want to test the confirm_purchase_order method as well as the create_order method in class PurchaseOrder. My approach so far:
I need some object doubles and an actual PurchaseOrder object
describe PurchaseOrder do
let(:product) { double :product, title: "guitar", price: 5 }
let(:order) { instance_double(Order) }
let(:customer) { double :customer, name: "Bob", type: :company }
let(:products) { {:product => 1} }
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
it "returns an order" do
expect(Order).to receive(:new).with(customer).and_return(order)
allow(order).to receive(products).and_return(???products??!)
purchase_order.add_product(product, 1)
purchase_order.create_order
expect(order.products).to eq (products)
end
end
end
I have also looked at the use of:
# order.stub(:products).and_return(products_hash)
# allow_any_instance_of(Order).to receive(:products) { products_hash }
# order.should_receive(:products).and_return(products_hash)
To setup the order double to return a products hash when order.products is called, but these all feel like they are 'rigging' the test too much. What is the most appropriate way to test the confirm_purchase_order and create_order methods in class PurchaseOrder?
It seems to me that perhaps you're giving PurchaseOrder too much responsibility. It now has intimate knowledge about Order and Invoice.
I'd perhaps test the current implementation like this:
it "returns an order with the same products" do
expect_any_instance_of(Order).to receive(:products=).with(products: 1)
purchase_order.add_product(product, 1)
expect(purchase_order.create_order).to be_a(Order)
end
But maybe it could make sense to decouple PurchaseOrder from Order and Invoice a little bit and do something like this:
class Invoice
def self.from_purchase_order(purchase_order)
new(purchase_order.customer, purchase_order.products.clone)
end
end
class Order
def self.from_purchase_order(purchase_order)
new.tap(purchase_order.customer) do |invoice|
invoice.products = purchase_order.products.clone
end
end
end
class PurchaseOrder
# ...
def create_order
Order.from_purchase_order(self)
end
def create_invoice
Invoice.from_purchase_order(self)
end
end
describe PurchaseOrder do
let(:customer) { double('a customer')}
let(:purchase_order) { PurchaseOrder.new(customer) }
describe "#create_order" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
describe "#create_invoice" do
expect(Order).to receive(:from_purchase_order).with(purchase_order)
purchase_order.create_order
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
describe Order do
describe '.from_purchase_order' do
# test this
end
end
This way you let the Order and Invoice classes know how to build themselves from a PurchaseOrder. You can test these class methods separately. The tests for create_order and create_invoice become simpler.
Some other things I thought of:
For products, try using a Hash with a default proc:
#products = Hash.new { |hash, unknown_key| hash[unknown_key] = 0 }
This way, you can always safely do #products[product] += 1.