Bound
Gem | Source | Documentation
In short: The mission: Bring the notion of interfaces to ruby.
More detailed: When you build separated or distributed architectures in ruby, you probably encountered the problem of stale mocks or wrongly mocked interfaces of specific services at the boundaries of the different domains.
To tackle this problem, we use Bound
. Instead of providing just a list of
arguments to a poor little boundary method, it will just accept an argument, its
request, to speak in more technical terms. By implementing the request and
response objects through Bound
, you get validated interfaces and more explicit
and self documenting code for free.
See Usage below for more details with a concrete example.
Installation
Add this line to your application's Gemfile:
gem 'bound'
And then execute:
$ bundle
Or install it yourself as:
$ gem install bound
Usage
Consider the folowing scenario:
A generic domain which is responsible for administration and management of user registrations:
module UserDesk
end
It will somehow provide access to a registration service which gives you the possibility to create new user accounts:
class UserDesk::RegistrationService
def register_account(email, password)
ensure_validity!(email)
user_uid = do_things_on_a_magical_repository(email, password)
user_uid
end
private
# ...
end
Since the scope of this service can (and will) be very large, it will be painful to provide consistency around the different other domains, which get an instance of the registration service injected. Especially order changes in a larger argument list or added optional arguments could lead to false passing tests and therefor probably runtime bugs.
By utilizing Bound
, you could implement this service like following:
class UserDesk::RegistrationService
Registration = Bound.required(
:email,
:password
)
SuccessfulRegistration = Bound.required(:user_uid)
def register_account(registration)
ensure_validity!(registration.email)
user_uid = do_things_on_a_magical_repository(
registration.email,
registration.password
)
SuccessfulRegistration.new(:user_uid => user_uid)
end
private
# ...
end
The consumer would now instanciate the boundary class instead of just passing arbitrary arguments to the service:
registration = UserDesk::RegistrationService::Registration.new(
:email => params[:email],
:password => params[:password]
)
result = registration_service.register_account(registration)
do_stuff_with(result.user_uid)
Side note: the Registration
bound here would also accept any Object
, which provides the
methods email
and password
.
Bound would also loudly fail, if one of the required arguments is omitted or a unknown argument is provided. (Specific additional features like nested and optional arguments can be seen in the specs).
By concretinzing the boundaries, the overall structure of your architecture will
become more rigid and solid. The mocking part on the consumer-side would only
occur for the actual register_account
call, which is fairly trivial now from
the perspective of boundaries (known object in, known object out).
Older versions
Because of legacy software we also support some older versions:
- stable-1.1 with latest version 1.1.1
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Release
# Bump version
edit lib/bound/version.rb
git commit -am "Bump to version X.Y.Z"
rake release