-
-
Save pricees/8963659 to your computer and use it in GitHub Desktop.
names = %w[SK Steve Pedro Anthony Milo Xiping] | |
Person = Struct.new :name | |
module Helloable | |
def hello_all_the_names | |
each { |person| puts "Helloable | Hello #{person.name}" } | |
end | |
end | |
# An interesting way to add functionality to an array is to extend it | |
ary = names.map { |name| Person.new name } | |
ary.extend(Helloable) | |
ary.hello_all_the_names | |
# Prints: | |
# Helloable | Hello SK | |
# Helloable | Hello Steve | |
# Helloable | Hello Pedro | |
# Helloable | Hello Anthony | |
# Helloable | Hello Milo | |
# Helloable | Hello Xiping | |
# My preference would be to subclass an array, it costs less | |
class HelloArray < Array | |
def hello_all_the_names | |
each { |person| puts "HelloArray | Hello #{person.name}" } | |
end | |
end | |
hello_ary = HelloArray.new(names.map { |name| Person.new name }) | |
hello_ary.hello_all_the_names | |
# Prints: | |
# HelloArray | Hello SK | |
# HelloArray | Hello Steve | |
# HelloArray | Hello Pedro | |
# HelloArray | Hello Anthony | |
# HelloArray | Hello Milo | |
# HelloArray | Hello Xiping | |
# Whats the point? | |
# | |
# Well we have code here | |
# | |
# | |
# | |
# /app/models/transaction.rb | |
# | |
class Transaction < ActiveRecord::Base | |
# | |
# [...] | |
# | |
module Balances | |
def sold | |
map{ |tx| tx.sold_amount }.sum | |
end | |
def sold_pending | |
map{ |tx| tx.sold_pending_amount }.sum | |
end | |
def sold_available | |
map{ |tx| tx.sold_available_amount }.sum | |
end | |
def payments | |
map{ |tx| tx.payments_amount }.sum | |
end | |
def payments_requested | |
map{ |tx| tx.payments_requested_amount }.sum | |
end | |
def payments_paid | |
map{ |tx| tx.payments_paid }.sum | |
end | |
def balance | |
sold - payments_requested - payments_paid | |
end | |
def available_balance | |
sold_available - payments_requested - payments_paid | |
end | |
def real_time_balance(id) | |
select { |tx| tx.id <= id && tx.count_in_total? }.map(&:amount_with_sign).sum | |
end | |
end | |
end | |
# Which are used to extend ActiveRelations | |
#app/controllers/user_transactions_finder.rb: @transactions.extend Transaction::Balances | |
#app/models/line_item_decorator.rb: include Transaction::Balances | |
#app/models/payment_transaction.rb: transactions.extend Transaction::Balances | |
# | |
# Extending an instance over and over wastes clock cycles. | |
# If we choose to make Balances an array subclass we can used it like so: | |
class Transaction < ActiveRecord::Base | |
module Balances < Array | |
end | |
end | |
transactions = Transaction::Balances.new transactions | |
@transactions = Transaction::Balances.new @transactions | |
# Now we do include it in a relationship in line item decorator. | |
# This might justify keep the balances in module form, however I might then | |
# decide to make: | |
# | |
# class BalancesArray < Array | |
# include Transaction:Balances | |
# end | |
# | |
# Either way, I think extending instances probably more of an anti-pattern that | |
# we should avoid. |
This kinda reminds me of what Refinements were intended for.
However, while these look like functions, they intrinsically depend on an interface and the state of a specific object. This already puts it into two possible categories:
- A Subclass that inherits the functionality and adds to it while carrying state.
- A Service object that does processing on a single object without having internal state.
Extending an individual instance is not terrible and with Ruby 2.0 the extend
operation is no longer ridiculously inefficient (it is, however, for the 1.9.x series). If you're going to use it on multiple instances, then a subclass is cleaner and more efficient anyway. Additionally, it significantly impairs debugging in practice since most debugging tools do not take instance extensions into account.
Personally I have no issue with subclassing core and standard library classes and in fact encourage it. The temptation towards primitive obsession as opposed to using domain specific (or even generalized) value or entity objects is very strong. However subclassing a primitive and adding your domain's functionality to it can be a very quick-to-the-finish line approach without causing complete hell when you refactor later. Also it means you aren't cluttering up the global namespace (like ActiveSupport does) while making the object reusable and testable.
I am less of a fan of extending core, standard, or library classes since its fragile. A Subclass is more defensible since its not polluting the global namespace, but a special purpose Service object with a specific set of possible operations is even better for most cases.
This looks really good; I like the direction of the discussion. The use of extend
or include
is a subtle, hidden way of implementing inheritance, even if it's only on a single instance. Back in GOF days, the maxim of the day was "prefer composition over inheritance", as that leads to small, focused objects that perform a small subset of functionality, and can be assembled in innovative ways.
An even better argument is how cryptic the Transaction::Balances
functionality has become. I had originally implemented the idea, and I don't recall why I chose extended
over Balance.new(array)
.
Agree. IMO Balance can even be a standalone class called BalanceCalculator.