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