CQS - Command Query Separation for Ruby (and Rails)
⚠️ This gem (and readme) are still Work In Progress ⚠️
Unlike CQRS, this gem tries to give a framework that allows to separate Commands from Queries in your Ruby programs.
Working on a Ruby on Rails application we got tired and frustrated about the ApplicationService
pattern that is so prevalent in so many Rails
codebases.
Here are a few reasons we did and did not like the pattern:
- A Service can be almost anything
- We are never sure what the service really does and we end up, if not careful, creating similar services
- The language got a little muddled
For these reasons we opted for creating a simpler language in our application.
When requesting information use a Query
When requesting a change use a Command
One thing we did not want to implement (the reason this is not a CQRS implementation) is the strictness of a pure CQRS implementation (meaning commands can, and many times will, have return values, specially if you consider a #save action in a Rails app).
Usage
Most of the times we use this gem in a Ruby on Rails project, so the usage examples will be for Rails. If you use it in plain Ruby projects it will still work, you just won't have access to the generators.
We are opinionated with the use of RSpec to test drive our code. Let us know if you'd like to have other testing tools supported!
General assumptions
Commands and Queries operate on a subject
. The subject is passed into the action by the provided methods (wtih
for commands, and in
for queries):
FindUser.in(email_address: "test@test.com")
You can register methods to change the way you call your action:
class FindUser
include Query
register_method :by
...
And then call the action with the method registered:
FindUser.by(email_address: "test@test.com")
This works for both, Commands
and Queries
.
The subject is what's passed into the calling method and can be anything you like (see below to see how it's used).
Queries
Imagine you want to create a query to find a user by passing a hash with some of their attributes (find_by
in Rails).
To save time and setup you'll run the provided generator:
rails generate cqs:query FindUser
This will create the following files:
- app/queries/query/find_user.rb
- spec/queries/query/find_user_spec.rb
The generated Query
looks like so:
module Query
class FindUser
include Query
def answer
raise AnswerUnknownApology.new "Please implement #answer"
end
end
end
And the corresponding spec:
require "rails_helper"
describe Query::FindUser do
it "does something" do
pending "test your query"
end
end
First we'd write our spec to ensure our Query
does what we'd like it to do:
require "rails_helper"
describe Query::FindUser do
it "finds a user by their email address" do
email_address = { email_address: "test@test.com"}
expect(User).to receive(:find_by).with(email_address)
Query::FindUser.by(email_address)
end
end
and the code to make this pass:
module Query
class FindUser
include Query
register_method :by
def answer
User.find_by(subject)
end
end
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run bundle execute rspec
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 the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/OurWeeSaas/cqs. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the CQS project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.