Rohbau
Gem | Source | Documentation
Description
Rohbau provides a set of patterns used in Domain Driven Design.
Installation
Add this line to your application's Gemfile:
gem 'rohbau'
And then execute:
$ bundle
Or install it yourself as:
$ gem install rohbau
Usage
Runtime
By instantiation of the RuntimeLoader
, an instance of the Runtime
is created and stored as a singleton.
Internal units of the respective component can access this instance by referring to the RuntimeLoader
.
By this a place is made where for example memories for in-memory gateway backend implementations can be stored.
Examples
Inject a user service to your application
examples/my_application.rb
require 'rohbau/runtime'
require 'rohbau/runtime_loader'
module MyApplication
class RuntimeLoader < Rohbau::RuntimeLoader
def initialize
super(Runtime)
end
end
class Runtime < Rohbau::Runtime
end
end
examples/user_service/runtime.rb
require 'rohbau/runtime'
require 'rohbau/runtime_loader'
module UserService
class RuntimeLoader < Rohbau::RuntimeLoader
def initialize
super(Runtime)
end
end
class Runtime < Rohbau::Runtime
end
end
examples/runtime.rb
require 'my_application'
require 'user_service/runtime'
# Register user service on my application runtime
MyApplication::Runtime.register :user_service, UserService::RuntimeLoader
# My application runtime knowns about the registered plugins
MyApplication::Runtime.plugins # => {:user_service=>UserService::RuntimeLoader}
# The registered plugin knows his registrar
UserService::RuntimeLoader.registrar # => MyApplication::Runtime
# Runtimes are not initialized yet
MyApplication::RuntimeLoader.instance # => nil
MyApplication::Runtime.plugins[:user_service].instance # => nil
# Boot my application runtime
MyApplication::RuntimeLoader.running? # => false
MyApplication::RuntimeLoader.new
MyApplication::RuntimeLoader.running? # => true
# Runtimes are initialized
MyApplication::RuntimeLoader.instance # => #<MyApplication::Runtime:0x00000000f5b8d8 @user_service=UserService::RuntimeLoader>
MyApplication::Runtime.plugins[:user_service].instance # => #<UserService::Runtime:0x00000000b1ecc0>
# Runtimes are singletons
MyApplication::RuntimeLoader.instance === MyApplication::RuntimeLoader.instance
MyApplication::Runtime.plugins[:user_service].instance === MyApplication::Runtime.plugins[:user_service].instance
# Terminate my application runtime
MyApplication::RuntimeLoader.terminate
MyApplication::RuntimeLoader.running? # => false
ServiceFactory
The ServiceFactory
is considered the authority for retrieval of service instances.
It follows partly the service locator / registry pattern.
Examples
Register and unregister default service and override with specific service.
examples/user_service/service_factory.rb
require 'rohbau/service_factory'
MyServiceFactory = Class.new(Rohbau::ServiceFactory)
user_service_1 = Struct.new(:users).new([:alice, :bob])
user_service_2 = Struct.new(:users).new([:jim, :kate])
runtime = Object.new
registry = MyServiceFactory.new(runtime)
MyServiceFactory.register(:user_service) { user_service_1 }
registry.user_service.users # => [:alice, :bob]
MyServiceFactory.register(:user_service) { user_service_2 }
registry.user_service.users # => [:jim, :kate]
MyServiceFactory.unregister(:user_service)
registry.user_service.users # => [:alice, :bob]
MyServiceFactory.unregister(:user_service)
registry.user_service # => NoMethodError: undefined method `user_service'
Validate registered dependencies
examples/user_service/service_factory_validation.rb
require 'rohbau/service_factory'
MyServiceFactory = Class.new(Rohbau::ServiceFactory)
MyServiceFactory.external_dependencies :user_service
MyServiceFactory.missing_dependencies # => [:user_service]
MyServiceFactory.external_dependencies_complied? # => false
MyServiceFactory.register(:user_service) { Object.new } # => :user_service
MyServiceFactory.external_dependencies_complied? # => true
MyServiceFactory.missing_dependencies # => []
Request
It ensures an initialized runtime and builds up a new the service factory instance.
examples/user_service/request.rb
require 'rohbau/request'
require 'user_service/service_factory'
module UserService
class Request < Rohbau::Request
def initialize(runtime = RuntimeLoader.instance)
super(runtime)
end
protected
def build_service_factory
ServiceFactory.new(@runtime)
end
end
end
Entity
Entities are low level, logic-less, data structures.
examples/user_entity.rb
require 'rohbau/entity'
class User < Rohbau::Entity
attributes :uid, :nickname
def initialize(user_data = {})
self.nickname = user_data[:nickname]
super()
end
end
bob = User.new
bob.nickname = 'Bob'
bob.nickname # => 'Bob'
other_bob = User.new
other_bob.nickname = 'Bob'
other_bob.nickname # => 'Bob'
bob == other_bob # => true
Gateway
Provides an interface to persist entities.
examples/user_service/user_gateway.rb
require 'user_service/event_tube'
require 'user_entity'
require 'rohbau/default_memory_gateway'
module UserService
class UserGateway < Rohbau::DefaultMemoryGateway
def create(user_data)
user = User.new(user_data)
add(user)
EventTube.publish :user_registered, UserRegisteredEvent.new(user)
end
class UserRegisteredEvent < Struct.new(:user)
end
end
end
UseCase
UseCases
define the interface for the end user who interacts with the system.
Examples
Define a class that inherits from Rohbau::UseCase
which has a #call
method:
examples/user_service/create_user_use_case.rb
require 'rohbau/use_case'
module UserService
class CreateUser < Rohbau::UseCase
def initialize(request, user_data)
super(request)
@user_data = user_data
end
def call
service(:user_service).create(@user_data)
end
end
end
And call the CreateUser use case as follows:
examples/use_case.rb
require 'user_service/runtime'
require 'user_service/request'
require 'user_service/create_user_use_case'
# Boot up user service
UserService::RuntimeLoader.new
request = UserService::Request.new
UserService::CreateUser.new(request, {:nickname => 'Bob'}).call # => 'Created user Bob'
Alternately, use cases can be called using Interface
, which is detailed in the next section.
Interface
Interface
allows for simpler and more semantic use case calling, with the additional benefit of fine grain control of return values and spy-like access in a test context.
Please note: Interface
requires an Input
class in your use case, as well as a Success
class if you are using the stub features.
In a nutshell, Interface
allows your use case calls to go from this:
require 'user_service/runtime'
require 'user_service/request'
require 'user_service/create_user_use_case'
# Boot up user service
UserService::RuntimeLoader.new
request = UserService::Request.new
input = {
:user_data => {
:nickname => 'Bob'
}
}
UserService::CreateUser.new(request, input).call
To this:
require 'rohbau/interface'
require 'user_service/create_user_use_case'
interface = Rohbau::Interface.instance
interface.user_service :create_user, :user_data => {
:nickname => 'Bob'
}
This increased simplicity is very helpful, but the majority of Interface
's usefulness becomes accessible while testing. Assume the following use case:
module UserService
module UseCases
class CreateUser
Input = Bound.required :user_data
Success = Bound.required :user_uid
Error = Bound.required :message
def initialize(request, input)
@request = request
@user_data = input.user_data
end
def call
result = service(:user_service).create(@user_data)
if result.nil?
Error.new :message => "Something went wrong"
else
Success.new :user_uid => "uid_for_#{user_data.nickname}"
end
end
end
end
end
Stubbing use case return values
Let's assume this use case will be called, along with many other use cases, by the frontend framework of your choosing.
Frontend tests should be implemented with the actual objects they would use in production, but since test isolation is an important concept in DDD, the UserService
domain, which is outside of the scope of the frontend, should never actually be called.
Beyond this, we will also want to stub return values to create the various test cases we may have - when there is no user present, for example.
These requirements can be realized by passing the following keys to your use case:
:stub_result
When thestub_result
key is present, its value will be passed to the called use case and returned in subsequent calls to that same use case as aSuccess
object.:stub_type
Much like thestub_result
key, thestub_type
key allows the type of return value to be overwritten, provided, of course, that it is a type which is defined by your use case.
require 'rohbau/interface'
describe 'stubbing use case return values' do
let(:interface) { Rohbau::Interface.instance }
it 'returns subsequent calls to the same use case as stubs' do
interface.user_service :create_user, :stub_result => {
:user_data => { :user_uid => "definitely NOT bob's uid" }
}
result = interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
assert_kind_of UserService::UseCases::CreateUser::Success, result
assert_equal "definitely NOT bob's uid", result.user_uid
end
it 'can also return other result types' do
interface.user_service :create_user,
:stub_type => :Error,
:stub_result => {
:message => "error"
}
result = interface.user_service :create_user
assert_kind_of UserService::UseCases::CreateUser::Error, result
assert_equal 'error', result.
end
end
Test spying
Sometimes it's helpful to look into the use case and see some details about how it has been called. There are two methods to this end, each of which returns a hash with keys corresponding to each use case which has been called:
interface.calls
is further keyed by argument and returns the value passed to the given argument.interface.call_count
returns the number of times a given use case has been called.
require 'rohbau/interface'
describe 'spying on tests' do
let(:interface) { Rohbau::Interface.instance }
it 'records passed arguments by use_case' do
interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
result = interface.calls[:create_user][:user_uid]
assert_equal "23", result
end
it 'records number of unstubbed calls to each use_case' do
interface.user_service :create_user, :stub_result => {
:user_data => { :user_uid => 'something else' }
}
interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
assert_equal 2, interface.call_count[:create_user]
end
end
Cleaning up
Interface
provides the following two convenience methods for cleaning up your test environment:
interface.clear_stubs
does what it says on the tin - All recorded arguments, call counts, stubbed results and stubbed types are cleared.interface.clear_cached_requests
empties the request cache.
require 'rohbau/interface'
let(:interface) { Rohbau::Interface.instance }
it 'can clear all stubbed results' do
interface.user_service :create_user, :stub_result => {
:user_data => { :user_uid => 'something else' }
}
result = interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
assert_equal 'something else', result.user_uid
interface.clear_stubs
result = interface.user_service :create_user, :user_data => {
:nickname => 'bob'
}
refute_equal 'something else', result.user_uid
assert_equal 'uid_for_bob', result.user_uid
end
EventTube
The EventTube
implements the Publish-subscribe
pattern. You can subscribe to events and publish them.
Examples
examples/email_service/email_service.rb
class EmailService
def self.send_user_registration_email_to(user)
print "Send out email to #{user.nickname}"
end
end
examples/user_service/event_tube.rb
require 'rohbau/event_tube'
module UserService
class EventTube < Rohbau::EventTube
end
end
Build README
Make changes to README.md.template
, not to README.md
Include examples with
include_example 'example_file_name'
Build README.md with
rake build_readme
Always commit README.md.template
and README.md
together.
Examples
Run all examples via
rake examples
To verify all examples run:
rake examples:verify
Note: Examples will be verified during CI run.
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