Provides easy and fast means to use the repository pattern to create separation between your business logic and your data sources.
The ownership of this project has been taken over from https://github.com/andreaseger/receptacle, where you can still find version 1.0
Add this line to your application's Gemfile:
gem 'receptacle'
And then execute:
$ bundle
Or install it yourself as:
$ gem install receptacle
A repository mediates requests based on it's configuration to a strategy which then itself implements the necessary functions to access the data source.
+--------------------+ +--------+
| | |Database|
| DatabaseStrategy +------> |
| | | |
+--------------------+ +--------------+ +----------^---------+ | |
| | | | | +--------+
| Business Logic +-----> Repository +----------------+
| | | |
+--------------------+ +--------|-----+ +--------------------+
| | |
| | InMemoryStrategy |
+-------|-----+ | |
|Configuration| +--------------------+
+-------------+
Let's look at the pieces:
- the repository itself - which is a simple module including the
Receptacle
mixin
module Repository
module User
include Receptacle::Repo
mediate :find
end
end
- at least one strategy class which are implemented as plain ruby classes
module Strategy
class Database
def find(id:)
# get data from data source and return a business entity
end
end
end
Optionally wrapper classes can be defined
module Wrapper
class Validator
def find(id:)
raise ArgumentError if id.nil?
yield(id: id)
end
end
class ModelMapper
def find(id:)
Model::User.new(yield(id: id))
end
end
end
Everything combined a simple example could look like the following:
require "receptacle"
module Repository
module User
include Receptacle::Repo
mediate :find
module Strategy
class DB
def find(id:)
# code to find from the database
end
end
class InMemory
def find(id:)
# code to find from InMemory store
end
end
end
module Wrapper
class Validator
def find(id:)
raise ArgumentError if id.nil?
yield(id: id)
end
end
class ModelMapper
def find(id:)
Model::User.new(yield(id: id))
end
end
end
end
end
For better separation to other repositories the fact that the repository itself is a module can be used to nest both strategies and wrapper underneath.
Somewhere in your application config you now need to setup the strategy and the wrappers for this repository like this:
Repository::User.strategy Repository::User::Strategy::DB
Repository::User.wrappers [Repository::User::Wrapper::Validator,
Repository::User::Wrapper::ModelMapper])
With this setup to use the repository method is as simple and straight forward
as calling Repository::User.find(id: 123)
What is the matter with this repository pattern and why should I care using it?
Often the business logic of applications directly accesses a data source like a database. This has several disadvantages such as
- code duplication cased by repeated need to transform raw data into business entities
- no separation between business logic and access to the data source
- harder to add or change global policies like caching
- caused by missing isolation it's harder to test the business logic independent from the data source
To improve on the disadvantages above and more we can introduce a repository which mediates between the business logic and the data source. The data source can be for example a database, an API(be it internal or external) or other web services.
A repository provides the business logic with a stable interface to interact with the data source. Hereby is the repository mapping the data to business entities. Because the repository is a central place to access the data source caching policies or similar can be applied easily there.
During testing the repository can be switched to a different strategy for example a fast and lightweight in memory data store to ease the process of testing the business logic.
Due to the ability to switch strategies a repository can also help to keep the application architecture flexible as a change in strategy has no impact on the business logic above.
A strategy is implemented as simple ruby class which implements the direct access to a data source by implementing the same method (as instance method) which was setup in the repository.
On each call to the repository a new instance of this class is created on which then the mediated method is called.
module Strategy
class Database
def find(id:)
# get data from data source and return a business entity
end
end
end
Due to the nature of creating a new instance on each method call persistent connections to the data source like a connection pool should be maintained outside the strategy itself. For example in a singleton class.
In addition to create a separation between data access and business logic often there is the need to perform certain actions in the context of a data source access. For example there can be the need to send message on a message bus whenever a resource was created - independent of the strategy.
This gem allow one to add such actions without adding them to all strategies or applying them in the business logic by using wrappers.
One or multiple wrappers sit logically between the repository and the strategies. Based on the repository configuration it knows when and in which order they should be applied.
Wrapper actions are implemented as plain ruby classes which provide instance methods named like the method that the repository/strategy method this action should be applied to.
module Wrapper
class Validator
def find(id:)
raise ArgumentError if id.nil?
yield(id: id)
end
end
end
This wrapper class would execute on any find
call. You can use it to execute code
before or after the next wrapper/strategy is called. Calling yield
executes the next
wrapper in line or the strategy, if this is the last wrapper that is called. The return
value is passed down to the previous wrapper and in the end to the repository caller.
Although currently not part of the gem a simple memory strategy can be implemented in this way:
class MemoryStore
class << self
def store
@store || clear
end
def clear
@store = {}
end
end
def clear
self.class.clear
end
private def store
self.class.store
end
end
Compared to other gem implementing the repository pattern this gem makes no assumptions regarding the interface of your repository or what kind of data source is used. Some alternative have some interesting features nevertheless:
- Hanami::Repository is for one closely tied to the the Hanami entities and does not separate the repository interface from the implementing strategies. For straight forward mapping of entity to data source this might be enough though. Another caveat is that it currently only supports SQL data sources.
- ROM::Repository similarly is tied to
other facilities of ROM like the ROM containers. It also appears to take a
similar approach as Hanami to custom queries which should not leak to the
outside application. There is predefined interface for manipulating resources
through. The addition of
ROM::Changeset
brings an interesting addition to the mix which might make it an interesting alternative if usingROM
fits into the applications structure.
This gem on the other hand makes absolutely no assumptions about your data source or general structure of your code. It can be simply plugged in between your business logic and data source to abstract the two. Of course, like the other repository pattern implementations, strategy details should be hidden from the interface. The data source can essentially be anything. A SQL database, a no-SQL database, a JSON API or even a gem. Placing a gem behind a repository can be useful if you're not yet sure this is the correct or best possible gem, the faraday gem is essentially doing this by giving all the different http libraries a common interface).
A module called TestSupport
can be found
here.
Right now it provides a helper method with_strategy
to easily toggle temporarily to another strategy. How to use it is described in more detail in the inline documentation.
- small core codebase
- flexible - all kind of methods should possible to be mediated
- basic but powerful callbacks/hooks/observer possibilities
After checking out the repo, run bin/setup
to install dependencies. Then, run
rake test
to run the tests. You can also run bin/console
for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To
release a new version, update the version number in version.rb
, and then run
bundle exec rake release
, which will create a git tag for the version, push
git commits and tags, and push the .gem
file
to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/runtastic/receptacle. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.