README.md in surrounded-0.9.0 vs README.md in surrounded-0.9.1

- old
+ new

@@ -134,10 +134,40 @@ ``` Trigger methods are different from regular instance methods in that they apply behaviors from the roles to the role players. A regular instance method just does what you define. But a trigger will make your role players come alive with their behaviors. +You may find that the code for your triggers is extremely simple and is merely creating a method to tell a role player what to do. If you find you have many methods like this: + +```ruby + def plan_weekend_work + employee.work_weekend + end + trigger :plan_weekend_work +``` + +You can shorten it to: + +```ruby + trigger :plan_weekend_work do + employee.work_weekend + end +``` + +But it can be even simpler and follows the same pattern provided by Ruby's standard library Forwardable: + +```ruby + # The first argument is the role to receive the messaged defined in the second argument. + # The third argument is optional and if provided will be the name of the trigger method on your context instance. + forward_trigger :employee, :work_weekend, :plan_weekend_work + + # Alternatively, you can use an API similar to that of the `delegate` method from Forwardable + forwarding [:work_weekend] => :employee +``` + +The difference between `forward_trigger` and `forwarding` is that the first accepts an alternative method name for the context instance method. There's more on this below in the "Overview in code" section, or see `lib/surrounded/context/forwarding.rb`. + There's one last thing to make this work. ## Getting your role players ready You'll need to include `Surrounded` in the classes of objects which will be role players in your context. @@ -449,15 +479,15 @@ Alternatively, if you just want to define your own methods without the DSL using `disallow`, you can just follow the pattern of `disallow_#{method_name}?` when creating your own protection. In fact, that's exactly what happens with the `disallow` keyword. After using it here, we'd have a `disallow_plan_weekend_work?` method defined. -If you call the disallowed trigger directly, you'll raise a `Employment::AccessError` exception and the code in your trigger will not be run. You may rescue from that or you may rescue from `Surrounded::Context::AccessError` although you should prefer to use the error name from your own class. +If you call the disallowed trigger directly, you'll raise an `Employment::AccessError` exception and the code in your trigger will not be run. You may rescue from that or you may rescue from `Surrounded::Context::AccessError` although you should prefer to use the error name from your own class. ## Restricting return values -_Tell, Don't Ask_ style programming can better be enforced by following East-oriented Code principles. This means that the returns values from methods on your objects should not provide information about their internal state. Instead of returning values, you can enforce that triggers return the context object. This forces you to place context responsiblities inside the context and prevents leaking the details and responsiblities outside of the system. +_Tell, Don't Ask_ style programming can better be enforced by following East-oriented Code principles. This means that the return values from methods on your objects should not provide information about their internal state. Instead of returning values, you can enforce that triggers return the context object. This forces you to place context responsiblities inside the context and prevents leaking the details and responsiblities outside of the system. Here's how you enforce it: ```ruby class Employment @@ -596,10 +626,14 @@ # or handle it yourself def initialize(activator, account) # this must be done to handle the mapping of roles to objects # pass an array of arrays with role name symbol and the object for that role map_roles([[:activator, activator],[:account, account]]) + + # or load extra objects, perform other functions, etc. if you need and then use super + account.perform_some_funtion + super end # these also must be done if you create your own initialize method. # this is a shortcut for using attr_reader and private private_attr_reader :activator, :account @@ -631,13 +665,17 @@ # if you use a regular method and want to use context-specific behavior, # you must handle storing the context yourself: def regular_method apply_behaviors # handles the adding of all the roles and behaviors activator.some_behavior # behavior not available unless you apply roles on initialize + ensure + # Use ensure to enforce the removal of behaviors in case of exceptions. + # This also does not affect the return value of this method. remove_behaviors # handles the removal of all roles and behaviors end + # This trigger or the forward* methods are preferred for creating triggers. trigger :some_trigger_method do activator.some_behavior # behavior always available end trigger def some_other_trigger @@ -657,10 +695,12 @@ # or define your own method without the `disallow` keyword def disallow_some_trigger_method? # whatever conditional code for the instance of the context end + # Prefer using `disallow` because it will wrap role players in their roles for you; + # the `disallow_some_trigger_method?` defined above, does not. # Create shortcuts for triggers as class methods # so you can do ActiviatingAccount.some_trigger_method(activator, account) # This will make all triggers shortcuts. shortcut_triggers @@ -672,17 +712,162 @@ # Set triggers to always return the context object # so you can enforce East-oriented style or Tell, Don't Ask east_oriented_triggers + # Forward context instance methods as triggers to role players + forward_trigger :role_name, :method_name + forward_trigger :role_name, :method_name, :alternative_trigger_name_for_method_name + forward_triggers :role_name, :list, :of, :methods, :to, :forward + forwarding [:list, :of, :methods, :to, :forward] => :role_name end ``` ## Dependencies The dependencies are minimal. The plan is to keep it that way but allow you to configure things as you need. The [Triad](http://github.com/saturnflyer/triad) project was written specifically to manage the mapping of roles and objects to the modules which contain the behaviors. If you're using [Casting](http://github.com/saturnflyer/casting), for example, Surrounded will attempt to use that before extending an object, but it will still work without it. + +## Support for other ways to apply behavior + +Surrounded is designed to be flexible for you. If you have your own code to manage applying behaviors, you can setup your context class to use it. + +### Additional libraries + +Here's an example using [Behavioral](https://github.com/saturnflyer/behavioral) + +```ruby +class MyCustomContext + extend Surrounded::Context + + initialize :employee, :boss + + def module_extension_methods + [:with_behaviors].concat(super) + end + + def module_removal_methods + [:without_behaviors].concat(super) + end +end +``` + +If you're using your own non-SimpleDelegator wrapper you can conform to that; whatever it may be. + +```ruby +class MyCustomContext + extend Surrounded::Context + + initialize :employee, :boss + + class Employee < SuperWrapper + include Surrounded + + # defined behaviors here... + + def wrapped_object + # return the object that is wrapped + end + + end + + def unwrap_methods + [:wrapped_object] + end +end +``` + +### Applying individual roles + +If you'd like to use a special approach for just a single role, you may do that too. + +When applying behaviors from a role to your role players, your Surrounded context will first look for a method named `"apply_behavior_#{role}"`. Define your own method and set it to accept 2 arguments: the role constant and the role player. + +```ruby +class MyCustomContext + extend Surrounded::Context + + initialize :employee, :boss + + def apply_behavior_employee(behavior_constant, role_player) + behavior_constant.build(role_player).apply # or whatever your need to do with your constant and object. + end +end +``` + +You can also plan for special ways to remove behavior as well. + +```ruby +class MyCustomContext + extend Surrounded::Context + + initialize :employee, :boss + + def remove_behavior_employee(behavior_constant, role_player) + role_player.cleanup # or whatever your need to do with your constant and object. + end +end +``` + +You can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (thet contsant holding the behaviors), and then the name of the role which refers to the role playing object: `remove_behavior_role`. + +## How to read this code + +If you use this library, it's important to understand it. + +As much as possible, when you use the Surrounded DSL for creating triggers, roles, initialize methods, and others you'll likely fine the actual method definitions created in a module and then find that module included in your class. + +This is a design choice which allows you to override any standard behavior more easily. + +### Where methods exist and why + +When you define an initialize method for a Context class, Surrounded _could_ define the method on your class like this: + +```ruby +def initialize(*roles) + self.class_eval do # <=== this evaluates on _your_ class and defines it there. + # code... + end +end +``` + +If we used the above approach, you'd need to redefine initialize in its entirety: + +```ruby +initialize(:role1, role2) + +def initialize(role1, role2) # <=== this will completely redefine initialize on _this class_ + super # <=== this will NOT be the initialize method as provided to the Surrounded initialize above. +end +``` + +Surrounded uses a more flexible approach for you: + +```ruby +def initialize(*roles) + mod = Module.new + mod.class_eval do # <=== this evaluates on the module and defines it there. + # code... + end + include mod # <=== this adds it to the class ancestors +end +``` + +With this approach you can use the way Surrounded is setup, but make changes if you need. + +```ruby +initialize(:role1, :role2) # <=== defined in a module in the class ancestors + +def initialize(role1, role2) + super # <=== run the method as defined above in the Surrounded DSL + # ... then do additional work +end +``` + +### Read methods, expect modules + +When you go to read the code, expect to find behavior defined in modules. ## Installation Add this line to your application's Gemfile: