Policy
A tiny library to implement a Policy Object pattern.
The gem was inspired by:
- the CodeClimate's blog post "7 ways to decompose fat ActiveRecord module"
- the part "How to Model Less Obvious Kinds of Concept" from the "Domain-Driven Design" by Eric Evans.
A Policy Object encapsulates a business rule in isolation from objects (such as entities or services) following it.
This separation provides a number of benefits:
- It makes business rules explicit instead of spreading and hiding them inside application objects.
- It makes the rules reusable in various context (think of the transaction consistency both in bank transfers and cach machine withdrawals).
- It allows definition of rules for numerous attributes that should correspond to each other in some way.
- It makes complex rules testable in isolation from even more complex objects.
Installation
Add this line to your application's Gemfile:
gem "policy"
And then execute:
$ bundle
Or install it yourself as:
$ gem install policy
Usage
The Model for Illustration
Suppose an over-simplified model of bank account transactions and account-to-account transfers.
# The account transaction (either enrollment or witdrawal)
class Transaction < Struct.new(:sum); end
# The transfer, connecting two separate transactions
# (maybe this isn't an optimal model, but helpful for the subject)
class Transfer < Struct.new(:withdrawal, :enrollment); end
What we need is to apply the simple policy (invariant):
The sum of withdrawal's and enrollment's sums should be 0.
Let's do it with Policy Objects!
Policy Declaration
Define policies with a list of necessary attributes like using Struct.
Tnen use ActiveModel::Validations methods to describe its rules:
# An arbitrary namespace for financial policies
module Policies::Financial
# Withdrawal from one account should be equal to enrollment to another
class Consistency < Policy.new(:withdrawal, :enrollment)
validates :withdrawal, :enrollment, presence: true
validates :total_sum, numericality: { equal_to: 0 }
private
def total_sum
withdrawal.sum + enrollment.sum
end
end
end
Note a policy knows nothing about the complex nature of its attributes until their quack like transactions with #sum
method defined.
Scaffolding a Policy
You can scaffold the policy with its specification and necessary translations using the generator:
policy new
For a list of available options call the generator with a -h
option:
policy new -h
Following a Policy
Include the Policy::Follower
module to the class and apply policies to corresponding attributes with follow_policy
class method.
class Transfer < Struct.new(:withdrawal, :enrollment)
include Policy::Follower # also includes ActiveModel::Validations
follow_policy Policies::Financial::Consistency, :withdrawal, :enrollment
end
The order of attributes should correspond to the policy definition.
You can swap attributes (this is ok for our example)...
follow_policy Policies::Financial::Consistency, :enrollment, :withdrawal
...or use the same attribute several times when necessary (not in our example, though):
follow_policy Policies::Financial::Consistency, :withdrawal, :withdrawal
Applied policies can be grouped by namespaces (useful when the object should follow many policies):
use_policies Policies::Financial do
follow_policy :Consistency, :withdrawal, :enrollment
end
Policies Verification
To verify object use #follow_policies?
or #follow_policies!
instance methods.
Transaction = Struct.new(:account, :sum)
withdrawal = Transaction.new(account_1, -100)
enrollment = Transaction.new(account_2, 1000)
transfer = Transfer.new withdrawal, enrollment
transfer.follow_policies?
# => false
transfer.follow_policies!
# => raises <Policy::ViolationError>
The policies are verified one-by-one until the first break - in just the same order they were declared.
Asyncronous Verification
Define names for policies using as:
option. The names should be unique in the class' scope:
class Transfer < Struct.new(:withdrawal, :enrollment)
include Policy::Follower
use_policies Policies::Financial do
follow_policy :Consistency, :withdrawal, :enrollment, as: :consistency
end
end
Check policies by names (you can also use singular forms follow_policy?
and follow_policy!
):
# Checks only consistency and skips all other policies
transfer.follow_policy? :consistency
transfer.follow_policy! :consistency
The set of policies can be checked at once:
transaction.follow_policies? :consistency, ...
Now the policies are verified one-by-one in given order (it may differ from the order of policies declaration) until the first break.
Compatibility
Tested under rubies, compatible with MRI 2.0+:
- MRI rubies 2.0+
- Rubinius 2+ (2.0+ mode)
- JRuby head (2.0+ mode)
Rubies with API 1.9 are not supported.
Uses ActiveModel::Validations - tested for 3.1+
Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.
Contributing
- Fork the project.
- Read the STYLEGUIDE.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
License
See MIT LICENSE.