README.md in surrounded-0.4.1 vs README.md in surrounded-0.5.0

- old
+ new

@@ -1,180 +1,361 @@ # Surrounded +## Bring your own complexity [![Build Status](https://travis-ci.org/saturnflyer/surrounded.png?branch=master)](https://travis-ci.org/saturnflyer/surrounded) [![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded) [![Coverage Status](https://coveralls.io/repos/saturnflyer/surrounded/badge.png)](https://coveralls.io/r/saturnflyer/surrounded) [![Gem Version](https://badge.fury.io/rb/surrounded.png)](http://badge.fury.io/rb/surrounded) -## Create encapsulated environments for your objects. +# Surrounded aims to make things simple and get out of your way. -Keep the distraction of other features out of your way. Write use cases and focus on just the business logic +Most of what you care about is defining the behavior of objects. How they interact is important. +The purpose of this library is to clear away the details of getting things setup and to allow you to make changes to the way you handle roles. -## Usage +There are two main parts to this library. -Add `Surrounded` to your objects to give them awareness of other objects. +1. `Surrounded` gives objects an implicit awareness of other objects in their environments. +2. `Surrounded::Context` helps you create objects which encapsulate other objects. These *are* the environments. +First, take a look at creating contexts. This is where you'll spend most of your time. + +## Easily create encapsulated environments for your objects. + +Typical initialization of an environment, or a Context in DCI, has a lot of code. For example: + ```ruby -class User - include Surrounded +class MyEnvironment + + attr_reader :employee, :boss + private :employee, :boss + def initialize(employee, boss) + @employee = employee.extend(Employee) + @boss = boss + end + + module Employee + # extra behavior here... + end end ``` -Now your user instances will be able to get objects in their environment. +This code allows the MyEnvironment class to create instances where it will have an `employee` and a `boss` role internally. These are set to `attr_reader`s and are made private. -_What environment!? I don't get it._ +The `employee` is extended with behaviors defined in the `Employee` module, and in this case there's no extra stuff for the `boss` so it doesn't get extended with anything. -I didn't explain that yet. +Most of the time you'll follow a pattern like this. Some objects will get extra behavior and some won't. The modules that you use to provide the behavior will match the names you use for the roles to which you assign objects. -You can make an object which contains other objects. It acts as an environment -and objects inside should have knowledge of the other objects in the environment. -Take a breath, because there's a lot going on. +By adding `Surrounded::Context` you can shortcut all this work. -First, you extend a class with the appropriate module to turn it into an object environment: - ```ruby class MyEnvironment extend Surrounded::Context + + initialize(:employee, :boss) + + module Employee + # extra behavior here... + end end ``` -Typical initialization of this environment has a lot of code. For example: +Surrounded gives you an `initialize` class method which does all the setup work for you. +## Managing Roles + +_I don't want to use modules. Can't I use something like SimpleDelegator?_ + +Well, it just so happens that you can. This code will work just fine: + ```ruby class MyEnvironment extend Surrounded::Context + + initialize(:employee, :boss) - attr_reader :employee, :boss - private :employee, :boss - def initialize(employee, boss) - @employee = employee.extend(Employee) - @boss = boss - end - - module Employee + class Employee < SimpleDelegator # extra behavior here... end end ``` -_WTF was all that!?_ +Instead of extending the `employee` object, Surrounded will run `Employee.new(employee)` to create the wrapper for you. You'll need to include the `Surrounded` module in your wrapper, but we'll get to that. -Relax. I'll explain. +But the syntax can be even simpler than that if you want. -When you create an instance of `MyEnvironment` it has certain objects inside. -Here we see that it has an `employee` and a `boss`. Inside the methods of the environment it's simpler and easier to write `employee` instead of `@employee` so we make them `attr_reader`s. But we don't need these methods to be externally accessible so we set them to private. +```ruby +class MyEnvironment + extend Surrounded::Context + + initialize(:employee, :boss) -Next, we want to add environment-specific behavior to the `employee` so we extend the object with the module `Employee`. + role :employee do + # extra behavior here... + end +end +``` -If you're going to be doing this a lot, it's painful. Here's what `Surrounded` does for you: +By default, this code will create a module for you named `Employee`. If you want to use a wrapper, you can do this: ```ruby class MyEnvironment extend Surrounded::Context + + initialize(:employee, :boss) + wrap :employee do + # extra behavior here... + end +end +``` + +But if you're making changes and you decide to move from a module to a wrapper or from a wrapper to a module, you'll need to change that method call. Instead, you could just tell it which type of role to use: + +```ruby +class MyEnvironment + extend Surrounded::Context + initialize(:employee, :boss) - module Employee + role :employee, :wrapper do # extra behavior here... end end ``` -There! All that boilerplate code is cleaned up. +The default available types are `:module`, `:wrap` or `:wrapper`, and `:interface`. We'll get to `interface` below. The `:wrap` and `:wrapper` types are the same and they'll both create classes which inherit from SimpleDelegator _and_ include Surrounded for you. -Notice that there's no `Boss` module. If a module of that name does not exist, the object passed into initialize simply won't gain any new behavior. +These are minor little changes which highlight how simple it is to use Surrounded. -_OK. I think I get it, but what about the objects? How are they aware of their environment? Isn't that what this is supposed to do?_ +_Well... I want to use [Casting](https://github.com/saturnflyer/casting) so I get the benefit of modules without extending objects. Can I do that?_ -Yup. Ruby doesn't have a notion of a local environment, so we lean on `method_missing` to do the work for us. +Yup. The ability to use Casting is built-in. If the objects you provide to your context respond to `cast_as` then Surrounded will use that. +_Ok. So is that it?_ + +There's a lot more. Let's look at the individual objects and what they need for this to be valuable... + +## Objects' access to their environments + +Add `Surrounded` to your objects to give them awareness of other objects. + ```ruby class User include Surrounded end ``` -With that, all instances of `User` have implicit access to their surroundings. +Now the `User` instances will be able to implicitly access objects in their environment. -_Yeah... How?_ +Via `method_missing` those `User` instances can access a `context` object it stores in an internal collection. -Via `method_missing` those `User` instances can access a `context` object it stores in a `@__surroundings__` collection. I didn't mention how the context is set, however. +Inside of the `MyEnvironment` context we saw above, the `employee` and `boss` objects are instances of `User` for this example. -Your environment will have methods of it's own that will trigger actions on the objects inside, but we need those trigger methods to set the environment instance as the current context so that the objects it contains can access them. +Because the `User` class includes `Surrounded`, the instances of that class will be able to access other objects in the same context implicitly. -Here's an example of what we want: +Let's make our context look like this: ```ruby class MyEnvironment # other stuff from above is still here... def shove_it - employee.store_context(self) employee.quit - employee.remove_context end - module Employee + role :employee do def quit say("I'm sick of this place, #{boss.name}!") stomp throw_papers say("I quit!") end end end ``` -What's happening in there is that when the `shove_it` method is called, the current environment object is stored as the context. +What's happening in there is that when the `shove_it` method is called on the instance of `MyEnvironment`, the `employee` has the ability to refer to `boss` because it is in the same context, e.g. the same environment. The behavior defined in the `Employee` module assumes that it may access other objects in it's local environment. The `boss` object, for example, is never explicitly passed in as an argument. -_WTF!? That's insane!_ +What `Surrounded` does for us is to make the relationship between objects and gives them the ability to access each other. Adding new or different roles to the context now only requires that we add them to the context and nothing else. No explicit references must be passed to each individual method. The objects are aware of the other objects around them and can refer to them by their role name. -I thought so too, at first. But continually passing references assumes there's no relationship between objects in that method. What `Surrounded` does for us is to make the relationship between objects and gives them the ability to access each other. +I didn't mention how the context is set, however. -This simple example may seem trivial, but the more contextual code you have the more cumbersome passing references becomes. By moving knowledge to the local environment, you're free to make changes to the procedures without the need to alter method signatures with new refrences or the removal of unused ones. +## Tying objects together -By using `Surrounded::Context` you are declaring a relationship between the objects inside. +Your context will have methods of it's own which will trigger actions on the objects inside, but we need those trigger methods to set the accessible context for each of the contained objects. -Because all the behavior is defined internally and only relevant internally, those relationships don't exist outside of the environment. +Here's an example of what we want: -_OK. I think I understand. So I can change business logic just by changing the procedures and the objects. I don't need to adjust arguments for a new requirement. That's kind of cool!_ +```ruby +class MyEnvironment + # other stuff from above is still here... -Damn right. + def shove_it + employee.store_context(self) + employee.quit + employee.remove_context + end -But you don't want to continually set those context details, do you? + role :employee do + def quit + say("I'm sick of this place, #{boss.name}!") + stomp + throw_papers + say("I quit!") + end + end +end +``` -_No. That's annoying._ +Now that the `employee` has a reference to the context, it won't blow up when it hits `boss` inside that `quit` method. -Yeah. Instead, it would be easier to have this library do the work for us. -Here's what you can do: +We saw how we were able to clear up a lot of that repetitive work with the `initialize` method, so this is how we do it here: ```ruby class MyEnvironment - # the other code from above... + # other stuff from above is still here... trigger :shove_it do employee.quit end + + role :employee do + def quit + say("I'm sick of this place, #{boss.name}!") + stomp + throw_papers + say("I quit!") + end + end end ``` -By using this `trigger` keyword, our block is the code we care about, but internally the method is written to set the `@__surroundings__` collection. +By using this `trigger` keyword, our block is the code we care about, but internally the method is created to first set all the objects' current contexts. -_Hmm. I don't like having to do that._ +The context will also store the triggers so that you can, for example, provide details outside of the environment about what triggers exist. -Me either. I'd rather just use `def` but getting automatic code for setting the context is really convenient. -It also allows us to store the triggers so that you can, for example, provide details outside of the environment about what triggers exist. - ```ruby context = MyEnvironment.new(current_user, the_boss) context.triggers #=> [:shove_it] ``` You might find that useful for dynamically defining user interfaces. +Sometimes I'd rather not use this DSL, however. I want to just write regular methods. + +We can do that too. You'll need to opt in to this by specifying `set_methods_as_triggers` for the context class. + +```ruby +class MyEnvironment + # other stuff from above is still here... + + set_methods_as_triggers + + def shove_it + employee.quit + end + + role :employee do + def quit + say("I'm sick of this place, #{boss.name}!") + stomp + throw_papers + say("I quit!") + end + end +end +``` + +This will allow you to write methods like you normally would. They are aliased internally with a prefix and the method name that you use is rewritten to add and remove the context for the objects in this context. The public API of your class remains the same, but the extra feature of wrapping your method is handled for you. + +This will treat all instance methods defined on your context the same way, so be aware of that. + +## Where roles exist + +By using `Surrounded::Context` you are declaring a relationship between the objects inside playing your defined roles. + +Because all the behavior is defined internally and only relevant internally, those relationships don't exist outside of the environment. + +Surrounded makes all of your role modules and classes private constants. It's not a good idea to try to reuse behavior defined for one context in another area. + +## The role DSL + +Using the `role` method to define modules and classes takes care of the setup for you. This way you can swap between implementations: + +```ruby + + # this uses modules + role :source do + def transfer + self.balance -= amount + destination.balance += amount + self + end + end + + # this uses SimpleDelegator and Surrounded + role :source, :wrap do + def transfer + self.balance -= amount + destination.balance += amount + __getobj__ + end + end + + # this uses a special interface object which pulls + # methods from a module and applies them to your object. + role :source, :interface do + def transfer + self.balance -= amount + destination.balance += amount + self + end + end +``` + +The `:interface` option is a special object which has all of its methods removed (excepting `__send__` and `object_id`) so that other methods will be pulled from the ones that you define, or from the object it attempts to proxy. + +Notice that the `:interface` allows you to return `self` whereas the `:wrap` acts more like a wrapper and forces you to deal with that shortcoming by using it's wrapped-object-accessor method: `__getobj__`. + +If you'd like to choose one and use it all the time, you can set the default: + +```ruby +class MoneyTransfer + extend Surrounded::Context + + self.default_role_type = :interface # also :wrap, :wrapper, or :module + + role :source do + def transfer + self.balance -= amount + destination.balance += amount + self + end + end +end +``` + +Or, if you like, you can choose the default for your entire project: + +```ruby +Surrounded::Context.default_role_type = :interface + +class MoneyTransfer + extend Surrounded::Context + + role :source do + def transfer + self.balance -= amount + destination.balance += amount + self + end + end +end +``` + ## Policies for the application of role methods There are 2 approaches to applying new behavior to your objects. By default your context will add methods to an object before a trigger is run @@ -192,11 +373,11 @@ apply_roles_on(:trigger) # this is the default # apply_roles_on(:initialize) # set this to apply behavior from the start initialize(:activator, :account) - module Activator + role :activator do def some_behavior; end end def non_trigger_method activator.some_behavior # not available unless you apply roles on initialize @@ -226,45 +407,70 @@ context = ActiviatingAccount.new(current_user, Account.find(123)) context.do_something current_user.some_behavior # NoMethodError ``` -## How's the performance? +## Overview in code -I haven't really tested yet, but there are several ways you can add behavior to your objects. +Here's a view of the possibilities in code. -There are a few defaults built in. +```ruby +# set default role type for *all* contexts in your program +Surrounded::Context.default_role_type = :module # also :wrap, :wrapper, or :interface -1. If you define modules for the added behavior, the code will run `object.extend(RoleInterface)` -2. If you are using [casting](http://github.com/saturnflyer/casting), the code will run `object.cast_as(RoleInterface)` -3. If you would rather use wrappers you can define classes and the code will run `RoleInterface.new(object)` and assumes that the `new` method takes 1 argument. You'll need to remember to `include Surrounded` in your classes, however. -4. If you want to use wrappers but would rather not muck about with including modules and whatnot, you can define them like this: - -``` -class SomeContext +class ActiviatingAccount extend Surrounded::Context - initialize(:admin, :user) + apply_roles_on(:trigger) # this is the default + # apply_roles_on(:initialize) # set this to apply behavior from the start + + set_methods_as_triggers # allows you to skip the 'trigger' dsl + + # set the default role type only for this class + self.default_role_type = :module # also :wrap, :wrapper, or :interface - wrap :admin do - # special methods defined here + initialize(:activator, :account) + + role :activator do # module by default + def some_behavior; end end -``` -The `wrap` method will create a class of the given name (`Admin` in this case) and will inherit from `SimpleDelegator` from the Ruby standard library _and_ will `include Surrounded`. + # role :activator, :module do + # def some_behavior; end + # end + # + # role :activator, :wrap do + # def some_behavior; end + # end + # + # role :activator, :interface do + # def some_behavior; end + # end + # + # use your own classes if you don't want SimpleDelegator + # class MySpecialClass + # include Surrounded # you must remember this + # # Surrounded assumes MySpecialClass.new(the_role_player_here) + # def initialize(...); + # # ... your code here + # end + # end -Lastly, there's a 5th option if you're using Ruby 2.x: `interface`. + # works as a trigger (assigning the current context) only if set_methods_as_triggers is set + def regular_method + activator.some_behavior # behavior not available unless you apply roles on initialize + end -The `interface` method acts similarly to the `wrap` method in that it returns an object that is not actually the object you want. But an `interface` is different in that it will apply methods from a module instead of using methods defined in a SimpleDelegator subclass. How is that important? Well you are free to use things like instance variables in your methods because they will be executed in the context of the object. This is unlike methods in a SimpleDelegator where the wrapper maintains its own instance variables. + trigger :some_trigger_method do + activator.some_behavior # behavior always available + end +end +``` -_Which should I use?_ - -Start with the default and see how it goes, then try another approach and measure the changes. - ## Dependencies -The dependencies are minimal. The plan is to keep it that way but allow you to configure things as you need. +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. ## Installation @@ -279,9 +485,13 @@ $ bundle Or install it yourself as: $ gem install surrounded + +## Installation for Rails + +See [surrounded-rails](https://github.com/saturnflyer/surrounded-rails) ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`)