{"id":13482625,"url":"https://github.com/saturnflyer/surrounded","last_synced_at":"2025-04-04T07:05:32.767Z","repository":{"id":8836920,"uuid":"10540705","full_name":"saturnflyer/surrounded","owner":"saturnflyer","description":"Create encapsulated systems of objects and focus on their interactions","archived":false,"fork":false,"pushed_at":"2023-03-08T17:47:25.000Z","size":506,"stargazers_count":252,"open_issues_count":0,"forks_count":14,"subscribers_count":11,"default_branch":"master","last_synced_at":"2024-10-14T16:09:07.962Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saturnflyer.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2013-06-07T02:15:58.000Z","updated_at":"2024-10-05T16:23:38.000Z","dependencies_parsed_at":"2024-01-05T21:58:39.355Z","dependency_job_id":null,"html_url":"https://github.com/saturnflyer/surrounded","commit_stats":{"total_commits":406,"total_committers":7,"mean_commits":58.0,"dds":"0.017241379310344862","last_synced_commit":"a51050f8182548254eb32fd2839bbe439ed82aaf"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saturnflyer%2Fsurrounded","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saturnflyer%2Fsurrounded/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saturnflyer%2Fsurrounded/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saturnflyer%2Fsurrounded/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saturnflyer","download_url":"https://codeload.github.com/saturnflyer/surrounded/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247135142,"owners_count":20889420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:03.856Z","updated_at":"2025-04-04T07:05:32.744Z","avatar_url":"https://github.com/saturnflyer.png","language":"Ruby","readme":"# ![Surrounded](http://saturnflyer.github.io/surrounded/images/surrounded.png \"Surrounded\")\n## Be in control of business logic.\n\n[![Build Status](https://github.com/saturnflyer/surrounded/actions/workflows/test.yml/badge.svg)](https://github.com/saturnflyer/surrounded/actions)\n[![Code Climate](https://codeclimate.com/github/saturnflyer/surrounded.png)](https://codeclimate.com/github/saturnflyer/surrounded)\n\nSurrounded is designed to help you better manage your business logic by keeping cohesive behaviors together. Bring objects together to implement your use cases and gain behavior only when necessary.\n\n## How to think about your objects\n\nFirst, name the problem you're solving. Then, break down your problem into responsible roles.\n\nUse your problem name as a class and extend it with `Surrounded::Context`\n\nIt might look like this:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  role :boss\n  role :employee\nend\n```\n\nIn your application, you'll initialize this class with objects to play the roles that you've defined, so you'll need to specify which role players will use which role.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  role :boss\n  role :employee\nend\n```\n\nHere, you've specified the order when initializing so you can use it like this:\n\n```ruby\nuser1 = User.find(1)\nuser2 = User.find(2)\ncontext = Employment.new(employee: user1, boss: user2)\n```\n\nThat ensures that `user1` will become (and have all the features of) the `employee` and `user2` will become (and have all the features of) the `boss`.\n\nThere are 2 things left to do:\n\n1. define behaviors for each role and\n2. define how you can trigger their actions\n\nInitializing contexts does not require the use of keyword arguments, but you may opt out.\n\nYou should consider using explicit names when initializing now by using `initialize_without_keywords`:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize_without_keywords :employee, :boss\nend\n\nuser1 = User.find(1)\nuser2 = User.find(2)\ncontext = Employment.new(user1, user2)\n```\n\nThis will allow you to prepare your accessing code to use keywords.\n\nIf you need to override the initializer with additional work, you have the ability to use a block to be evaluated in the context of the initialized object.\n\n```ruby\ninitialize :role1, :role2 do\n  map_role(:role3, 'SomeRoleConstantName', initialize_the_object_to_play)\nend\n```\n\nThis block will be called _after_ the default initialization is done.\n\n## Defining behaviors for roles\n\nBehaviors for your roles are easily defined just like you define a method. Provide your role a block and define methods there.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  role :boss\n\n  role :employee do\n    def work_weekend\n      if fed_up?\n        quit\n      else\n        schedule_weekend_work\n      end\n    end\n\n    def quit\n      say(\"I'm sick of this place, #{boss.name}!\")\n      stomp\n      throw_papers\n      say(\"I quit!\")\n    end\n\n    def schedule_weekend_work\n      # ...\n    end\n  end\nend\n```\n\nIf any of your roles don't have special behaviors, like `boss`, you don't need to specify it. Your `initialize` setup will handle assiging who's who when this context is used.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  role :employee do\n    #...\n  end\nend\n```\n\n## Triggering interactions\n\nYou'll need to define way to trigger these behaviors to occur so that you can use them.\n\n```ruby\ncontext = Employment.new(employee: user1, boss: user2)\n\ncontext.plan_weekend_work\n```\n\nThe method you need is defined as an instance method in your context, but before that method will work as expected you'll need to mark it as a trigger.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  def plan_weekend_work\n    employee.work_weekend\n  end\n  trigger :plan_weekend_work\n\n  role :employee do\n    #...\n  end\nend\n```\n\nTrigger methods are different from regular instance methods in that they apply behaviors from the roles to the role players.\nA regular instance method just does what you define. But a trigger will make your role players come alive with their behaviors.\n\nYou 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:\n\n```ruby\n  def plan_weekend_work\n    employee.work_weekend\n  end\n  trigger :plan_weekend_work\n```\n\nYou can shorten it to:\n\n```ruby\n  trigger :plan_weekend_work do\n    employee.work_weekend\n  end\n```\n\nBut it can be even simpler and follows the same pattern provided by Ruby's standard library Forwardable:\n\n```ruby\n  # The first argument is the role to receive the messaged defined in the second argument.\n  # The third argument is optional and if provided will be the name of the trigger method on your context instance.\n  forward_trigger :employee, :work_weekend, :plan_weekend_work\n\n  # Alternatively, you can use an API similar to that of the `delegate` method from Forwardable\n  forwarding [:work_weekend] =\u003e :employee\n```\n\nThe 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`.\n\nThere's one last thing to make this work.\n\n## Getting your role players ready\n\nYou'll need to include `Surrounded` in the classes of objects which will be role players in your context.\n\nIt's as easy as:\n\n```ruby\nclass User\n  include Surrounded\n\n  # ...\nend\n```\n\nThis gives each of the objects the ability to understand its context and direct access to other objects in the context.\n\n## Why is this valuable?\n\nBy creating environments which encapsulate roles and all necessary behaviors, you will be better able to isolate the logic of your system. A `user` in your system doesn't have all possible behaviors defined in its class, it gains the behaviors only when they are necessary.\n\nThe objects that interact have their behaviors defined and available right where they are needed. Implementation is in proximity to necessity. The behaviors you need for each role player are highly cohesive and are coupled to their use rather than being coupled to the class of an object which might use them at some point.\n\n# Deeper Dive\n\n## Create encapsulated environments for your objects.\n\nTypical initialization of an environment, or a Context in DCI, has a lot of code. For example:\n\n```ruby\nclass Employment\n\n  attr_reader :employee, :boss\n  private :employee, :boss\n  def initialize(employee, boss)\n    @employee = employee.extend(Employee)\n    @boss = boss\n  end\n\n  module Employee\n    # extra behavior here...\n  end\nend\n```\n\nThis code allows the Employment 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.\n\nThe `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.\n\nMost 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.\n\nBy adding `Surrounded::Context` you can shortcut all this work.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize(:employee, :boss)\n\n  module Employee\n    # extra behavior here...\n  end\nend\n```\n\nSurrounded gives you an `initialize` class method which does all the setup work for you.\n\n## Managing Roles\n\n_I don't want to use modules. Can't I use something like SimpleDelegator?_\n\nWell, it just so happens that you can. This code will work just fine:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize(:employee, :boss)\n\n  class Employee \u003c SimpleDelegator\n    # extra behavior here...\n  end\nend\n```\n\nInstead 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.\n\nBut the syntax can be even simpler than that if you want.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize(:employee, :boss)\n\n  role :employee do\n    # extra behavior here...\n  end\nend\n```\n\nBy default, this code will create a module for you named `Employee`. If you want to use a wrapper, you can do this:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize(:employee, :boss)\n\n  wrap :employee do\n    # extra behavior here...\n  end\nend\n```\n\nBut 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:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n\n  initialize(:employee, :boss)\n\n  role :employee, :wrapper do\n    # extra behavior here...\n  end\nend\n```\n\nThe 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.\n\nThese are minor little changes which highlight how simple it is to use Surrounded.\n\n_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?_\n\nYup. 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.\n\n_Ok. So is that it?_\n\nThere's a lot more. Let's look at the individual objects and what they need for this to be valuable...\n\n## Objects' access to their environments\n\nAdd `Surrounded` to your objects to give them awareness of other objects.\n\n```ruby\nclass User\n  include Surrounded\nend\n```\n\nNow the `User` instances will be able to implicitly access objects in their environment.\n\nVia `method_missing` those `User` instances can access a `context` object it stores in an internal collection.\n\nInside of the `Employment` context we saw above, the `employee` and `boss` objects are instances of `User` for this example.\n\nBecause the `User` class includes `Surrounded`, the instances of that class will be able to access other objects in the same context implicitly.\n\nLet's make our context look like this:\n\n```ruby\nclass Employment\n  # other stuff from above is still here...\n\n  def plan_weekend_work\n    employee.quit\n  end\n\n  role :employee do\n    def quit\n      say(\"I'm sick of this place, #{boss.name}!\")\n      stomp\n      throw_papers\n      say(\"I quit!\")\n    end\n  end\nend\n```\n\nWhat's happening in there is that when the `plan_weekend_work` method is called on the instance of `Employment`, the `employee` has the ability to refer to `boss` because it is in the same context, e.g. the same environment.\n\nThe 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.\n\nWhat `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.\n\nI didn't mention how the context is set, however.\n\n## Tying objects together\n\nYour 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.\n\nHere's an example of what we want:\n\n```ruby\nclass Employment\n  # other stuff from above is still here...\n\n  def plan_weekend_work\n    employee.store_context(self)\n    employee.quit\n    employee.remove_context\n  end\n\n  role :employee do\n    def quit\n      say(\"I'm sick of this place, #{boss.name}!\")\n      stomp\n      throw_papers\n      say(\"I quit!\")\n    end\n  end\nend\n```\n\nNow that the `employee` has a reference to the context, it won't blow up when it hits `boss` inside that `quit` method.\n\nWe 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:\n\n```ruby\nclass Employment\n  # other stuff from above is still here...\n\n  trigger :plan_weekend_work do\n    employee.quit\n  end\n\n  role :employee do\n    def quit\n      say(\"I'm sick of this place, #{boss.name}!\")\n      stomp\n      throw_papers\n      say(\"I quit!\")\n    end\n  end\nend\n```\n\nBy 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.\n\nThe context will also store the triggers so that you can, for example, provide details outside of the environment about what triggers exist.\n\n```ruby\ncontext = Employment.new(current_user, the_boss)\ncontext.triggers #=\u003e [:plan_weekend_work]\n```\n\nYou might find that useful for dynamically defining user interfaces.\n\nSometimes I'd rather not use this DSL, however. I want to just write regular methods.\n\nWe can do that too. You'll need to opt in to this by specifying `trigger :your_method_name` for the methods you want to use.\n\n```ruby\nclass Employment\n  # other stuff from above is still here...\n\n  def plan_weekend_work\n    employee.quit\n  end\n  trigger :plan_weekend_work\n\n  # or in Ruby 2.x\n  trigger def plan_weekend_work\n    employee.quit\n  end\n\n  role :employee do\n    def quit\n      say(\"I'm sick of this place, #{boss.name}!\")\n      stomp\n      throw_papers\n      say(\"I quit!\")\n    end\n  end\nend\n```\n\nThis 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.\n\nThis works like Ruby's `public`,`protected`, and `private` keywords in that you can send symbols of method names to it. But `trigger` does not alter the parsing of the document like those core keywords do. In other words, you can't merely type `trigger` on one line, and have methods added afterward be treated as trigger methods.\n\n## Access Control / Permissions for Triggers\n\nIf you decide to build a user interface from the available triggers, you'll find you need to know what triggers are available.\n\nFortunately, you can make it easy.\n\nBy running `protect_triggers` you'll be able to define when triggers may or may not be run. You can still run them, but they'll raise an error. Here's an example.\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n  protect_triggers\n\n  def plan_weekend_work\n    employee.quit\n  end\n  trigger :plan_weekend_work\n\n  disallow :plan_weekend_work do\n    employee.bank_balance \u003e 1000000\n  end\nend\n```\n\nThen, when the employee role's `bank_balance` is greater than `1000000`, the available triggers won't include `:plan_weekend_work`.\n\nYou can compare the instance of the context by listing `all_triggers` and `triggers` to see what could be possible and what's currently possible.\n\nAlternatively, 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.\n\nIn fact, that's exactly what happens with the `disallow` keyword. After using it here, we'd have a `disallow_plan_weekend_work?` method defined.\n\nIf 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.\n\n## Restricting return values\n\n_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.\n\nHere's how you enforce it:\n\n```ruby\nclass Employment\n  extend Surrounded::Context\n  east_oriented_triggers\nend\n```\n\nThat's it.\n\nWith that change, any trigger you define will execute the block you provide and return `self`, being the instance of the context.\n\n## Where roles exist\n\nBy using `Surrounded::Context` you are declaring a relationship between the objects inside playing your defined roles.\n\nBecause all the behavior is defined internally and only relevant internally, those relationships don't exist outside of the environment.\n\nSurrounded 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.\n\n## The role DSL\n\nUsing the `role` method to define modules and classes takes care of the setup for you. This way you can swap between implementations:\n\n```ruby\n\n  # this uses modules which include Surrounded\n  role :source do\n    def transfer\n      self.balance -= amount\n      destination.balance += amount\n      self\n    end\n  end\n\n  # this uses SimpleDelegator and Surrounded\n  role :source, :wrap do\n    def transfer\n      self.balance -= amount\n      destination.balance += amount\n      __getobj__\n    end\n  end\n\n  # this uses a special interface object which pulls\n  # methods from a module and applies them to your object.\n  role :source, :interface do\n    def transfer\n      self.balance -= amount\n      # not able to access destination unless the object playing source is Surrounded\n      destination.balance += amount\n      self\n    end\n  end\n```\n\nThe `:interface` option is a special object which has all of the standard Object methods removed (excepting ones like `__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.\n\nNotice 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__`.\n\nThe downside of using an interface is that it is still a wrapper and it only has access to the other objects in the context if the wrapped object already includes Surrounded. All of your defined role methods are executed in the context of the object playing the role, but the interface has it's own identity.\n\nIf you'd like to choose one and use it all the time, you can set the default:\n\n```ruby\nclass MoneyTransfer\n  extend Surrounded::Context\n\n  self.default_role_type = :interface # also :wrap, :wrapper, or :module\n\n  role :source do\n    def transfer\n      self.balance -= amount\n      destination.balance += amount\n      self\n    end\n  end\nend\n```\n\nOr, if you like, you can choose the default for your entire project:\n\n```ruby\nSurrounded::Context.default_role_type = :interface\n\nclass MoneyTransfer\n  extend Surrounded::Context\n\n  role :source do\n    def transfer\n      self.balance -= amount\n      destination.balance += amount\n      self\n    end\n  end\nend\n```\n\n## Working with collections\n\nIf you want to use an Array of objects (for example) as a role player in your context,\nyou may do so. If you want each item in your collection to gain behavior, you merely need to\ncreate a role for the items.\n\nSurrounded will attempt to guess at the singular role name. For example, a role player named `members` would\nbe given the behaviors from a `Members` behavior module or class. Each item in your `members` collection\nwould be given behavior from a `Member` behavior module or class if you create one.\n\n```ruby\nclass Organization\n  extend Surrounded::Context\n\n  initialize_without_keywords :leader, :members\n\n  role :members do\n    # special behavior for the collection\n  end\n\n  role :member do\n    # special behavior to be applied to each member in the collection\n  end\nend\n```\n\nIf you want to change the way the singular verson of a role is used, override `singularize_name`:\n\n```ruby\nclass Organization\n  extend Surrounded::Context\n\n  def singularize_name(name)\n    if name == \"my special rule\"\n      # do your thing\n    else\n      super # use the default\n    end\n  end\nend\n```\n\n## Reusing context objects\n\nIf you create a context object and need to use the same type of object with new role players, you may use the `rebind` method. It will clear any instance_variables from your context object and map the given objects to their names:\n\n```ruby\ncontext = Employment.new(employee: current_user, boss: the_boss)\ncontext.rebind(employee: another_user, boss: someone_else) # same context, new players\n```\n\n## Background Processing\n\nWhile there's no specific support for background processing, your context objects make it easy for you to add your own by remembering what arguments were provided during initialization.\n\nWhen you initialize a context, it will keep track of the parameters and their matching arguments in a private hash called `initializer_arguments`. This allows you to write methods to create a context object and have itself sent to a background processor.\n\n```ruby\nclass ExpensiveCalculation\n  extend Surrounded::Context\n\n  initialize :leader, :members\n\n  def send_to_background(trigger_method)\n    background_arguments = initializer_arguments.merge(trigger: trigger_method)\n    BackgroundProcessor.enqueue(self.class.name, **background_arguments)\n  end\n\n  class BackgroundProcessor\n    def perform(**args)\n      trigger_name = args.delete(:trigger)\n      job_class.new(args).send(trigger_name)\n    end\n  end\nend\nExpensiveCalculation.new(leader: some_object, members: some_collection).send_to_background(:do_expensive_calculation)\n```\n\nThe above example is merely pseudo-code to show how `initializer_arguments` can be used. Customize it according to your own needs.\n\n## Overview in code\n\nHere's a view of the possibilities in code.\n\n```ruby\n# set default role type for *all* contexts in your program\nSurrounded::Context.default_role_type = :module # also :wrap, :wrapper, or :interface\n\nclass ActiviatingAccount\n  extend Surrounded::Context\n\n  # set the default role type only for this class\n  self.default_role_type = :module # also :wrap, :wrapper, or :interface\n\n  # shortcut initialization code\n  initialize(:activator, :account)\n  # or handle it yourself\n  def initialize(activator:, account:)\n    # this must be done to handle the mapping of roles to objects\n    # pass an array of arrays with role name symbol and the object for that role\n    map_roles([[:activator, activator],[:account, account]])\n    # or pass a hash\n    map_roles(:activator =\u003e activator, :account =\u003e account)\n\n    # or load extra objects, perform other functions, etc. if you need and then use super\n    account.perform_some_funtion\n    super\n  end\n  # these also must be done if you create your own initialize method.\n  # this is a shortcut for using attr_reader and private\n  private_attr_reader :activator, :account\n\n  # If you need to mix default initialzation and extra work use a block\n  initialize :activator, :account do\n    map_roles(:third_party =\u003e get_some_other_object)\n    # explicitly set a single role\n    map_role(:something_new, 'SomeRoleConstant', object_to_play_the_role)\n  end\n\n  # but remember to set the extra accessors:\n  private_attr_reader :third_party, :something_new\n\n  # initialize without keyword arguments\n  initialize_without_keywords(:activator, :account)\n  # this makes the following instance method signature with positional arguments\n  def initialize(activator, account)\n    # ...\n  end\n\n  # Handle method name collisions on role players against role names in the context\n  on_name_collision :raise # will raise your context namespaced error: ActiviatingAccount::NameCollisionError\n  on_name_collision :warn\n  on_name_collision -\u003e(message){ puts \"Here's the message! #{message}\" }\n  on_name_collision :my_custom_handler\n  def my_custom_handler(message)\n    # do something with the message here\n  end\n\n  role :activator do # module by default\n    def some_behavior; end\n  end\n\n  #  role_methods :activator, :module do # alternatively use role_methods if you choose\n  #    def some_behavior; end\n  #  end\n  #\n  #  role :activator, :wrap do\n  #    def some_behavior; end\n  #  end\n  #\n  #  role :activator, :interface do\n  #    def some_behavior; end\n  #  end\n  #\n  # use your own classes if you don't want SimpleDelegator\n  # class SomeSpecialRole\n  #   include Surrounded # \u003c-- you must remember this in your own classes\n  #   # Surrounded assumes SomeSpecialRole.new(some_special_role)\n  #   def initialize(...);\n  #     # ... your code here\n  #   end\n  # end\n\n  # if you use a regular method and want to use context-specific behavior,\n  # you must handle storing the context yourself:\n  def regular_method\n    apply_behaviors # handles the adding of all the roles and behaviors\n    activator.some_behavior # behavior not available unless you apply roles on initialize\n  ensure\n     # Use ensure to enforce the removal of behaviors in case of exceptions.\n     # This also does not affect the return value of this method.\n    remove_behaviors # handles the removal of all roles and behaviors\n  end\n\n  # This trigger or the forward* methods are preferred for creating triggers.\n  trigger :some_trigger_method do\n    activator.some_behavior # behavior always available\n  end\n\n  trigger def some_other_trigger\n    activator.some_behavior # behavior always available\n  end\n\n  def regular_non_trigger\n    activator.some_behavior # behavior always available with the following line\n  end\n  trigger :regular_non_trigger # turns the method into a trigger\n\n  # create restrictions on what triggers may be used\n  protect_triggers # \u003c-- this is required if you want to protect your triggers this way.\n  disallow :some_trigger_method do\n    # whatever conditional code for the instance of the context\n  end\n  # you could also use `guard` instead of `disallow`\n\n  # or define your own method without the `disallow` keyword\n  def disallow_some_trigger_method?\n    # whatever conditional code for the instance of the context\n  end\n  # Prefer using `disallow` because it will wrap role players in their roles for you;\n  # the `disallow_some_trigger_method?` defined above, does not.\n\n  # Create shortcuts for triggers as class methods\n  # so you can do ActiviatingAccount.some_trigger_method(activator, account)\n  # This will make all triggers shortcuts.\n  shortcut_triggers\n  # Alterantively, you could implement shortcuts individually:\n  def self.some_trigger_method(activator, account)\n    instance = self.new(activator, account)\n    instance.some_trigger_method\n  end\n\n  # Set triggers to always return the context object\n  # so you can enforce East-oriented style or Tell, Don't Ask\n  east_oriented_triggers\n\n  # Forward context instance methods as triggers to role players\n  forward_trigger :role_name, :method_name\n  forward_trigger :role_name, :method_name, :alternative_trigger_name_for_method_name\n  forward_triggers :role_name, :list, :of, :methods, :to, :forward\n  forwarding [:list, :of, :methods, :to, :forward] =\u003e :role_name\nend\n\n# with initialize (also keyword_initialize)\ncontext = ActiviatingAccount.new(activator: some_object, account: some_account)\n# with initialize_without_keywords\ncontext = ActiviatingAccount.new(some_object, some_account)\ncontext.triggers # =\u003e lists a Set of triggers\n# when using protect_triggers\ncontext.triggers # =\u003e lists a Set of triggers which may currently be called\ncontext.all_triggers # =\u003e lists a Set of all triggers (the same as if protect_triggers was _not_ used)\ncontext.allow?(:trigger_name) # =\u003e returns a boolean if the trigger may be run\n\n# reuse the context object with new role players\ncontext.rebind(activator: another_object, account: another_account)\n```\n\n## Dependencies\n\nThe 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. It is used in Surrounded to keep track of role player, roles, and role constant names but it is not a hard requirement. You may implement your own but presently you'll need to dive into the implementation to fully understand how. Future updates may provide better support and guidance.\n\nIf you want to override the class used for mapping roles to behaviors, override the `role_map` method.\n\n```ruby\nclass MyContext\n  extend Surrounded::Context\n\n  def role_map\n    @container ||= role_mapper_class.new(base: MySpecialDataContainer)\n  end\nend\n```\n\nThe class you provide will be initialized with `new` and is expected to implement the methods: `:update`, `:each`, `:values`, and `:keys`.\n\nIf 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.\n\n## Support for other ways to apply behavior\n\nSurrounded 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.\n\n### Additional libraries\n\nHere's an example using [Behavioral](https://github.com/saturnflyer/behavioral)\n\n```ruby\nclass MyCustomContext\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  def module_extension_methods\n    [:with_behaviors].concat(super)\n  end\n\n  def module_removal_methods\n    [:without_behaviors].concat(super)\n  end\nend\n```\n\nIf you're using your own non-SimpleDelegator wrapper you can conform to that; whatever it may be.\n\n```ruby\nclass MyCustomContext\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  class Employee \u003c SuperWrapper\n    include Surrounded\n\n    # defined behaviors here...\n\n    def wrapped_object\n      # return the object that is wrapped\n    end\n\n  end\n\n  def unwrap_methods\n    [:wrapped_object]\n  end\nend\n```\n\n### Applying individual roles\n\nIf you'd like to use a special approach for just a single role, you may do that too.\n\nWhen 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.\n\n```ruby\nclass MyCustomContext\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  def apply_behavior_employee(behavior_constant, role_player)\n    behavior_constant.build(role_player).apply # or whatever your need to do with your constant and object.\n  end\nend\n```\n\nYou can also plan for special ways to remove behavior as well.\n\n```ruby\nclass MyCustomContext\n  extend Surrounded::Context\n\n  initialize :employee, :boss\n\n  def remove_behavior_employee(behavior_constant, role_player)\n    role_player.cleanup # or whatever your need to do with your constant and object.\n  end\nend\n```\n\nYou can remember the method name by the convention that `remove` or `apply` describes it's function, `behavior` refers to the first argument (the constant holding the behaviors), and then the name of the role which refers to the role playing object: `remove_behavior_role`.\n\n## Name collisions between methods and roles\n\nLets say that you wish to create a context as below, intending to use instances of the following two classes as role players:\n\n```ruby\n  class Postcode\n    # other methods...\n    def code\n      @code\n    end\n\n    def country\n      @country\n    end\n  end\n\n  class Country\n    # other methods...\n    def country_code\n      @code\n    end\n  end\n\n  class SendAParcel\n    extend Surrounded::Context\n\n    keyword_initialize :postcode, :country\n\n    trigger :send do\n      postcode.send\n    end\n\n    role :postcode do\n      def send\n        # do things...\n        country_code = country.country_code # name collision...probably raises an exception!\n      end\n    end\n  end\n```\nWhen you call the `:send` trigger you are likely to be greeted with an `NoMethodError` exception. The reason for this is that there is a name collision between `Postcode#country`, and the `:country` role in the `SendAParcel` context. Where a name collision exists, the method in the role player overrides that of the calling class and you get unexpected results.\n\nTo address this issue, use `on_name_collision` to specify the name of a method to use when collisions are found:\n\n```ruby\n\n  class SendAParcel\n    extend Surrounded::Context\n\n    on_name_collision :raise\n  end\n\n```\n\nThis option will raise an exception (obviously). You may use any method which is available to the context but it must accept a single message as the argument.\n\nYou can also use a lambda:\n\n```ruby\n\nclass SendAParcel\n  extend Surrounded::Context\n\n  on_name_collision -\u003e(message){ puts \"Here's the message: #{message}\"}\nend\n\n```\n\nYou may also user a class method:\n\n```ruby\n  class SendAParcel\n    extend Surrounded::Context\n\n    def self.handle_collisions(message)\n      Logger.debug \"#{Time.now}: #{message}\"\n    end\n  end\n```\n\n## How to read this code\n\nIf you use this library, it's important to understand it.\n\nAs much as possible, when you use the Surrounded DSL for creating triggers, roles, initialize methods, and others you'll likely find the actual method definitions created in a module and then find that module included in your class.\n\nThis is a design choice which allows you to override any standard behavior more easily.\n\n### Where methods exist and why\n\nWhen you define an initialize method for a Context class, Surrounded _could_ define the method on your class like this:\n\n```ruby\ndef initialize(*roles)\n  self.class_eval do # \u003c=== this evaluates on _your_ class and defines it there.\n    # code...\n  end\nend\n```\n\nIf we used the above approach, you'd need to redefine initialize in its entirety:\n\n```ruby\ninitialize(:role1, role2)\n\ndef initialize(role1, role2) # \u003c=== this will completely redefine initialize on _this class_\n  super # \u003c=== this will NOT be the initialize method as provided to the Surrounded initialize above.\nend\n```\n\nSurrounded uses a more flexible approach for you:\n\n```ruby\ndef initialize(*roles)\n  mod = Module.new\n  mod.class_eval do # \u003c=== this evaluates on the module and defines it there.\n    # code...\n  end\n  include mod # \u003c=== this adds it to the class ancestors\nend\n```\n\nWith this approach you can use the way Surrounded is setup, but make changes if you need.\n\n```ruby\ninitialize(:role1, :role2) # \u003c=== defined in a module in the class ancestors\n\ndef initialize(role1, role2)\n  super # \u003c=== run the method as defined above in the Surrounded DSL\n  # ... then do additional work\nend\n```\n\n### Read methods, expect modules\n\nWhen you go to read the code, expect to find behavior defined in modules.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'surrounded'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install surrounded\n\n## Installation for Rails\n\nSee [surrounded-rails](https://github.com/saturnflyer/surrounded-rails)\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","funding_links":[],"categories":["Abstraction","Ruby","Business logic"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaturnflyer%2Fsurrounded","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaturnflyer%2Fsurrounded","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaturnflyer%2Fsurrounded/lists"}