Skip to content

Instantly share code, notes, and snippets.

@pricees
Created February 12, 2014 20:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pricees/8963659 to your computer and use it in GitHub Desktop.
Save pricees/8963659 to your computer and use it in GitHub Desktop.
Extending an Array/ActiveRelation vs Subclassing Array
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.
@BreadPaPa
Copy link

Agree. IMO Balance can even be a standalone class called BalanceCalculator.

@acook
Copy link

acook commented Feb 12, 2014

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.

@milotodorovich
Copy link

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment