ActiveShepherd
Is your app/models directory growing unweildy? Do you find yourself desiring the notion of aggregates to help corral your less important models under the umbrella of more important "business entities?" That's the problem I had that led me to write this gem. I wanted to be able to reason about an entire namespace of models as one thing; or an "aggregate" in enterprisey development parlance.
My main goal was to be able to keep using ActiveRecord and intrude on it as little as possible. The result was an approach that requires you to wire up your models a bit more strictly -- you need to be setting options like dependent: 'destroy'
, autosave: true
, and inverse_of
on all associations to the sub objects. The benefit you get from this gem is to be able to both query and manipulate the state of the entire aggregate all at once.
There are more requirements that are outlined by Eric Evans in his brilliant Domain Driven Design book, whose self titled concept is still very new to me.
Installation
Add this line to your application's Gemfile:
gem 'activeshepherd'
And then execute:
$ bundle
Or install it yourself as:
$ gem install activeshepherd
In your config/initializers
directory, add a tiny shim into ActiveRecord::Base:
ActiveShepherd.enable!(ActiveRecord::Base)
Usage
- Pick a model you'd like to make into an aggregate root
- Add
act_as_aggregate_root!
to the model, e.g.: end - Make sure it follows the rules (e.g. see this blog post)
- ??
- Profit!
Examples:
See the test suite for more fleshed out examples. For now, say you have two models:
# app/models/my_model.rb
class MyModel < ActiveRecord::Base
act_as_aggregate_root!
has_many :bunnies, autosave: true, dependent: :destroy, inverse_of: :my_model,
validate: true
end
# app/models/my_model/bunny.rb
class MyModel::Bunny < ActiveRecord::Base
belongs_to :my_model, inverse_of: :bunnies, touch: true
end
Now add a test to make sure your models always meet the requirements for being an aggregate root:
# spec/models/my_model_spec.rb
describe MyModel do
it "is an aggregate root" do
MyModel.should be_able_to_act_as_aggregate_root
end
end
# test/unit/my_model_test.rb
class MyModel::TestCase < Minitest::Unit::TestCase
def test_should_be_aggregate_root
assert MyModel.able_to_act_as_aggregate_root?
end
end
You now get some new behavior on MyModel that will let you deal with the entire aggregate nicely:
>> @my_model = MyModel.new
>> @my_model.bunnies.build({ name: "Roger"})
>> @my_model.save
# Nothing new, right? wrong.
>> @my_model.aggregate_state
=> {
bunnies: [
{ name: "Roger" }
]
}
# Sweet, what about changes?
>> @my_model.bunnies.first.name = "Roger Rabbit"
>> @my_model.bunnies.build({ name: "Energizer" })
# BAM!
>> @my_model.aggregate_changes
=> {
bunnies: {
0 => { name: ["Roger", "Roger Rabbit"] },
1 => { name: [nil, "Energizer"] }
}
}
So #aggregate_changes
is just like ActiveRecord's #changes
, except it includes all of the nested changes within the aggregate.
That's a brief description of what this gem does. Here are the main methods that acts_as_aggregate_root!
brings to your ActiveRecord models:
Method name | Description |
---|---|
#aggregate_state |
Serializes the entire state of the aggregate |
#aggregate_state= |
Takes a serialized blob and uses it to set the entire state of the aggregate |
#aggregate_changes |
Analagous to #changes ; it tells you what all has changes in the entire aggregate |
#aggregate_changes= |
Takes an existing set of changes and applies it to the aggregate |
Todo
This project is way alpha right now, hence the "eat-my-babies" project name.
- Implement
ClassValidator
which will correctly tell you if a class can be an aggregate root (e.g. are your associations wired up correctly?) - Implement
ChangeValidator
that adds a little more niceness around#aggregate_changes=
My main goal right now is to use the code as it exists for a while and deal with problems as they arise. Consider the entire gem incomplete for right now.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request