Rectify
Rectify is a gem that provides some lightweight classes that will make it easier to build Rails applications in a more maintainable way. It's built on top of several other gems and adds improved APIs to make things easier.
Rectify is an extraction from a number of projects that use these techniques and proved to be successful.
Video
In June 2016, I spoke at RubyC about Rectify and how it can be used to improve areas of your application. The full video and slides can be found here:
Building maintainable Rails apps - RubyC 2016
Installation
To install, add it to your Gemfile
:
gem "rectify"
Then use Bundler to install it:
bundle install
Overview
Currently, Rectify consists of the following concepts:
- Form Objects
- Commands
- Presenters
- Query Objects
You can use these separately or together to improve the structure of your Rails applications.
The main problem that Rectify tries to solve is where your logic should go. Commonly, business logic is either placed in the controller or the model and the views are filled with too much logic. The opinion of Rectify is that these places are incorrect and that your models in particular are doing too much.
Rectify's opinion is that controllers should just be concerned with HTTP related things and models should just be concerned with data relationships. The problem then becomes, how and where do you place validations, queries and other business logic?
Using Rectify, Form Objects contain validations and represent the data input of your system. Commands then take a Form Object (as well as other data) and perform a single action which is invoked by a controller. Query objects encapsulate a single database query (and any logic it needs). Presenters contain the presentation logic in a way that is easily testable and keeps your views as clean as possible.
Rectify is designed to be very lightweight and allows you to use some or all of it's components. We also advise to use these components where they make sense not just blindly everywhere. More on that later.
Here's an example controller that shows details about a user and also allows a user to register an account. This creates a user, sends some emails, does some special auditing and integrates with a third party system:
class UserController < ApplicationController
include Rectify::ControllerHelpers
def show
present UserDetailsPresenter.new(:user => current_user)
end
def new
@form = RegistrationForm.new
end
def create
@form = RegistrationForm.from_params(params)
RegisterAccount.call(@form) do
on(:ok) { redirect_to dashboard_path }
on(:invalid) { render :new }
on(:already_registered) { redirect_to login_path }
end
end
end
The RegistrationForm
Form Object encapsulates the relevant data that is
required for the action and the RegisterAccount
Command encapsulates the
business logic of registering a new account. The controller is clean and
business logic now has a natural home:
HTTP => Controller (redirecting, rendering, etc)
Data Input => Form Object (validation, acceptable input)
Business Logic => Command (logic for a specific use case)
Data Persistence => Model (relationships between models)
Data Access => Query Object (database queries)
View Logic => Presenter (formatting data)
The next sections will give further details about using Form Objects, Commands and Presenters.
Form Objects
The role of the Form Object is to manage the input data for a given action. It validates data and only allows whitelisted attributes (replacing the need for Strong Parameters). This is a departure from "The Rails Way" where the model contains the validations. Form Objects help to reduce the weight of your models for one, but also, in an app of reasonable complexity even simple things like validations become harder because context is important.
For example, you can add validation for a User
model but there are different
context where the validations change. When a user registers themselves you might
have one set of validations, when an admin edits that user you might have
another set, maybe even when a user edits themselves you may have a third. In
"The Rails Way" you would have to have conditional validation in your model.
With Rectify you can have a different Form Object per context and keep things
easier to manage.
Form objects in Rectify are based on Virtus and make them compatible with Rails form builders, add ActiveModel validations and all allow you to specify a model to mimic.
Here is how you define a form object:
class UserForm < Rectify::Form
attribute :first_name, String
attribute :last_name, String
validates :first_name, :last_name, :presence => true
end
You can then set that up in your controller instead of a normal ActiveRecord model:
class UsersController < ApplicationController
def new
@form = UserForm.new
end
def create
@form = UserForm.from_params(params)
if @form.valid?
# Do something interesting
end
end
end
You can use the form object with form builders such as simple_form like this:
= simple_form_for @form do |f|
= f.input :first_name
= f.input :last_name
= f.submit
Mimicking models
When the form is generated it uses the name of the form class to infer what
"model" it should mimic. In the example above, it will mimic the User
model
as it removes the Form
suffix from the form class name by default.
The model being mimicked affects two things about the form:
- The route path helpers to use as the url to post to, for example:
users_path
. - The parent key in the params hash that the controller receives, for example
user
in this case:
params = {
"id" => "1",
"user" => {
"first_name" => "Andy",
"last_name" => "Pike"
}
}
You might want to mimic something different and use a form object that is not named in a way where the correct model can be mimicked. For example:
class UserForm < Rectify::Form
mimic :teacher
attribute :first_name, String
attribute :last_name, String
validates :first_name, :last_name, :presence => true
end
In this example we are using the same UserForm
class but am mimicking a
Teacher
model. The above form will then use the route path helpers
teachers_path
and the params key will be teacher
rather than users_path
and user
respectively.
Attributes
You define your attributes for your form object just like you do in Virtus.
By default, Rectify forms include an id
attribute for you so you don't need to
add that. We use this id
attribute to fulfill some of the requirements of
ActiveModel so your forms will work with form builders. For example, your form
object has a #persisted?
method. Your form object is never persisted so
technically this should always return false
.
However, you are normally representing something that is persistable. So we use
the value of id
to workout if what this should return. If id
is a number
greater than zero then we assume it is persisted otherwise we assume it isn't.
This is important as it affects where your form is posted (to the #create
or
#update
action in your controller).
Populating attributes
There are a number of ways to populate attributes of a form object.
Constructor
You can use the constructor and pass it a hash of values:
form = UserForm.new(:first_name => "Andy", :last_name => "Pike")
Params hash
You can use the params hash that a Rails controller provides that contains all the data in the request:
form = UserForm.from_params(params)
When populating from params we will populate the built in id
attribute from
the root of the params hash and populate the rest of the form attributes from
within the parent key. For example:
params = {
"id" => "1",
"user" => {
"first_name" => "Andy",
"last_name" => "Pike"
}
}
form = UserForm.from_params(params)
form.id # => 1
form.first_name # => "Andy"
form.last_name # => "Pike"
The other thing to notice is that (thanks to Virtus), attribute values are cast
to the correct type. The params hash is actually all string based but when you
get values from the form, they are returned as the correct type (see id
above).
In addition to the params hash, you may want to add additional contextual data.
This can be done by supplying a second hash to the .from_params
method.
Elements from this hash will be available to populate form attributes as if they
were under the params key:
form = UserForm.from_params(params, :ip_address => "1.2.3.4")
form.id # => 1
form.first_name # => "Andy"
form.last_name # => "Pike"
form.ip_address # => "1.2.3.4"
Model
You can pass a Ruby object instance (which is normally an ActiveModel but can be any PORO) to the form to populate it's attribute values. This is useful when editing a model:
user = User.create(:first_name => "Andy", :last_name => "Pike")
form = UserForm.from_model(user)
form.id # => 1
form.first_name # => "Andy"
form.last_name # => "Pike"
This works by trying to match (deeply) the attributes of the form object with the passed in object. If there is matching attribute or method in the model, then whatever it returns will be assigned to the form attribute.
This works great for most cases, but sometimes you need more control and need the
ability to do custom mapping from the model to the form. When this is required,
you just need to implement the #map_model
method in your form object:
class UserForm < Rectify::Form
attribute :full_name, String
def map_model(model)
self.full_name = "#{model.first_name} #{model.last_name}"
end
end
The #map_model
method is called as part of .from_model
after all the automatic
attribute assignment is complete.
One important thing that is different about Rectify forms is that they are not bound to a model. You can use a model to populate the form's attributes but that is all it will do. It does not keep a reference to the model or interact with it.
Rectify forms are designed to be lightweight representations of the data you want to collect or show in your forms, not something that is linked to a model. This allows you to create any form that you like which doesn't need to match the representation of the data in the database.
JSON
You can also populate a form object from a JSON string. Just pass it in to the
.from_json
class method and the form will be created with the attributes
populated by matching names:
json = <<-JSON
{
"first_name": "Andy",
"age": 38
}
JSON
form = UserForm.from_json(json)
form.first_name # => "Andy"
form.age # => 38
Populating the form from JSON can be useful when dealing with API requests into your system. Which allows you to easily access data and perform validation if required.
Validations
Rectify includes ActiveModel::Validations
for you so you can use all of the
Rails validations that you are used to within your models.
Your Form Object has a #valid?
method that will validate the attributes of
your form as well as any (deeply) nested form objects and array attributes that
contain form objects. There is also an #invalid?
method that returns the
opposite of #valid?
.
The #valid?
and #invalid?
methods also take a set of options. These options allow
you to not validate nested form objects or array attributes that contain form objects.
For example:
class UserForm < Rectify::Form
attribute :name, String
attribute :address, AddressForm
attribute :contacts, Array[ContactForm]
validates :name, :presence => true
end
class AddressForm < Rectify::Form
attribute :street, String
attribute :town, String
attribute :city, String
attribute :post_code, String
validates :street, :post_code, :presence => true
end
class ContactForm < Rectify::Form
attribute :name, String
attribute :number, String
validates :name, :presence => true
end
form = UserForm.from_params(params)
form.valid?(:exclude_nested => true, :exclude_arrays => true)
In this case, the UserForm
attributes will be validated (name
in the example above)
but the address
and contacts
will not be validated.
Deep Context
It's sometimes useful to have some context within your form objects when performing validations or some other type of data manipulation of the input. For example, you might want to check that the current user owns a particular resource as part of your validations. You could add the current user as an additional contextual option as the example shows above. However, sometimes you need this context to be available at all levels within your form not just at the root form object. You might have nested forms or arrays of form objects and they all might need access to this context. As there is no link up the chain from child to parent forms, we need a way to supply some context and make it available to all child forms.
You can do that using the #with_context
method.
form = UserForm.from_params(params).with_context(:user => current_user)
This allows us to access #context
in any form, and use the information within
it when we perform validations or other work:
class PostForm < Rectify::Form
attribute :blog_id, Integer
attribute :title, String
attribute :body, String
attribute :tags, Array[TagForm]
validate :check_blog_ownership
def check_blog_ownership
return if context.user.blogs.exists?(:id => blog_id)
errors.add(:blog_id, "not owned by this user")
end
end
class TagForm < Rectify::Form
attribute :name, String
attribute :category_id, Integer
validate :check_category
def check_category
return if context.user.categories.exists?(:id => category_id)
errors.add(:category_id, "not a category for this user")
end
end
The context is passed to all nested forms within a form object to make it easy to perform all the validations and data conversions you might need from within the form object without having to do this as part of the command.
Strong Parameters
Did you notice in the example above that there was no mention of Strong Parameters. That's because with Form Objects you do not need strong parameters. You only specify attributes in your form that are allowed to be accepted. All other data in your params hash is ignored.
Take a look at Virtus for more information about how to build a form object.
I18n
Regarding internationalization, the main affected classes when coercing are Date
and Time
classes. This is coercing Strings into Date
, DateTime
and Time
. Texts don't usually need to be coerced as they are simple String attributes with nothing special in them.
When coercing dates and times in a multi-language application, each locale will have its own date and time formats, and these formats should be taken into account when coercing strings (inputs entered by the user, or comming form external sources).
So for Date
, DateTime
and Time
classes, Rectify does not support I18n by default. But there are some ways to achieve it indirectly.
Probably the best is to define custom Virtus::Attribute
s for each kind of temporal class. For exmaple:
class LocalizedDate < Virtus::Attribute
def coerce(value)
return value unless value.is_a?(String)
Date.strptime(value, I18n.t("date.formats.short"))
rescue ArgumentError
nil
end
end
Commands
Commands (also known as Service Objects) are the home of your business logic. They allow you to simplify your models and controllers and allow them to focus on what they are responsible for. A Command should encapsulate a single user task such as registering for a new account or placing an order. You of course don't need to put all code for this task within the Command, you can (and should) create other classes that your Command uses to perform it's work.
With regard to naming, Rectify suggests using verbs rather than nouns for
Command class names, for example RegisterAccount
, PlaceOrder
or
GenerateEndOfYearReport
. Notice that we don't suffix commands with Command
or Service
or similar.
Commands in Rectify are based on Wisper
which allows classes to broadcast events for publish/subscribe capabilities.
Rectify::Command
is a lightweight class that gives an alternate API and adds
some helper methods to improve Command logic.
The reason for using the pub/sub model rather than returning a result means that we can reduce the number of conditionals in our code as the outcome of a Command might be more complex than just success or failure.
Here is an example Command with the structure Rectify suggests (as seen in the overview above):
class RegisterAccount < Rectify::Command
def initialize(form)
@form = form
end
def call
return broadcast(:invalid) if form.invalid?
transaction do
create_user
notify_admins
audit_event
send_user_details_to_crm
end
broadcast(:ok)
end
private
attr_reader :form
def create_user
# ...
end
def notify_admins
# ...
end
def audit_event
# ...
end
def send_user_details_to_crm
# ...
end
end
To invoke this Command, you would do the following:
def create
@form = RegistrationForm.from_params(params)
RegisterAccount.call(@form) do
on(:ok) { redirect_to dashboard_path }
on(:invalid) { render :new }
on(:already_registered) { redirect_to login_path }
end
end
What happens inside a Command?
When you call the .call
class method, Rectify will instantiate a new instance
of the command and will pass the parameters to it's constructor, it will then
call the instance method #call
on the newly created command object. The
.call
method also allows you to supply a block where you can handle the events
that may have been broadcast from the command.
The events that your Command broadcasts can be anything, Rectify suggests :ok
for success and :invalid
if the form data is not valid, but it's totally up to
you.
From here you can choose to implement your Command how you see fit. A
Rectify::Command
only has to have the instance method #call
.
Writing Commands
As your application grows and Commands get more complex we recommend using the
structure above. Within the #call
method you first check that the input data
is valid. If it is you then perform the various tasks that need to be completed.
We recommend using private methods for each step that are well named which makes
it very easy for anyone reading the code to workout what it does.
Feel free to use other classes and objects where appropriate to keep your code well organized and maintainable.
Events
Just as in Wisper, you fire events using
the broadcast
method. You can use any event name you like. You can also pass
parameters to the handling block:
# within the command:
class RegisterAccount < Rectify::Command
def call
# ...
broadcast(:ok, user)
end
end
# within the controller:
def create
RegisterAccount.call(@form) do
on(:ok) { |user| logger.info("#{user.first_name} created") }
end
end
When an event is handled, the appropriate block is called in the context of the controller. Basically, any method call within the block is delegated back to the controller.
As well as capturing events in a block, the command will also return a hash of the broadcast events together with any parameters that were passed. For example:
events = RegisterAccount.call(form)
events # => { :ok => user }
There will be a key for each event broadcast and its value will be the parameters passed. If there is a single parameter it will be the value. If there are no parameters or many, the hash value for the event key will be an array of the parameters:
events = RegisterAccount.call(form)
events # => {
# :ok => user,
# :messages => ["User registered", "Email sent", "Account ready"],
# :next => []
# }
You may occasionally want to expose a value within a handler block to the view.
You do this via the expose
method within the handler block. If you want to
use expose
then you must include the Rectify::ControllerHelpers
module in
your controller. You pass a hash of the variables you wish to expose to the view
and they will then be available. If you have set a Presenter for the view then
expose
will try to set an attribute on that presenter. If there is no
Presenter or the Presenter doesn't have a matching attribute then expose
will
set an instance variable of the same name. See below for more details about
Presenters.
# within the controller:
include Rectify::ControllerHelpers
def create
present HomePresenter.new(:name => "Guest")
RegisterAccount.call(@form) do
on(:ok) { |user| expose(:name => user.name, :greeting => "Hello") }
end
end
<!-- within the view: -->
<p><%= @greeting %> <%= presenter.name %></p>
# => <p>Hello Andy</p>
Take a look at Wisper for more information around how to do publish/subscribe.
Presenters
A Presenter is a class that contains the presentational logic for your views. These are also known as an "exhibit", "view model", "view object" or just a "view" (Rails views are actually templates, but anyway). To avoid confusion Rectify calls these classes Presenters.
It's often the case that you need some logic that is just for the UI. The same question comes up, where should this logic go? You could put it directly in the view, add it to the model or create a helper. Rectify's opinion is that all of these are incorrect. Instead, create a Presenter for the view (or component of the view) and place your logic here. These classes are easily testable and provide a more object oriented approach to the problem.
To create a Presenter just derive off of Rectify::Presenter
, add attributes as
you do for Form Objects using Virtus
attribute
declaration. Inside a Presenter you have access to all view helper
methods so it's easy to move the presentation logic here:
class UserDetailsPresenter < Rectify::Presenter
attribute :user, User
def edit_link
return "" unless user.admin?
link_to "Edit #{user.name}", edit_user_path(user)
end
end
Once you have a Presenter, you typically create it in your controller and make it accessible to your views. There are two ways to do that. The first way is to just treat it as a normal class:
class UsersController < ApplicationController
def show
user = User.find(params[:id])
@presenter = UserDetailsPresenter.new(:user => user).attach_controller(self)
end
end
You need to call #attach_controller
and pass it a controller instance which will
allow it access to the view helpers. You can then use the Presenter in your
views as you would expect:
<p><%= @presenter.edit_link %></p>
The second way is a little cleaner as we have supplied a few helper methods to
clean up remove some of the boilerplate. You need to include the
Rectify::ControllerHelpers
module and then use the present
helper:
class UsersController < ApplicationController
include Rectify::ControllerHelpers
def show
user = User.find(params[:id])
present UserDetailsPresenter.new(:user => user)
end
end
In your view, you can access this presenter using the presenter
helper method:
<p><%= presenter.edit_link %></p>
We recommend having a single Presenter per view but you may want to have more
than one presenter. You can use a Presenter to to hold the presentation logic
of your layout or for a component view. To do this, you can either use the first
method above or use the present
method and add a for
option with any key:
class ApplicationController < ActionController::Base
include Rectify::ControllerHelpers
before_action { present LayoutPresenter.new(:user => user), :for => :layout }
end
To access this Presenter in the view, just pass the Presenter key to the
presenter
method like so:
<p><%= presenter(:layout).login_link %></p>
Updating values of a Presenter
After a presenter has been instantiated you can update it's values by just setting their attributes:
class UsersController < ApplicationController
include Rectify::ControllerHelpers
def show
user = User.find(params[:id])
present UserDetailsPresenter.new(:user => user)
presenter.user = User.first
end
# or...
def other_action
user = User.find(params[:id])
@presenter = UserDetailsPresenter.new(:user => user).attach_controller(self)
@presenter.user = User.first
end
end
As mentioned above in the Commands section, you can use the expose
method (if
you include Rectify::ControllerHelpers
). You can use this anywhere in the
controller action including the Command handler block. If you have set a
Presenter for the view then expose
will try to set an attribute on that
presenter. If there is no Presenter or the Presenter doesn't have a matching
attribute then expose
will set an instance variable of the same name:
class UsersController < ApplicationController
include Rectify::ControllerHelpers
def show
user = User.find(params[:id])
present UserDetailsPresenter.new(:user => user)
expose(:user => User.first, :message => "Hello there!")
# presenter.user == User.first
# @message == "Hello there!"
end
end
Decorators
Another option for containing your UI logic is to use a Decorator. Rectify
doesn't ship with a built in way to create a decorator but we recommend either
using Draper or you can roll your own
using SimpleDelegator
:
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
user = User.new(:first_name => "Andy", :last_name => "Pike")
decorator = UserDecorator.new(user)
decorator.full_name # => "Andy Pike"
If you want to decorate a collection of objects you can do that by adding the
for_collection
method:
class UserDecorator < SimpleDelegator
# ...
def self.for_collection(users)
users.map { |u| new(u) }
end
end
users = UserDecorator.for_collection(User.all)
user.each do |u|
u.full_name # => Works for each user :o)
end
Query Objects
The final main component to Rectify is the Query Object. It's role is to encapsulate a single database query and any logic that it query needs to operate. It still uses ActiveRecord but adds some very light sugar on the top to make this style of architecture easier. This helps to keep your model classes lean and gives a natural home to this code.
To create a query object, you create a new class and derive off of
Rectify::Query
. The only thing you need to do is to implement the
#query
method and return an ActiveRecord::Relation
object from it:
class ActiveUsers < Rectify::Query
def query
User.where(:active => true)
end
end
To use this object, you just instantiate it and then use one of the following methods to make use of it:
ActiveUsers.new.count # => Returns the number of records
ActiveUsers.new.first # => Returns the first record
ActiveUsers.new.exists? # => Returns true if there are any records, else false
ActiveUsers.new.none? # => Returns true if there are no records, else false
ActiveUsers.new.to_a # => Execute the query and returns the resulting objects
ActiveUsers.new.each do |user| # => Iterates over each result
puts user.name
end
ActiveUsers.new.map(&:age) # => All Enumerable methods
Passing data to query objects
Passing data that your queries need to operate is best done via the constructor:
class UsersOlderThan < Rectify::Query
def initialize(age)
@age = age
end
def query
User.where("age > ?", @age)
end
end
UsersOlderThan.new(25).count # => Returns the number of users over 25 years old
Sometimes your queries will need to do a little work with the provided data before they can use it. Having your query encapsulated in an object makes this easy and maintainable (here's a trivial example):
class UsersWithBlacklistedEmail < Rectify::Query
def initialize(blacklist)
@blacklist = blacklist
end
def query
User.where(:email => blacklisted_emails)
end
private
def blacklisted_emails
@blacklist.map { |b| b.email.strip.downcase }
end
end
Composition
One of this great features of ActiveRecord is the ability to easily compose
queries together in a simple way which helps reusability. Rectify Query Objects
can also be combined to created composed queries using the |
operator as we
use in Ruby for Set Union. Here's how it looks:
active_users_over_20 = ActiveUsers.new | UsersOlderThan.new(20)
active_users_over_20.count # => Returns number of active users over 20 years old
You can union many queries in this manner which will result in another
Rectify::Query
object that you can use just like any other. This results in a
single database query.
As an alternative you can also use the #merge
method which is simply an alias
of the |
operator:
active_users_over_20 = ActiveUsers.new.merge(UsersOlderThan.new(20))
active_users_over_20.count # => Returns number of active users over 20 years old
The .merge
class method of Rectify::Query
accepts multiple Rectify::Query
objects to union together. This is the same as using the |
operator on multiple Rectify::Query
objects.
active_users_over_20 = Rectify::Query.merge(
ActiveUsers.new,
UsersOlderThan.new(20)
)
active_users_over_20.count # => Returns number of active users over 20 years old
You can also pass a Rectify::Query
object into the constructor of another Rectify::Query
object to set it as the base scope.
class UsersOlderThan < Rectify::Query
def initialize(age, scope = AllUsers.new)
@age = age
@scope = scope
end
def query
@scope.query.where("age > ?", @age)
end
end
UsersOlderThan.new(20, ActiveUsers.new).count
Leveraging your database
Using ActiveRecord::Relation
is a great way to construct your database queries
but sometimes you need to to use features of your database that aren't supported
by ActiveRecord directly. These are usually database specific and can greatly
improve your query efficiency. When that happens, you will need to write some
raw SQL. Rectify Query Objects allow for this. In addition to your #query
method returning an ActiveRecord::Relation
you can also return an array of
objects. This means you can run raw SQL using
ActiveRecord::Querying#find_by_sql
:
class UsersOverUsingSql < Rectify::Query
def initialize(age)
@age = age
end
def query
User.find_by_sql([
"SELECT * FROM users WHERE age > :age ORDER BY age ASC", { :age => @age }
])
end
end
When you do this, the normal Rectify::Query
methods are available but they
operate on the returned array rather than on the ActiveRecord::Relation
. This
includes composition using the |
operator but you can't compose an
ActiveRecord::Relation
query object with one that returns an array of objects
from its #query
method. You can compose two queries where both return arrays
but be aware that this will query the database for each query object and then
perform a Ruby array set union on the results. This might not be the most
efficient way to get the results so only use this when you are sure it's the
right thing to do.
The above example is fine for short SQL statements but if you are using raw SQL, they will probably be much longer than a single line. Rectify provides a small module that you can include to makes your query objects cleaner:
class UsersOverUsingSql < Rectify::Query
include Rectify::SqlQuery
def initialize(age)
@age = age
end
def model
User
end
def sql
<<-SQL.strip_heredoc
SELECT *
FROM users
WHERE age > :age
ORDER BY age ASC
SQL
end
def params
{ :age => @age }
end
end
Just include Rectify::SqlQuery
in your query object and then supply the a
model
method that returns the model of the returned objects. A
params
method that returns a hash containing named parameters that the SQL
statement requires. Lastly, you must supply a sql
method that returns the raw
SQL. We recommend using a heredoc which makes the SQL much cleaner and easier
to read. Parameters use the ActiveRecord standard symbol notation as shown above
with the :age
parameter.
Stubbing Query Objects in tests
Now that you have your queries nicely encapsulated, it's now easier with a clear division of responsibility to improve how you use the database within your tests. You should unit test your Query Objects to ensure they return the correct data from a know database state.
What you can now do it stub out these database calls when you use them in other classes. This improves your test code in a couple of ways:
- You need less database setup code within your tests. Normally you might use something like factory_girl to create records in your database and then when your tests run they query this set of data. Stubbing the queries within your tests can reduce this complexity.
- Fewer database queries running and less factory usage means that your tests
- are doing less work and therefore will run a bit faster.
In Rectify, we provide the RSpec helper method stub_query
that will make
stubbing Query Objects easy:
# inside spec/rails_helper.rb
require "rectify/rspec"
RSpec.configure do |config|
# snip ...
config.include Rectify::RSpec::Helpers
end
# within a spec:
it "returns the number of users" do
stub_query(UsersOlderThan, :results => [User.new, User.new])
expect(subject.awesome_method).to eq(2)
end
As a convenience :results
accepts either an array of objects or a single
instance:
stub_query(UsersOlderThan, :results => [User.new, User.new])
stub_query(UsersOlderThan, :results => User.new)
Where do I put my files?
The next inevitable question is "Where do I put my Forms, Commands, Queries and
Presenters?". You could create forms
, commands
, queries
and presenters
folders and follow the Rails Way. Rectify suggests grouping your classes by
feature rather than by pattern. For example, create a folder called core
(this
can be anything) and within that, create a folder for each broad feature of your
application. Something like the following:
.
└── app
├── controllers
├── core
│ ├── billing
│ ├── fulfillment
│ ├── ordering
│ ├── reporting
│ └── security
├── models
└── views
Then you would place your classes in the appropriate feature folder. If you follow this pattern remember to namespace your classes with a matching module which will allow Rails to load them:
# in app/core/billing/send_invoice.rb
module Billing
class SendInvoice < Rectify::Command
# ...
end
end
You don't need to alter your load path as everything in the app
folder is
loaded automatically.
As stated above, if you prefer not to use this method of organizing your code
then that is totally fine. Just create folders under app
for the things in
Rectify that you use:
.
└── app
├── commands
├── controllers
├── forms
├── models
├── presenters
├── queries
└── views
You don't need to make any configuration changes for your preferred folder structure, just use whichever you feel most comfortable with.
Trade offs
This style of Rails architecture is not a silver bullet for all projects. If your app is pretty much just basic CRUD then you are unlikely to get much benefit from this. However, if your app is more than just CRUD then you should see an improvement in code structure and maintainability.
The downside to this approach is that there will be many more classes and files to deal with. This can be tricky as the application gets bigger to hold the whole system in your head. Personally I would prefer that as maintaining it will be easier as all code around a specific user task is on one place.
Before you use these methods in your project, consider the trade off and use these strategies where they make sense for you and your project. It maybe most pragmatic to use a mixture of the classic Rails Way and the Rectify approach depending on the complexity of different areas of your application.
Developing Rectify
Some tests (specifically for Query objects) we need access to a database that
ActiveRecord can connect to. We use SQLite for this at present. When you run the
specs with bundle exec rspec
, the database will be created for you.
There are some Rake tasks to help with the management of this test database using normal(ish) commands from Rails:
rake db:migrate # => Migrates the test database
rake db:schema # => Dumps database schema
rake g:migration # => Create a new migration file (use snake_case name)
Releasing a new version
Bump the version in lib/rectify/version.rb
then do the following:
bundle
gem build rectify.gemspec
gem push rectify-0.0.0.gem