Surrounded
Create encapsulated systems of objects and focus on their interactions
Install / Use
/learn @saturnflyer/SurroundedREADME
Be in control of business logic.
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:
- define behaviors for each role and
- 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
node-connect
344.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
96.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
344.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
344.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。

