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.
Related
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 currently working on a Student - Courses program. Everything was going well until I found myself struggling to understand who to allow my initialize method to have a parameter days which values could be any of the days of the week :mon, :tue, ...
To be more specific, what I am struggling with is, where and how should I include the list of days of the week that can be included in the initialize parameter of my class Course
This is how my class Course looks like so far:
class Course
attr_reader :department, :name, :credits, :time_block, :days, :students
def initialize(course_name, department, credits, time_block = nil, days = nil) # where should I include the list of symbols : :mon,:tue,:weds ... ?? , also time_block?
#course_name = course_name
#department = department
#credits = credits
#students = []
#days = days
#time_block = time_block
end
def name
#course_name
end
def department
#department
end
def credits
#credits
end
def add_student(student)
return if #students.include?(student)
student.enroll(self)
end
def conflicts_with?(course2)
return false if self.time_block != course2.time_block
days.any? do |day|
course2.days.include?(day)
end
end
end
class Course
attr_reader :days
def initialize(days)
#days = days
end
end
course = Course.new([:mon, :tue, ...])
Yep, it's that simple.
I am trying to add an attribute to an object after it's been created using a method from within the object Class. I'd like to put this code in the def set_sell_by and def get_sell_by methods, if this is possible. So, in the end I'd like to do apple.set_sell_by(10) and then get that value later by doing apple.get_sell_by to check if the item has 5 days or less left to sell it.
class Grocery_Inventory
attr_accessor :product, :store_buy, :quantity, :serial_number, :customer_buy
def initialize(product, store_buy, quantity, serial_number, customer_buy)
#product = product
#store_buy = store_buy
#quantity = quantity + 5
#serial_number = serial_number
#customer_buy = customer_buy
end
def get_product_name
p product
self
end
def get_cost_customer
p "$#{customer_buy}"
self
end
def get_product_quantity
p "You have #{quantity} #{product}"
self
end
def set_sell_by
#some code...
self
end
def get_sell_by
if sell_by < 5
p "You need to sell this item within five days."
self
else
p "Item doesn't currently need to be sold."
self
end
end
end
apples = Grocery_Inventory.new("apples", 1.00, 5, 123, 0.25)
apples.get_product_name
apples.get_cost_customer
apples.get_product_quantity
Ruby is very lax in this regard. Simply access a variable with #and if it doesn't exist it will be created.
def set_sell_by
#sell_by = value
self
end
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.
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