README.md in policy-1.2.0 vs README.md in policy-2.0.0
- old
+ new
@@ -15,32 +15,40 @@
[travis]: https://travis-ci.org/nepalez/policy
[inch]: https://inch-ci.org/github/nepalez/policy
A tiny library to implement a **Policy Object pattern**.
+**NOTE** the gem was re-written from scratch in v2.0.0 (see Changelog section below)
+
+Introduction
+------------
+
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.
+* the CodeClimate's blog post "[7 ways to decompose fat ActiveRecord module]".
+* the Chapter 10 of the book "[Domain-Driven Design]" by Eric Evans.
-A **Policy Object** encapsulates a business rule in isolation from objects (such as entities or services) following it.
+A **Policy Object** (assertion, invariant) encapsulates a business rule in isolation from objects (such as entities or services) following it.
-This separation provides a number of benefits:
+Policy Objects can be combined by logical operators `and`, `or`, `xor`, `not` to provide complex policies.
+This approach gives 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.
+* It allows definition of rules for **numerous attributes** at once that should correspond to each other in some way.
+* It makes the rules **simple** and **reusable** in various context and combinations.
+* It makes complex rules **testable** in isolation from their parts.
[7 ways to decompose fat ActiveRecord module]: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
[Domain-Driven Design]: http://www.amazon.com/dp/B00794TAUG/
-# Installation
+Installation
+------------
Add this line to your application's Gemfile:
```ruby
-gem "policy"
+gem "policy", ">= 1.0"
```
And then execute:
```
@@ -51,166 +59,255 @@
```
$ gem install policy
```
-# Usage
+Usage
+-----
-## The Model for Illustration
+### The Model for Illustration
Suppose an over-simplified model of bank account transactions and account-to-account transfers.
```ruby
-# The account transaction (either enrollment or witdrawal)
-class Transaction < Struct.new(:sum); end
+# The account has a limit
+class Account < Struct.new(:owner, :limit); end
+# The transaction belongs to account and has a sum (< 0 for withdrawals)
+class Transaction < Struct.new(:account, :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):
+What we need is to apply set of policies:
**The sum of withdrawal's and enrollment's sums should be 0.**
+**The sum of withdrawal doesn't exceed the accounts' limit.**
+**The sum of transfers between client's own accounts can exceed the limit.**
Let's do it with Policy Objects!
-## Policy Declaration
+### Policy Declaration
-Define policies with a list of necessary attributes like using [Struct].
+Define policies with `Policy::Base` module included. Tnen use [ActiveModel::Validations] methods to describe its rules:
-Tnen use [ActiveModel::Validations] methods to describe its rules:
-
```ruby
# An arbitrary namespace for financial policies
-module Policies::Financial
+module Policies
# Withdrawal from one account should be equal to enrollment to another
- class Consistency < Policy.new(:withdrawal, :enrollment)
+ class Consistency < Struct.new(:withdrawal, :enrollment)
+ include Policy::Base
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.
+ # The sum of withdrawal doesn't exceed the accounts' limit
+ class Limited < Struct.new(:withdrawal)
+ include Policy::Base
-### Scaffolding a Policy
+ validate :not_exceeds_the_limit
-You can scaffold the policy with its specification and necessary translations using the generator:
+ private
-```
-policy new
-```
+ def not_exceeds_the_limit
+ return if withdrawal.sum + withdrawal.limit > 0
+ errors.add :base, :exceeds_the_limit
+ end
+ end
-For a list of available options call the generator with a `-h` option:
+ # The transfer is made between client's own accounts
+ class InternalTransfer < Struct.new(:withdrawal, :enrollment)
+ include Policy::Base
+ validate :the_same_client
+
+ private
+
+ def the_same_client
+ return if withdrawal.account.owner == enrollment.account.owner
+ errors.add :base, :different_owners
+ end
+ end
+
+end
```
-policy new -h
-```
[Struct]: http://ruby-doc.org//core-2.2.0/Struct.html
[ActiveModel::Validations]: http://apidock.com/rails/ActiveModel/Validations
-## Following a Policy
+### Combining Policies
-Include the `Policy::Follower` module to the class and apply policies to corresponding attributes with `follow_policy` **class** method.
+Use `and`, `or`, `xor` instance methods to provide complex policies from elementary ones.
+You can use factory methods:
+
```ruby
-class Transfer < Struct.new(:withdrawal, :enrollment)
- include Policy::Follower # also includes ActiveModel::Validations
+module Policies
- follow_policy Policies::Financial::Consistency, :withdrawal, :enrollment
+ module LimitedOrInternal
+ def self.new(withdrawal, enrollment)
+ InternalTransfer.new(withdrawal, enrollment).or Limited.new(withdrawal)
+ end
+ end
+
end
```
-The order of attributes should correspond to the policy definition.
+As an alternative to instance methods, use the `Policy` module's methods:
-You can swap attributes (this is ok for our example)...
-
```ruby
-follow_policy Policies::Financial::Consistency, :enrollment, :withdrawal
+def self.new(withdrawal, enrollment)
+ Policy.or(
+ InternalTransfer.new(withdrawal, enrollment),
+ Limited.new(withdrawal)
+ )
+end
```
-...or use the same attribute several times when necessary (not in our example, though):
+To provide negation use `and.not`, `or.not`, `xor.not` syntax:
```ruby
-follow_policy Policies::Financial::Consistency, :withdrawal, :withdrawal
+first_policy.and.not(second_policy, third_policy)
+
+# this is equal to:
+Policy.and(first_policy, Policy.not(second_policy), Policy.not(third_policy))
```
-Applied policies can be grouped by namespaces (useful when the object should follow many policies):
+Policies can composed at any number of levels.
+### Following Policies
+
+Include the `Policy::Follower` module to the policies follower class.
+
+Use the **class** method `.follows_policies` to declare policies (like ActiveModel::Validations `.validate` method does).
+
```ruby
-use_policies Policies::Financial do
- follow_policy :Consistency, :withdrawal, :enrollment
+class Transfer < Struct.new(:withdrawal, :enrollment)
+ include Policy::Follower
+
+ follows_policies :consistent, :limited_or_internal
+
+ private
+
+ def consistent
+ Policies::Consistency.new(withdrawal, enrollment)
+ end
+
+ def limited_or_internal
+ Policies::LimitedOrInternal.new(withdrawal, enrollment)
+ end
+
end
```
-## Policies Verification
+Surely, you can skip creating `LimitedOrInternal` builder and combine policies for current class only:
-To verify object use `#follow_policies?` or `#follow_policies!` **instance** methods.
-
```ruby
-Transaction = Struct.new(:account, :sum)
-withdrawal = Transaction.new(account_1, -100)
-enrollment = Transaction.new(account_2, 1000)
+def limited_or_internal
+ limited.or internal
+end
-transfer = Transfer.new withdrawal, enrollment
+def limited
+ Policies::Limited.new(withdrawal)
+end
-transfer.follow_policies?
-# => false
-
-transfer.follow_policies!
-# => raises <Policy::ViolationError>
+def internal
+ Policies::Internal.new(withdrawal, enrollment)
+end
```
-The policies are verified one-by-one until the first break - in just the same order they were declared.
+[builder]: http://sourcemaking.com/design_patterns/builder
-### Asyncronous Verification
+### Checking Policies
-Define names for policies using `as:` option. The names should be unique in the class' scope:
+Use the **instance** method `follow_policies?` to check whether an instance follows policies.
+The method checks all policies and **raises** the `Policy::ViolationError` when the first followed policy is broken.
+
```ruby
-class Transfer < Struct.new(:withdrawal, :enrollment)
- include Policy::Follower
+transfer = Transfer.new(
+ Transaction.new(Account.new("Alice", 50), -100),
+ Transaction.new(Account.new("Bob", 50), 100)
+)
- use_policies Policies::Financial do
- follow_policy :Consistency, :withdrawal, :enrollment, as: :consistency
- end
-end
+transfer.follow_policies?
+# => <Policy::ViolationError ... > because Alice's limit of 50 is exceeded
```
-Check policies by names (you can also use singular forms `follow_policy?` and `follow_policy!`):
+The method doesn't mutate the follower. It collects errors inside the exception `#errors` method, not the follower's one.
```ruby
-# Checks only consistency and skips all other policies
-transfer.follow_policy? :consistency
-transfer.follow_policy! :consistency
+begin
+ transfer.follow_policies?
+rescue ViolationError => err
+ err.errors
+end
```
-The set of policies can be checked at once:
+You can check subset of policies by calling the method with policy names:
```ruby
-transaction.follow_policies? :consistency, ...
+transfer.follow_policies? :consistent
+# passes because the transfer is consistent: -100 + 100 = 0
+# this doesn't check the :limited_or_internal policy
```
-Now the policies are verified one-by-one in **given order** (it may differ from the order of policies declaration) until the first break.
+The method ignores policies, not declared by `.follows_policies` class method.
-# Compatibility
+The method has singular alias `follow_policy?(name)` that accepts one argument.
+Scaffolding
+-----------
+
+You can scaffold the policy with its specification and necessary translations using the generator:
+
+```
+policy new consistency -n policies financial -a withdrawal enrollment -l fr de
+```
+
+For a list of available options call the generator with an `-h` option:
+
+```
+policy new -h
+```
+
+Changelog
+---------
+
+Version 2 was redesigned and rewritten from scratch. The main changes are:
+
+* Instead of building policy with a `Policy.new` method, it is now created by including the `Policy::Base` module.
+
+ In the previous version building a policy was needed to define an order of policy attributes. Now the definition of policy attributes is not the responsibility of the gem.
+
+* Instead of generating policies in a class scope (in the ActiveModel `validates` style), the `.follows_policy` refers to followers' instance methods (in the ActiveModel `validate` style).
+
+ This allows combining policy objects with logical expressions. Policies themselves becames more DRY, granular and testable in isolation.
+
+* Instead of mutating the follower, `follow_policy?` method raises an exception.
+
+ This allows follower to be immutable (frozen). The follower doesn't need to be messed with `ActiveModule::Validations` at all.
+
+ This approach makes `follow_policy!` method unnecessary.
+
+Compatibility
+-------------
+
Tested under rubies, compatible with MRI 2.0+:
* MRI rubies 2.0+
* Rubinius 2+ (2.0+ mode)
-* JRuby head (2.0+ mode)
+* JRuby 9000 (2.0+ mode)
Rubies with API 1.9 are not supported.
Uses [ActiveModel::Validations] - tested for 3.1+
@@ -218,11 +315,12 @@
[RSpec]: http://rspec.info/
[hexx-suit]: https://github.com/nepalez/hexx-suit/
[ActiveModel::Validations]: http://apidock.com/rails/v3.1.0/ActiveModel/Validations
-# Contributing
+Contributing
+------------
* Fork the project.
* Read the [STYLEGUIDE](config/metrics/STYLEGUIDE).
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a
@@ -230,8 +328,9 @@
* 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
+License
+-------
See [MIT LICENSE](LICENSE).