attr_extras
Takes some boilerplate out of Ruby, lowering the barrier to extracting small focused classes, without the downsides of using Struct.
Instead of
class InvoiceBuilder
def initialize(invoice, employee)
@invoice, @employee = invoice, employee
end
private
attr_reader :invoice, :employee
end
you can just do
class InvoiceBuilder
pattr_initialize :invoice, :employee
end
This nicely complements Ruby's built-in attr_accessor, attr_reader and attr_writer.
Supports positional arguments as well as optional and required hash arguments.
Also provides conveniences for creating value objects, method objects, query methods and abstract methods.
Usage
pattr_initializevattr_initializeattr_initializeattr_privateattr_valuestatic_facademethod_objectattr_implementattr_queryattr_id_query
pattr_initialize
pattr_initialize :foo, :bar defines both initializer and private readers: shortcut for
attr_initialize :foo, :bar
attr_private :foo, :bar
Example:
class Item
pattr_initalize :name, :price
def price_with_vat
price * 1.25
end
end
Item.new("Pug", 100).price_with_vat # => 125.0
The attr_initialize notation for hash arguments is also supported: pattr_initialize :foo, [:bar, :baz!]
vattr_initialize
vattr_initialize :foo, :bar defines initializer, public readers and value object identity: shortcut for
attr_initialize :foo, :bar
attr_value :foo, :bar
Example:
class Country
vattr_initialize :code
end
Country.new("SE") == Country.new("SE") # => true
Country.new("SE").code # => "SE"
The attr_initialize notation for hash arguments is also supported: vattr_initialize :foo, [:bar, :baz!]
attr_initialize
attr_initialize :foo, :bar defines an initializer that takes two arguments and assigns @foo and @bar.
attr_initialize :foo, [:bar, :baz!] defines an initializer that takes one regular argument, assigning @foo, and one hash argument, assigning @bar (optional) and @baz (required).
attr_initialize [:bar, :baz!] defines an initializer that takes one hash argument, assigning @bar (optional) and @baz (required).
attr_initialize can also accept a block which will be invoked after initialization. This is useful for calling super appropriately in subclasses or initializing private data as necessary.
attr_private
attr_private :foo, :bar defines private readers for @foo and @bar.
attr_value
attr_value :foo, :bar defines public readers for @foo and @bar and also defines object equality: two value objects of the same class with the same values will be considered equal (with == and eql?, in Sets, as Hash keys etc).
It does not define writers, because value objects are typically immutable.
static_facade
static_facade :allow?, :user defines an .allow? class method that delegates to an instance method by the same name, having first provided user as a private reader.
This is handy when a class-method API makes sense but you still want the refactorability of instance methods.
Example:
class PublishingPolicy
static_facade :allow?, :user
def allow?
user.admin? && complicated_extracted_method
end
private
def complicated_extracted_method
# …
end
end
PublishingPolicy.allow?(user)
static_facade :allow?, :user is a shortcut for
pattr_initialize :user
def self.allow?(user)
new(user).allow?
end
The attr_initialize notation for hash arguments is also supported: static_facade :allow?, :user, [:user_agent, :ip!]
You don't have to specify arguments/readers if you don't want them: just static_facade :tuesday? is also valid.
"Static façade" is the least bad name for this pattern we've come up with. Suggestions are welcome.
method_object
NOTE: v4.0.0 made a breaking change! static_facade does exactly what method_object used to do; the new method_object no longer accepts a method name argument.
method_object :foo defines a .call class method that delegates to an instance method by the same name, having first provided foo as a private reader.
This is a special case of static_facade for when you want a Method Object, and the class name itself will communicate the action it performs.
Example:
class CalculatePrice
method_object :order
def call
total * factor
end
private
def total
order.items.map(&:price).inject(:+)
end
def factor
1 + rand
end
end
class Order
def price
CalculatePrice.call(self)
end
end
You could even do CalculatePrice.(self) if you like, since we're using the call convention.
method_object :foo is a shortcut for
static_facade :call, :foo
which is a shortcut for
pattr_initialize :foo
def self.call(foo)
new(foo).call
end
The attr_initialize notation for hash arguments is also supported: method_object :foo, [:bar, :baz!]
You don't have to specify arguments/readers if you don't want them: just method_object is also valid.
attr_implement
attr_implement :foo, :bar defines nullary (0-argument) methods foo and bar that raise e.g. "Implement a 'foo()' method".
attr_implement :foo, [:name, :age] will define a binary (2-argument) method foo that raises "Implement a 'foo(name, age)' method".
This is suitable for abstract methods in base classes, e.g. when using the template method pattern.
It's more or less a shortcut for
def my_method
raise "Implement me in a subclass!"
end
though it is shorter, more declarative, gives you a clear message and handles edge cases you might not have thought about (see tests).
attr_query
attr_query :foo?, :bar? defines query methods like foo?, which is true if (and only if) foo is truthy.
attr_id_query
attr_id_query :foo?, :bar? defines query methods like foo?, which is true if (and only if) foo_id is truthy. Goes well with Active Record.
Philosophy
Findability is a core value.
Hence the long name attr_initialize, so you see it when scanning for the initializer;
and the enforced questionmarks with attr_id_query :foo?, so you can search for that method.
Q & A
Why not use Struct instead of pattr_initialize?
See: "Struct inheritance is overused"
Why not use private; attr_reader :foo instead of attr_private :foo?
Other than being more to type, declaring attr_reader after private will actually give you a warning (deserved or not) if you run Ruby with warnings turned on.
If you don't want the dependency on attr_extras, you can get rid of the warnings with attr_reader :foo; private :foo. Or just define a regular private method.
Installation
Add this line to your application's Gemfile:
gem "attr_extras"
And then execute:
bundle
Or install it yourself as:
gem install attr_extras
Running the tests
Run them with:
rake
Or to see warnings (try not to have any):
RUBYOPT=-w rake
