Решение на Трета задача от Марин Иванов

Обратно към всички решения

Към профила на Марин Иванов

Резултати

  • 6 точки от тестове
  • 0 бонус точки
  • 6 точки общо
  • 19 успешни тест(а)
  • 0 неуспешни тест(а)

Код

require 'bigdecimal'
require 'bigdecimal/util' # adds up String#to_d
#coupon.rb
class Coupon
def self.produce(name, discount = {})
if discount[:amount]
AmountCoupon.new(name, discount[:amount])
elsif discount[:percent]
PercentCoupon.new(name, discount[:percent])
else
raise "Invalid coupon parameters."
end
end
def invoice_text
"Coupon #{@name}"
end
module Inventory
def register_coupon(name, coupon = {})
raise ::Inventory::Error, "Already registered coupon." if registered_coupon? name
@coupons[name] = Coupon.produce(name, coupon)
end
def registered_coupon?(name)
@coupons.has_key? name
end
def get_coupon(name)
raise ::Inventory::Error, "Unregistered coupon name." if not registered_coupon? name
@coupons[name]
end
end
module Cart
def use(name)
@coupon = @inventory.get_coupon(name)
end
def uses_coupon?
not not @coupon # Just like !! in JavaScript
end
def coupon
@coupon
end
def coupon_discount
@coupon.discount(items_total)
end
def total
uses_coupon? ? items_total - coupon_discount : items_total
end
end
module Invoice
def coupon
row(@cart.coupon.invoice_text, '', '%.2f' % -@cart.coupon_discount)
end
end
end
class AmountCoupon < Coupon
def initialize(name, amount)
@name = name
@amount = amount.to_d
end
def discount(total)
total > @amount ? @amount : total
end
def invoice_text
super + ' - ' + ('%.2f' % @amount) + ' off'
end
end
class PercentCoupon < Coupon
def initialize(name, percent)
@name = name
@percent = percent
end
def discount(total)
total * (@percent / 100.0)
end
def invoice_text
super + ' - ' + ('%d' % @percent) + '% off'
end
end
#invoice_builder.rb
class InvoiceBuilder
include ::Coupon::Invoice
def initialize(cart)
@cart = cart
end
def build
invoice = header
invoice += items(@cart.items)
invoice += coupon if @cart.uses_coupon?
invoice += footer(@cart.total)
invoice
end
private # Private
def delimiter
"+------------------------------------------------+----------+\n"
end
def row(left, middle, right)
lpart = left.ljust(46 - middle.length) + middle
rpart = right.rjust(8)
"| #{lpart} | #{rpart} |\n"
end
def header
delimiter +
row('Name', 'qty', 'price') +
delimiter
end
def footer(total_price)
delimiter +
row('TOTAL', '', ('%.2f' % total_price.round(2))) +
delimiter
end
def items(items)
items.map { |name, item| item(name, item) }.join()
end
def item(name, item)
row = row(name, item.quantity.to_s, ('%.2f' % item.base_total.round(2)))
row += item_discount(item) if item.has_discount?
row
end
def item_discount(item)
left = " (#{item.promotion.invoice_text})"
right = '%.2f' % -item.discount.round(2)
row(left, '', right)
end
module Cart
def invoice()
builder = InvoiceBuilder.new(self)
builder.build
end
end
end
#init.rb
# Extends built-in Integer class
class Integer
def ordinal
last_digit = self % 10
postfixes = {1 => 'st', 2 => 'nd', 3 => 'rd'}
postfix = (((self / 10 == 1) and 'th') or (postfixes[last_digit] or 'th'))
self.to_s + postfix
end
end
#inventory.rb
class Inventory
include ::Coupon::Inventory
def initialize(items = [])
@items = {}
@coupons = {}
end
def new_cart()
Cart.new(self)
end
def register(name, price, discount = {})
decimal_price = BigDecimal(price)
raise ::Inventory::Error, "Already registered." if registered? name
@items[name] = Item.new(name, decimal_price, discount)
end
def registered?(name)
@items.has_key? name
end
def get_item(name)
@items[name]
end
# class Inventory::Item
class Item
MIN_PRICE = '0.01'.to_d
MAX_PRICE = '999.99'.to_d
MAX_NAME_LENGTH = 40
def initialize(name, price, discount)
self.name = name
self.price = price
self.discount = discount
end
attr_accessor :name, :price, :discount
# Override
def name=(name)
raise ::Inventory::Error, "Invalid name." if not valid_name? name
@name = name
end
def price=(price)
raise ::Inventory::Error, "Invalid price." if not valid_price? price
@price = price
end
def discount=(options)
@discount = Discount.produce(self, options)
end
private # Private
def valid_price?(price)
price >= MIN_PRICE and price <= MAX_PRICE
end
def valid_name?(name)
name.length > 0 and name.length <= MAX_NAME_LENGTH
end
class Discount
def self.produce(item, options)
{ :get_one_free => DiscountGetOneFree,
:package => DiscountPackage,
:threshold => DiscountThreshold
}.each do |option_name, cls|
option = options[option_name]
return cls.new(item, option) if option
end
nil
end
def initialize(item, option)
@item = item
@option = option
end
end
class DiscountGetOneFree < Discount
def discount_for(qty)
@item.price * (qty / @option)
end
def invoice_text
"buy #{@option - 1}, get 1 free"
end
end
class DiscountPackage < Discount
def initialize(item, option)
super
@size, @discount = @option.flatten(1)
end
def discount_for(qty)
@item.price * (qty / @size) * (@discount / 100.0) * @size
end
def invoice_text
"get #{@discount}% off for every #{@size}"
end
end
class DiscountThreshold < Discount
def initialize(item, option)
super
@threshold, @discount = @option.flatten(1)
end
def discount_for(qty)
qty > @threshold ? @item.price * (qty - @threshold) * (@discount / 100.0) : 0
end
def invoice_text
"#{@discount}% off of every after the #{@threshold.ordinal}"
end
end
end
# class Inventory::Error
class Error < RuntimeError
end
end
#cart.rb
class Cart
include ::Coupon::Cart
include ::InvoiceBuilder::Cart
def initialize(inventory)
@inventory = inventory
@items = {}
end
def add(name, qty = 1)
if not @inventory.registered? name
raise Error, "Item is not registered in the inventory"
end
if has_item? name
@items[name].add_quantity(qty)
else
add_new_item(name, qty)
end
end
def has_item?(name)
@items.has_key? name
end
def items
@items.dup
end
def items_total
@items.inject(0) { |sum, (name, item)| sum + item.total }
end
# class Cart::Item
class Item
MAX_QTY = 99
def initialize(inventory_item, qty = 1)
@inventory_item = inventory_item
self.quantity = qty
end
attr_accessor :quantity
# Override #quantity setter
def quantity=(qty)
raise ::Cart::Error, "Invalid quantity." if qty <= 0 or qty > MAX_QTY
raise ::Cart::Error, "Exceeded maximum item quantity." if qty > MAX_QTY
@quantity = qty
end
def add_quantity(qty)
self.quantity += qty
end
def name
@inventory_item.name
end
def base_price
@inventory_item.price
end
def base_total
quantity * base_price
end
def total
discounted_total = base_total
discounted_total -= discount if has_discount?
discounted_total
end
def has_discount?
not not @inventory_item.discount
end
# Promotion value
def discount
promotion.discount_for(self.quantity)
end
# Promotion itself
def promotion
@inventory_item.discount
end
end
# class Cart::Error
class Error < RuntimeError
end
private
def add_new_item(name, qty)
inventory_item = @inventory.get_item(name)
item = Item.new(inventory_item, qty)
@items[name] = item
end
end

Лог от изпълнението

...................

Finished in 0.56971 seconds
19 examples, 0 failures

История (1 версия и 12 коментара)

Марин обнови решението на 06.11.2011 00:46 (преди над 12 години)

+require 'bigdecimal'
+require 'bigdecimal/util' # adds up String#to_d
+
+#coupon.rb
+class Coupon
+ def self.produce(name, discount = {})
+ if discount[:amount]
+ AmountCoupon.new(name, discount[:amount])
+ elsif discount[:percent]
+ PercentCoupon.new(name, discount[:percent])
+ else
+ raise "Invalid coupon parameters."
+ end
+ end
+
+ def invoice_text
+ "Coupon #{@name}"
+ end
+
+ module Inventory
+ def register_coupon(name, coupon = {})
+ raise ::Inventory::Error, "Already registered coupon." if registered_coupon? name
+ @coupons[name] = Coupon.produce(name, coupon)
+ end
+
+ def registered_coupon?(name)
+ @coupons.has_key? name
+ end
+
+ def get_coupon(name)
+ raise ::Inventory::Error, "Unregistered coupon name." if not registered_coupon? name
+ @coupons[name]
+ end
+ end
+ module Cart
+ def use(name)
+ @coupon = @inventory.get_coupon(name)
+ end
+
+ def uses_coupon?
+ not not @coupon # Just like !! in JavaScript
+ end
+
+ def coupon
+ @coupon
+ end
+
+ def coupon_discount
+ @coupon.discount(items_total)
+ end
+
+ def total
+ uses_coupon? ? items_total - coupon_discount : items_total
+ end
+ end
+ module Invoice
+ def coupon
+ row(@cart.coupon.invoice_text, '', '%.2f' % -@cart.coupon_discount)
+ end
+ end
+end
+
+class AmountCoupon < Coupon
+ def initialize(name, amount)
+ @name = name
+ @amount = amount.to_d
+ end
+ def discount(total)
+ total > @amount ? @amount : total
+ end
+ def invoice_text
+ super + ' - ' + ('%.2f' % @amount) + ' off'
+ end
+end
+
+class PercentCoupon < Coupon
+ def initialize(name, percent)
+ @name = name
+ @percent = percent
+ end
+ def discount(total)
+ total * (@percent / 100.0)
+ end
+ def invoice_text
+ super + ' - ' + ('%d' % @percent) + '% off'
+ end
+end
+
+#invoice_builder.rb
+class InvoiceBuilder
+ include ::Coupon::Invoice
+
+ def initialize(cart)
+ @cart = cart
+ end
+
+ def build
+ invoice = header
+ invoice += items(@cart.items)
+ invoice += coupon if @cart.uses_coupon?
+ invoice += footer(@cart.total)
+ invoice
+ end
+
+ private # Private
+
+ def delimiter
+ "+------------------------------------------------+----------+\n"
+ end
+
+ def row(left, middle, right)
+ lpart = left.ljust(46 - middle.length) + middle
+ rpart = right.rjust(8)
+ "| #{lpart} | #{rpart} |\n"
+ end
+
+ def header
+ delimiter +
+ row('Name', 'qty', 'price') +
+ delimiter
+ end
+
+ def footer(total_price)
+ delimiter +
+ row('TOTAL', '', ('%.2f' % total_price.round(2))) +
+ delimiter
+ end
+
+ def items(items)
+ items.map { |name, item| item(name, item) }.join()
+ end
+
+ def item(name, item)
+ row = row(name, item.quantity.to_s, ('%.2f' % item.base_total.round(2)))
+ row += item_discount(item) if item.has_discount?
+ row
+ end
+
+ def item_discount(item)
+ left = " (#{item.promotion.invoice_text})"
+ right = '%.2f' % -item.discount.round(2)
+ row(left, '', right)
+ end
+
+ module Cart
+ def invoice()
+ builder = InvoiceBuilder.new(self)
+ builder.build
+ end
+ end
+end
+
+
+#init.rb
+# Extends built-in Integer class
+class Integer
+ def ordinal
+ last_digit = self % 10
+ postfixes = {1 => 'st', 2 => 'nd', 3 => 'rd'}
+ postfix = (((self / 10 == 1) and 'th') or (postfixes[last_digit] or 'th'))
+ self.to_s + postfix
+ end
+end
+
+#inventory.rb
+class Inventory
+ include ::Coupon::Inventory
+
+ def initialize(items = [])
+ @items = {}
+ @coupons = {}
+ end
+
+ def new_cart()
+ Cart.new(self)
+ end
+
+ def register(name, price, discount = {})
+ decimal_price = BigDecimal(price)
+ raise ::Inventory::Error, "Already registered." if registered? name
+ @items[name] = Item.new(name, decimal_price, discount)
+ end
+
+ def registered?(name)
+ @items.has_key? name
+ end
+
+ def get_item(name)
+ @items[name]
+ end
+
+ # class Inventory::Item
+ class Item
+ MIN_PRICE = '0.01'.to_d
+ MAX_PRICE = '999.99'.to_d
+ MAX_NAME_LENGTH = 40
+
+ def initialize(name, price, discount)
+ self.name = name
+ self.price = price
+ self.discount = discount
+ end
+
+ attr_accessor :name, :price, :discount
+
+ # Override
+ def name=(name)
+ raise ::Inventory::Error, "Invalid name." if not valid_name? name
+ @name = name
+ end
+
+ def price=(price)
+ raise ::Inventory::Error, "Invalid price." if not valid_price? price
+ @price = price
+ end
+
+
+ def discount=(options)
+ @discount = Discount.produce(self, options)
+ end
+
+ private # Private
+
+ def valid_price?(price)
+ price >= MIN_PRICE and price <= MAX_PRICE
+ end
+
+ def valid_name?(name)
+ name.length > 0 and name.length <= MAX_NAME_LENGTH
+ end
+
+
+ class Discount
+ def self.produce(item, options)
+ { :get_one_free => DiscountGetOneFree,
+ :package => DiscountPackage,
+ :threshold => DiscountThreshold
+ }.each do |option_name, cls|
+ option = options[option_name]
+ return cls.new(item, option) if option
+ end
+ nil
+ end
+
+ def initialize(item, option)
+ @item = item
+ @option = option
+ end
+ end
+
+ class DiscountGetOneFree < Discount
+ def discount_for(qty)
+ @item.price * (qty / @option)
+ end
+ def invoice_text
+ "buy #{@option - 1}, get 1 free"
+ end
+ end
+
+ class DiscountPackage < Discount
+ def initialize(item, option)
+ super
+ @size, @discount = @option.flatten(1)
+ end
+ def discount_for(qty)
+ @item.price * (qty / @size) * (@discount / 100.0) * @size
+ end
+ def invoice_text
+ "get #{@discount}% off for every #{@size}"
+ end
+ end
+
+ class DiscountThreshold < Discount
+ def initialize(item, option)
+ super
+ @threshold, @discount = @option.flatten(1)
+ end
+ def discount_for(qty)
+ qty > @threshold ? @item.price * (qty - @threshold) * (@discount / 100.0) : 0
+ end
+ def invoice_text
+ "#{@discount}% off of every after the #{@threshold.ordinal}"
+ end
+ end
+ end
+
+ # class Inventory::Error
+ class Error < RuntimeError
+
+ end
+end
+
+
+#cart.rb
+class Cart
+ include ::Coupon::Cart
+ include ::InvoiceBuilder::Cart
+
+ def initialize(inventory)
+ @inventory = inventory
+ @items = {}
+ end
+
+ def add(name, qty = 1)
+ if not @inventory.registered? name
+ raise Error, "Item is not registered in the inventory"
+ end
+ if has_item? name
+ @items[name].add_quantity(qty)
+ else
+ add_new_item(name, qty)
+ end
+ end
+
+ def has_item?(name)
+ @items.has_key? name
+ end
+
+ def items
+ @items.dup
+ end
+
+ def items_total
+ @items.inject(0) { |sum, (name, item)| sum + item.total }
+ end
+
+ # class Cart::Item
+ class Item
+ MAX_QTY = 99
+
+ def initialize(inventory_item, qty = 1)
+ @inventory_item = inventory_item
+ self.quantity = qty
+ end
+
+ attr_accessor :quantity
+
+ # Override #quantity setter
+ def quantity=(qty)
+ raise ::Cart::Error, "Invalid quantity." if qty <= 0 or qty > MAX_QTY
+ raise ::Cart::Error, "Exceeded maximum item quantity." if qty > MAX_QTY
+ @quantity = qty
+ end
+
+ def add_quantity(qty)
+ self.quantity += qty
+ end
+
+ def name
+ @inventory_item.name
+ end
+
+ def base_price
+ @inventory_item.price
+ end
+
+ def base_total
+ quantity * base_price
+ end
+
+ def total
+ discounted_total = base_total
+ discounted_total -= discount if has_discount?
+ discounted_total
+ end
+
+ def has_discount?
+ not not @inventory_item.discount
+ end
+
+ # Promotion value
+ def discount
+ promotion.discount_for(self.quantity)
+ end
+
+ # Promotion itself
+ def promotion
+ @inventory_item.discount
+ end
+
+ end
+
+ # class Cart::Error
+ class Error < RuntimeError
+
+ end
+
+ private
+
+ def add_new_item(name, qty)
+ inventory_item = @inventory.get_item(name)
+ item = Item.new(inventory_item, qty)
+ @items[name] = item
+ end
+end

Основно ме интересува, какво трябва да променя, за кода да в стил "Ruby Way". Защото, някак ми се струва, че не го правя по правилният начин и ще съм благодарен на коментар и критика.

Неща по синтаксиса и кода ти:

  • Излишните неща в Ruby не са на почит; излишно е да се ползва then с if, когато тялото на if-а е на следващия ред
  • Класови (статични) методи в Ruby почти никога не се дефинират с def SomeClass.my_method; вместо това, за целта се ползва например def self.my_method
  • Когато декларираш или викаш метод, който няма никакви аргументи, избягвай да пишеш скобите; например def build_delimiter() е по-добре като def build_delimiter
  • Спокойно може да ползваш '999.99'.to_d вместо BigDecimal('999.99')
  • Конвенцията за константи, които не са имена на класове, е MIN_PRICE, а не MinPrice
  • Скобите около условието в методи от типа на Item#valid_price? са излишни и е добре да се изпускат, например: MIN_PRICE <= price and price <= MAX_PRICE
  • Изразите в някои #discount_for методи са леко сложни за тернари-оператора, ползавй if-else в такива случаи, за да улесниш четенето на кода
  • Не виждам къде си дефинирал CartError, но на едно място го raise-ваш
  • Когато дефинираш празни изключения, идиоматично е да ги събираш на един ред: class MyError < RuntimeError; end
  • На доста места бих оставял по един празен ред за яснота, напр. м/у дефинициите на методи/модули/класове; преди израза, връщан от даден метод, ако методът не е едноредов
  • Хешът в Discount.produce е добре да изглежда така, ако ще го пишеш multiline:
{
  get_one_free: DiscountGetOneFree,
  package:      DiscountPackage,
  threshold:    DiscountThreshold,
}

Общи неща по дизайна ти:

  • Като цяло ми изглежда малко твърде сложно като дизайн и с трудно-проследима логика
  • Доста ми е странно да видя модул (че даже повече от един) в клас; обикновено класовете се намират в един или повече вложени модула, с цел създаване на именовано пространство (namespace), или пък модулите, предназначени за include-ване в други класове са си top-level (или в съответното именовано пространство на твоята библиотека, но вложени в друг клас — не)
  • Бих кръстил класовете CouponAmount и CouponPercent съответно PercentCoupon и AmountCoupon
  • Имаш страшно много build_* методи в Invoice; повторение на част от името на метод също се брои за повторение :) може би нямаше да е лоша идея тези методи да бяха изведени в някакъв клас/модул, например ReceiptBuilder, който дефинира същите неща, но без build_-префикс
  • Кодът ти ме умори :)

Благодаря за отделеното време. И съжалявам, че е бил толкова брутален кода... минах през няколко дизайна и накрая този стработи най-добре и ми изглеждаше готин (до момента). Естествено има поостанали неща, като build_, когато смятах, че методите трябва да започват с действие, и класа се наричаше InvoiceBuilder...

Като цяло if-then са остатъци или са сложени да не крашва skeptic. Иначе за съжаление, Руби няма готиния тернарен оператор на python true if cond else false, или ако го има, аз не можах да го подкарам, за това направих най-лесното.

Ще се опитам да поизчистя малко кода, но вероятно архитектурата ще си я оставя същата.

Трябва някои лекции да се променят: http://fmi.ruby.bg/lectures/05-classes-namespaces-inheritance#24 Относно дефинирането на статични методи.

http://fmi.ruby.bg/lectures/02-arrays-hashes-functions#8 Относно именоването на константите

Относно лекциите, за класови методи още не е говорено в дълбочина и затова не е демонстрирана употребата на self. Примерът от лекцията, който ти си дал, е само за "учебна" цел, преди да сме влезли в дълбочина и не отговаря на конвенцията, която се ползва на практика. Аз ти давам малко предварителни насоки :)

За константите, слайдът, към който си линкнал, си е технически правилен, няма нужда от промяна. Имената на класове също са константи и те се записват както ти ги ползваш — с CamelCase. Но конвенцията за константи, които не са имена на класове, е CAMEL_CASE.

Относно това за което питах в час: Ето горе-долу каква ми беше идеята.

class Inventory
  def initialize
    @items = {}
  end
  ...
end
module Coupon
  class Inventory
    def initialize
      super
      @coupons = {}
    end
    def register_coupon
      ...
    end
    ...
  end
end

include Coupon

Т.е. правя си клас Inventory (или Cart..) и после след като инклудна модул, да разширя класа и по възможност да презапиша метода за инициализация...

Не стана... Защо? Прицнипно очаквах, че initialize няма да бъде override-нат, но очаквах и register_coupon да се появи в класа...

Тц... стана съвсем логичното:

metala@obsidian:~/projects/ruby-fmi$ pry -r ./test2.rb

[1] pry(main)> Inventory::Inventory

=> Coupon::Inventory

[1] pry(main)> i = Inventory.new

=> #<Inventory:0x000000025a6f20 @items={}>

[2] pry(main)> i.register_coupon

NoMethodError: undefined method `register_coupon' for #<Inventory:0x000000025a6f20 @items={}>

$ pry -r ./s.rb 
pry(main)> Inventory::Inventory
(pry):1: warning: toplevel constant Inventory referenced by Inventory::Inventory
=> Inventory
pry(main)> Coupon::Inventory 
=> Coupon::Inventory
pry(main)> Inventory.new.register # Мисля че е бъг във pry
Inventory.new.register         Inventory.new.register_coupon  Inventory.new.register_for     
pry(main)> Inventory.new.register_coupon 
NoMethodError: undefined method `register_coupon' for #<Inventory:0x000000019a11d0 @items={}>
from (pry):3:in `<main>'
pry(main)> Coupon::Inventory.new.register_coupon 
"Works xD"
=> "Works xD"
pry(main)> 

Предположение : Inventory и Coupon::Inventory са два различни Класа защото са във два различни ... namespace-а ^_^

Това очевидно ще работи. Но това няма: module Coupon def initialize super @coupons = {} end def register_coupon p "Does not work X(" if not @coupons end end

class Inventory
  def initialize
    @items = {}
  end
  include Coupon
end

Но не това ми беше идеята.. Иначе предположението изглежда вярно, но все пак с include вкарвам ::Coupon::Inventory в ::

super в initialize на Inventory и ... не се изкарва (предполагам че това е идеята) pry(main)> Inventory.ancestors => [Inventory, Coupon, Object, PP::ObjectMixin, Kernel, BasicObject]

и не, вкарваш Coupon във ::, за Coupon::Inventory не знам как става защото е клас а не module :)

@Марин, Михаил е прав. Виж си ancestors-веригата на Inventory. Когато включиш модул, методите му идват по ред след методите на самия клас, т.е. той може да се разглежда като "родител" на Inventory и следователно трябва да викаш super в Inventory#initialize, а не в Coupon::Inventory#initialize.