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 :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 :foo, :bar

Defines private readers for @foo and @bar.

attr_value :foo, :bar

Defines public readers. Does not define writers, as value objects are typically immutable.

Defines object equality: two value objects of the same class with the same values are equal.

pattr_initialize :foo, :bar

Defines both initializer and private readers: shortcut for

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

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

vattr_initialize :foo, :bar

Defines initializer, public readers and value object identity: shortcut for

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

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

static_facade :fooable?, :foo

Defines a .fooable? class method that delegates to an instance method by the same name, having first provided foo as a private reader.

This is handy when a class-method API makes sense but you still want the refactorability of instance methods.

class PublishingPolicy
  static_facade :allow?, :user
  static_facade :disallow?, :user

  def allow?
    user.admin? && 
  end

  def disallow?
    !allow?
  end
end

PublishingPolicy.allow?(user)

static_facade :fooable?, :foo is a shortcut for

pattr_initialize :foo

def self.fooable?(foo)
  new(foo).fooable?
end

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

You don't have to specify arguments/readers if you don't want them: just static_facade :fooable? is also valid.

method_object :foo

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.

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.

class PriceCalculator
  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
    PriceCalculator.call(self)
  end
end

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

attr_query :foo?, :bar?

Defines query methods like foo?, which is true if (and only if) foo is truthy.

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

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.

Why not use Struct?

See: "Struct inheritance is overused"

Why not use private; attr_reader :foo?

Instead of attr_private :foo, you could do private; attr_reader :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 then with:

rake

Or to see warnings (try not to have any):

RUBYOPT=-w rake

Contributors

License

MIT