Apply the YAGNI and KISS principles to all of the following.
- General architecture
- Product and API features
- Implementation specifics
Make an effort for code to be self documenting.
- Prefer descriptive names in your code. e.g.
user_count
is a better name thanlen
. - Use YARD formatted comments when code documentation is deemed necessary.
- Avoid in method comments as they are a cue that the method is too complex; instead, refactor into additional classes/methods that better express intent & purpose.
These guidelines are based on Sandi Metz's programming "rules" which she introduced on Ruby Rogues.
The rules are purposefully aggressive and are designed to give you pause so your app won't run amok. It's expected that you will break them for pragmatic reasons... just be sure that you aware of the trade-offs.
- Classes can be no longer than 100 lines of code.
- Methods can be no longer than 5 lines of code.
- Methods can take a maximum of 4 parameters.
- Controllers should only instantiate 1 object.
- Views should only have access to 1 instance variable.
- Never directly reference another class/module from within a class. Such references should be passed in.
Be thoughtful when applying these rules. If you find yourself fighting Rails too often, a more pragmatic approach might be warranted.
- Never use dynamic finders. e.g.
find_by_...
- Apply extreme caution when using callbacks and observers as they typically lead to unwanted coupling.
All models should be organized using the following format.
class MyModel < ActiveRecord::Base
# extends ...................................................................
# includes ..................................................................
# relationships .............................................................
# validations ...............................................................
# callbacks .................................................................
# scopes ....................................................................
# additional config (i.e. accepts_nested_attribute_for etc...) ..............
# class methods .............................................................
# public instance methods ...................................................
# protected instance methods ................................................
# private instance methods ..................................................
end
NOTE: The comments listed above should exist in the file to serve as a visual reminder of the format.
Controllers should use strong parameters to sanitize params before performing any other logic.
Its generally a good idea to isolate concerns into separate modules. Use concerns as outlined in this blog post.
- Concerns should be created in the
app/COMPONENT/concerns
directory. - Concerns should use the naming convention
COMPONENT
+BEHAVIOR
. For example,app/models/concerns/model_has_status.rb
orapp/controllers/concerns/controller_supports_cors.rb
. - CRUD operations that are limited to a single model should be implemented in the model.
For example, a
full_name
method that concatsfirst_name
andlast_name
- CRUD operations that reach beyond this model should be implemented as a Concern.
For example, a
status
method that needs to look at a different model to calculate. - Simple non-CRUD operations should be implemented as a Concern.
- Important! Concerns should be isolated and self contained. They should NOT make assumptions about how the receiver is composed at runtime. It's unacceptable for a concern to invoke methods defined in other concerns; however, invoking methods defined in the intended receiver is permissible.
- Be thoughtful about monkey patching and look for alternative solutions first.
- Use an
initializer
to load extensions & monkey patches.
All extensions & monkey patches should live under an extensions
directory in lib
.
|-project
|-app
|-config
|-db
|-lib
|-extensions <-----
Use modules to extend objects or add monkey patches. This provides some introspection assistance when you need to track down weirdness.
Here's an example:
module CowboyString
def downcase
self.upcase
end
end
::String.send(:include, CowboyString)
String.ancestors # => [String, CowboyString, Enumerable, Comparable, Object, Kernel]
Unfortunately, some of the defaults in Rails seem to encourage monolithic design. This is especially true for developers new to the framework. However, it's important to note that Ruby & Rails include everything you need to create well organized projects.
The key is to keep concerns physically isolated. Applying the right strategies will ensure your project is testable and maintainable well into the future.
Meaningful projects warrant the creation of domain objects, which can usually be grouped into categories like:
- policies
- presenters
- services
- etc...
Domain objects should be treated as first class citizens of your Rails application.
As such they should live under app/DOMAIN
.
For example:
app/policies
app/presenters
- etc...
Always apply Rails-like standards to domain object names.
For example, app/presenters/user_presenter.rb
Additional reading on the subject of domain objects.
Sometimes concerns should be grouped into isolated libraries. This creates clear separation & allows strict control of depedencies.
Note: This approach does not require you to open-source parts of the app.
Bundler supports 2 strategies that facilitate this type of application structure.
- Locally pathed gems - look for the
:path
option - Git hosted gems
You can also host a local Gemserver.
It can be daunting to know when creating a gem or engine is appropriate. Stephan Hagemann's presentation at Mountain West Ruby provides some guidance. He is also writing a book on Component based Rails Applications.
Custom gems & engines should be created under the vendor directory within your project.
|-project
|-vendor
|-assets
|-engines <-----
|-gems <-----
Additional reading on creating modular Rails applications.
- How to design for loosely-coupled, highly-cohesive components within a Rails application
- Migrating from a single Rails app to a suite of Rails engines
- Rails Engines at TaskRabbit
Here are some popular Rails engines that illustrate how to properly isolate responsibilities to achieve modularity.
- https://github.com/plataformatec/devise
- https://github.com/spree/spree
- https://github.com/radiant/radiant
- https://github.com/refinery/refinerycms
- https://github.com/seyhunak/twitter-bootstrap-rails
Good software design often emerges empirically from implementation. The practice of continually moving toward better design is known as refactoring. Plan on a persistent effort to combat code's natural state of entropy. Use prudence to ensure you don't attempt refactoring too much at once.
- Refactor to methods before classes
- Refactor to classes before libraries
- Refactor to libraries before services
- Refactor to libraries and/or services before a rewrite
Gem dependencies should be hardened before deploying the application to production. This will ensure application stability.
We recommend using exact or tilde version specifiers. When using tilde specifiers, be sure to include at least the major & minor numbers. Here's an example.
# Gemfile
gem 'rails', '3.2.11' # GOOD: exact
gem 'pg', '~>0.9' # GOOD: tilde
gem 'yell', '>=1.2' # BAD: unspecific
gem 'nokogiri' # BAD: unversioned
Bundler's Gemfile.lock solves the same problem, but we discovered that inadvertent bundle updates
can cause problems.
It's much better to be explicit in the Gemfile and guarantee app stability.
This will cause your project to slowy drift away from the bleeding edge.
A strategy should be employed to ensure the project doesn't drift too far from contemporary gem versions.
For example, upgrade gems on a regular schedule (every 3-4 months) and be vigilant about security patches.
Using bundle outdated
will help with this.
It's a good idea to run regular code analysis against your project. Here are some of the tools we use.
Exciting things are happening in the world of client side frameworks.
Be thoughtful about the decision to use a client side framework. Ask yourself if the complexity of maintaining 2 independent full stacks is the right decision. Often times there are better and simpler solutions.
Read the following articles before deciding. In the end, you should be able to articulate why your decision is the right one.
- How Basecamp Next got to be so damn fast without using much client-side UI
- Rails in Realtime
- Rails in Realtime, Part 2
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>JavaScript is like a spice. Best used in sprinkles and moderation.
— DHH (@dhh) September 2, 2013
We generally agree with DHH regarding client side frameworks and have discovered that thoughtful use of something like Vue meets most of our needs.