SkillAgentSearch skills...

Surrounded

Create encapsulated systems of objects and focus on their interactions

Install / Use

/learn @saturnflyer/Surrounded
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Surrounded

Be in control of business logic.

Build Status

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

How to think about your objects

First, name the problem you're solving. Then, break down your problem into responsible roles.

Use your problem name as a class and extend it with Surrounded::Context

It might look like this:

class Employment
  extend Surrounded::Context

  role :boss
  role :employee
end

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

class Employment
  extend Surrounded::Context

  initialize :employee, :boss

  role :boss
  role :employee
end

Here, you've specified the order when initializing so you can use it like this:

user1 = User.find(1)
user2 = User.find(2)
context = Employment.new(employee: user1, boss: user2)

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

There are 2 things left to do:

  1. define behaviors for each role and
  2. define how you can trigger their actions

Initializing contexts does not require the use of keyword arguments, but you may opt out.

You should consider using explicit names when initializing now by using initialize_without_keywords:

class Employment
  extend Surrounded::Context

  initialize_without_keywords :employee, :boss
end

user1 = User.find(1)
user2 = User.find(2)
context = Employment.new(user1, user2)

This will allow you to prepare your accessing code to use keywords.

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

initialize :role1, :role2 do
  map_role(:role3, 'SomeRoleConstantName', initialize_the_object_to_play)
end

This block will be called after the default initialization is done.

Defining behaviors for roles

Behaviors for your roles are easily defined just like you define a method. Provide your role a block and define methods there.

class Employment
  extend Surrounded::Context

  initialize :employee, :boss

  role :boss

  role :employee do
    def work_weekend
      if fed_up?
        quit
      else
        schedule_weekend_work
      end
    end

    def quit
      say("I'm sick of this place, #{boss.name}!")
      stomp
      throw_papers
      say("I quit!")
    end

    def schedule_weekend_work
      # ...
    end
  end
end

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

class Employment
  extend Surrounded::Context

  initialize :employee, :boss

  role :employee do
    #...
  end
end

Triggering interactions

You'll need to define way to trigger these behaviors to occur so that you can use them.

context = Employment.new(employee: user1, boss: user2)

context.plan_weekend_work

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

class Employment
  extend Surrounded::Context

  initialize :employee, :boss

  def plan_weekend_work
    employee.work_weekend
  end
  trigger :plan_weekend_work

  role :employee do
    #...
  end
end

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:

  def plan_weekend_work
    employee.work_weekend
  end
  trigger :plan_weekend_work

You can shorten it to:

  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:

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

It's as easy as:

class User
  include Surrounded

  # ...
end

This gives each of the objects the ability to understand its context and direct access to other objects in the context.

Why is this valuable?

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

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

Deeper Dive

Create encapsulated environments for your objects.

Typical initialization of an environment, or a Context in DCI, has a lot of code. For example:

class Employment

  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

This code allows the Employment class to create instances where it will have an employee and a boss role internally. These are set to attr_readers and are made private.

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.

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.

By adding Surrounded::Context you can shortcut all this work.

class Employment
  extend Surrounded::Context

  initialize(:employee, :boss)

  module Employee
    # extra behavior here...
  end
end

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:

class Employment
  extend Surrounded::Context

  initialize(:employee, :boss)

  class Employee < SimpleDelegator
    # extra behavior here...
  end
end

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.

But the syntax can be even simpler than that if you want.

class Employment
  extend Surrounded::Context

  initialize(:employee, :boss)

  role :employee do
    # extra behavior here...
  end
end

By default, this code will create a module for you named Employee. If you want to use a wrapper, you can do this:

class Employment
  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:

class Employment
  extend Surrounded::Context

  initialize(:employee, :boss)

  role :employee, :wrapper do
    # extra behavior here...
  end
end

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.

These are minor little changes which highlight how simple it is to use Surrounded.

Well... I want to use Casting so I get the benefit of modules without extending objects. Can I do that?

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.

class User
  include Surrounded
end

Now the User instances will be able to implicitly access objects in their environment.

Via method_missing those User instances c

Related Skills

View on GitHub
GitHub Stars256
CategoryDevelopment
Updated12d ago
Forks13

Languages

Ruby

Security Score

95/100

Audited on Mar 20, 2026

No findings