Build status

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

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.

pattr_initialize

attr_private_initialize

pattr_initialize :foo, :bar defines both initializer and private readers. Shortcut for:

attr_initialize :foo, :bar
attr_private :foo, :bar

pattr_initialize is aliased as attr_private_initialize if you prefer a longer but clearer name.

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

attr_value_initialize

vattr_initialize :foo, :bar defines initializer, public readers and value object identity. Shortcut for:

attr_initialize :foo, :bar
attr_value :foo, :bar

vattr_initialize is aliased as attr_value_initialize if you prefer a longer but clearer name.

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!]

rattr_initialize

attr_reader_initialize

rattr_initialize :foo, :bar defines both initializer and public readers. Shortcut for:

attr_initialize :foo, :bar
attr_reader :foo, :bar

rattr_initialize is aliased as attr_reader_initialize if you prefer a longer but clearer name.

Example:

class PublishBook
  rattr_initalize :book_name, :publisher_backend

  def call
    publisher_backend.publish book_name
  end
end

service = PublishBook.new("A Novel")
service.book_name  # => "A Novel"

The attr_initialize notation for hash arguments is also supported: rattr_initialize :foo, [:bar, :baz!]

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

Contributors

License

MIT