Compartilhando comportamento usando Mixins
Mixins no Ruby
Quando queremos compartilhar um comportamento entre classes geralmente usamos herança. Mas nem sempre a herança faz sentido.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Product
def initialize(name, price, weight_kg)
@name = name
@price = price
@weight = weight_kg
end
def calculate_shipping
puts "Shipping for #{@name}: $#{(@weight * 8).round(2)}"
end
def apply_discount(pct)
puts "#{@name} at #{pct}% off: $#{(@price * (1 - pct / 100.0)).round(2)}"
end
end
class Book < Product
def initialize(name, price, weight_kg, author)
super(name, price, weight_kg)
@author = author
end
end
class Vinyl < Product
def initialize(name, price, weight_kg, artist)
super(name, price, weight_kg)
@artist = artist
end
end
class Ebook < Product
def initialize(name, price)
super(name, price, 0)
end
def calculate_shipping
puts "Ebooks don't have shipping!"
end
end
book = Book.new("Sapiens", 19.90, 0.6, "Harari")
vinyl = Vinyl.new("Kind of Blue", 34.90, 0.3, "Miles Davis")
book.calculate_shipping # => Shipping for Sapiens: $4.8
book.apply_discount(10) # => Sapiens at 10% off: $17.91
vinyl.calculate_shipping # => Shipping for Kind of Blue: $2.4
Não faz sentido o Ebook herdar o método calculate_shipping de Product.
Nesses casos, podemos usar Mixins.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
module Discountable
def apply_discount(pct)
discounted = (@price * (1 - pct / 100.0)).round(2)
puts "#{@name} at #{pct}% off: $#{discounted}"
end
end
module Shippable
def calculate_shipping
puts "Shipping for #{@name}: $#{(@weight * 8).round(2)}"
end
end
module Stockable
def check_stock
puts "#{@name}: #{@stock} units in stock"
end
end
class Book
include Discountable
include Shippable
include Stockable
def initialize(name, price, weight_kg, stock, author)
@name = name
@price = price
@weight = weight_kg
@stock = stock
@author = author
end
end
class Vinyl
include Discountable
include Shippable
include Stockable
def initialize(name, price, weight_kg, stock, artist)
@name = name
@price = price
@weight = weight_kg
@stock = stock
@artist = artist
end
end
class Ebook
include Discountable
def initialize(name, price, author)
@name = name
@price = price
@author = author
end
end
book = Book.new("Sapiens", 19.90, 0.6, 15, "Harari")
vinyl = Vinyl.new("Kind of Blue", 34.90, 0.3, 4, "Miles Davis")
ebook = Ebook.new("Sapiens Digital", 9.90, "Harari")
book.calculate_shipping # => Shipping for Sapiens: $4.8
book.check_stock # => Sapiens: 15 units in stock
book.apply_discount(10) # => Sapiens at 10% off: $17.91
vinyl.calculate_shipping # => Shipping for Kind of Blue: $2.4
vinyl.check_stock # => Kind of Blue: 4 units in stock
ebook.apply_discount(20) # => Sapiens Digital at 20% off: $7.92
# ebook.calculate_shipping # => NoMethodError
Module
Você deve ter visto que usamos module ao invés de class na definição dos mixins. Módulos no Ruby são usados para agrupar métodos e constantes que se relacionam logicamente. Eles não podem ser instanciados. Também podemos usá-los para namespace e para definir métodos de classe que serão compartilhados entre classes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# warehouse/order.rb
class Order
def initialize(customer, items)
@customer = customer
@items = items
end
def to_s
"Order for #{@customer}: #{@items.join(", ")}"
end
end
#A
# storefront/order.rb
class Order
def initialize(sku, quantity)
@sku = sku
@quantity = quantity
end
def to_s
"Order for #{@sku}: #{@quantity}"
end
end
O codigo acima são dois tipos de orders diferentes e um acaba sobrescrevendo o outro. Pra resolver isso, podemos usar módulos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# storefront/order.rb
module Storefront
class Order
def initialize(sku, quantity)
@sku = sku
@quantity = quantity
end
def to_s
"Order for #{@sku}: #{@quantity}"
end
end
end
# warehouse/order.rb
module Warehouse
class Order
def initialize(customer, items)
@customer = customer
@items = items
end
def to_s
"Order for #{@customer}: #{@items.join(", ")}"
end
end
end
sf_order = Storefront::Order.new("GR-86", 3)
wh_order = Warehouse::Order.new("Alice", ["Book: Sapiens", "Vinyl: Kind of Blue"])
puts sf_order # => Order for GR-86: 3
puts wh_order # => Order for Alice: Book: Sapiens, Vinyl: Kind of Blue
Inlude, Extend e Prepend
Nos vimos uma forma de adicionar esses métodos do mixin dentro da classe usando o include. Mas ainda temos mais duas que sao o extend e o prepend.
```include`` Adiciona os métodos do módulo como métodos de instância.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module Discountable
def apply_discount(pct)
puts "#{@name} at #{pct}% off: $#{(@price * (1 - pct / 100.0)).round(2)}"
end
end
class Book
include Discountable
def initialize(name, price)
@name = name
@price = price
end
end
book = Book.new("Sapiens", 19.90)
book.apply_discount(10) # => Sapiens at 10% off: $17.91
Book.apply_discount(10) # => NoMethodError
Book.ancestors
# => [Book, Discountable, Object, Kernel, BasicObject]
# ^^^^^^^^^^^^
Book → Discountable → Object → Kernel → BasicObject extend Adiciona os métodos do módulo como métodos de classe (na classe singleton). Instâncias não os recebem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module Searchable
def find_by_name(name)
puts "Querying database for '#{name}'..."
end
def all
puts "Fetching all records..."
end
end
class Book
extend Searchable
def initialize(name)
@name = name
end
end
Book.find_by_name("Sapiens")
Book.all
book = Book.new("Sapiens")
book.find_by_name("Sapiens")
vinyl = Object.new
vinyl.extend(Searchable)
vinyl.find_by_name("Kind of Blue")
Book (singleton) → Searchable → Class → Object
No prepend o módulo é colocado na frente da classe na hierarquia de métodos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module Loggable
def display
puts "--- Iniciando Log ---" # 1. Executa antes
super # 2. Chama o método original
puts "--- Finalizando Log ---" # 3. Executa depois
end
end
class Report
prepend Loggable # Prependendo o módulo
def display
puts "Conteúdo do Relatório"
end
end
report = Report.new
report.display
# Saída:
# --- Iniciando Log ---
# Conteúdo do Relatório
# --- Finalizando Log ---
# Verificando a cadeia de ancestrais
p Report.ancestors
# Saída: [Loggable, Report, Object, Kernel, BasicObject]