Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Notes from Growing Rails Applications in Practice

Growing Rails Applications in Practice

  • How to use discipline, consistency and code organization to make your code grow more gently.

  • As you cycle through patterns, your application is becoming a patchwork of different coding techniques.

    All those new techniques actually help, or if you are just adding layers of inderection.

  • Large applications are large so what we can do is organize a codebase in a way that "scales logarithmically".

New rules for Rails

Introduction to design conventions for controllers and user-facing models.

Beautiful controllers

It's hard to decide where to put a bit of functionality (controller or model): email-notification for example.

Any kind of code is hard to test, because requires a complex environment.

Following a few simple guidelines we can reduce the importance of controllers in application an move the controller code to a better place.

Consistent controller design

Use a standard controller design for every single user interaction. This reduce the mental overhead required to navigate through the application to understanding what is happening.

Normalizing user interactions

The pattern we use is to reduce every user interaction to a Rails CRUD resource. Even interations that do not look like plain old CRUD resources can be modeled as such.

A better controller implementation

  • Controllers should be short, DRY and easy to read
  • Controllers should provide the minimum amount of glue code to negotiate between request and model

Why have controllers at all?

  • Security (authentication, authorization)
  • Parsing and white-listing parameters
  • Loading or instantiating the model
  • Deciding which view to render

There are gems like Inherited Resources or Resource Controller that generates a uniform controller implementation, but there are too much magic and too much implicit behavior.

Relearning ActiveRecord

Developers burnt by large Rails applications often blame their pain on ActiveRecord.

ActiveRecord is focused around error handling and input validation. It allows you to collect and process possibly broken user with a minimum amount of code in controllers and views.

Undesrtanding the ActiveRecord lifecycle

ActiveRecord requires your models to be written in a certain style on order to be effective.

Example of Invite class:

 class Invite < ActiveRecord::Base
  def accept!(user)
    self.user = user
    self.accepted = true

    Membership.create!(user: user)

    save!
  end
 end

The problem with the code is that there are a dozen ways to the circumvent the accept! method by setting the user or accepeted attribute. One the most basic OOP principles is that is should be very hard to misuse a class.

 class Invite < ActiveRecord::Base
  after_save :create_membership_on_accept

  def create_membership_on_accept
    if accepted? && accepted_changed?
      Membership.create!(user: user)
    end
  end
 end

This way we can ensure data integrity because ActiveRecord guarantees that callbacks will be called.

Aren't callbacks evil? -> Dealing with Fat models

The true API of ActiveRecord models

  • You instantiate a record using any API that suits your needs.
  • You manipulate the record using any API that suits your needs.
  • Manipulating a record does not automatically commit changes to the database.
  • Once a record passes validations all changes can be commited to the database in a single transaction.

Befefits to embrace the restriction using callbacks:

  • Your views become significantly simpler by leveraging ActiveRecord's error tracking.
  • Develpers no longer need to understand a custom API for each model.
  • It is no longer possoble to accidentally misuse a model.
  • You can use a standard, lean controller design.
  • You will find that there are many libraries that works with the standard ActiveRecord API (state_machine, paper_trail).

User interactions without a database

There are many UI interactions that do not result in a database change at all. This chapter shows how to configure ActiveModel to give all conviniences of ActiveRecord without many lines of controller code.

Example: SiginIn using ActiveModel Example: SiginIn using ActiveType (gem to define the of the attributes I've a felling that is the same behavior of Virtuos)

Refactoring controllers from hell

ActiveModel based classes are a fantastic way to refactor controllers from hell.

Example: Merge Accounts

When we use some ActiveModel class we can unit-test much easier than in a controller, we can become the view much shorter ans easier to read and simplify the controller bahavior:

  • Instantiate an object
  • Addign attributes from the params
  • Try to save the object
  • Render a view or redirect

Creating a system for growth

Dealing with far models

Some problems of fat models:

  • Save record trigger undesired callbacks
  • Too many validations and callbacks make it harder to test
  • Different UI screens require different support code from your model

Why models grow fat

The main reason why models increase in size is that they must serve more and more purposes over time.

Example of User model that supports a lot of use cases:

  • New registration form
  • Reset password
  • Login via Facebook

The case of the missing classes

Fat models are often the symptoms of undiscovered classes:

  • PasswordRecovery
  • AdminUserForm
  • RegistrationForm
  • ProfileForm
  • FacebookConnect

Getting into a habit of organizing

When you are looking for a place to add new code, don't immediately look for an existing ActiveRecord class. Instead of look for new classes to contain that new logic.

A home for interation-specific code

The first step to deal with a fat model should be to reduce it's a slicm core model with a minimum:

  • Set of validations
  • Definitions for associations
  • Convinience methods to find or manipulate records

Move interaction-specific logic to better place like form models, which only exist to facilitate a single UI interation.

A modest approach to form models

Vanila inheritance is all we need in order to extract screen-specific code into their own form models.

Example of User model that contains a sign up logic:

class User < ActiveRecord::Base
  validates :email, presence: true, uniqueness: true
  validates :password, presence: true, confirmation: true
  validates :terms, acceptance: true

  after_create :send_welcome_email

  private

  def send_welcome_email
    Mailer.welcome(self).deliver
  end
end

Reduce this obese model to the minimum amount of code:

class User < ActiveRecord::Base
  validates :email, presence: true, uniqueness: true
end

Move the part that pertain to the sign up form:

class User::AsSignUp < User
  validates :password, presence: true, confirmation: true
  validates :terms, acceptance: true

  after_create :send_welcome_email

  private

  def send_welcome_email
    Mailer.welcome(self).deliver
  end
end

Look at ActiveType (I prefer to use only Rails features).

Extracting service objects

When looking through a fat model, you will usually find a lot of code that does not need to be tied to ActiveRecord or ActiveModel.

SUch code should be extracted into plain Ruby classes that do one job and do them well.

Example: Note::Search

When we extract a commom behavior to a service class and need to change this behavior there's one point of the application to change.

Organizing large codebases with namespaces

Namespace models into sub-folders, this make much easier to browse through your model and highlights the important parts.

class Invoice < ActiveRecord::Base
 has_many :items
end

class Item < ActiveRecord::Base
 belongs_to :invoice
end

Nesting Item into the Invoice namespace:

class Invoice::Item < ActiveRecord::Base
 belongs_to :invoice
end

When you start using namespace, make sure that namespacing is also adopted in all the other places (helpers, controller, views ans tests) that are organized by model.

Taming stylesheets

Related problems:

  • Afraid to change a style because it might break screns your are not aware of;
  • Styles being overshadowed by othes styles that are more specific because of rules you do not care about;

In CSS every attribute of every selector can be overshadowed by any number of attributes from any other selector. The sum of all side effects of all selectors determines how the site renders in the end.

An API for your stylesheets:

  • We want to restrict how styles can influence each other
  • We want to encourage the reuse of styles
  • We want to limit code duplication
  • We want to easily see which styles are already available
  • We want clear rules where to find existing styles
  • We want clear rules how to add new styles
  • We want to be able to refactor styles without the fear of removing an intended side effect

BEM (Block Element Modifier) = it divides responsabilities and provides a clear API for your styles.

Blocks

BEM stylesheets consist of a simple, flat list of blocks.

Think of blocks as the classes of your stylesshet API:

.navigation {

}

.article {

}

.buttons {

}

Elements

Think of the elements as the methods of your stylesheet classes (blocks). Elements are also implemented as simples CSS selectors, prefixed with the block name:

<div class="article">
  <div class="article_title">
    Awesome article
  </div>

  <div class="article_text">
    Awesome text
  </div>
</div>

We avoid nesting selectors to evade selector specificty. This keeps the overshadowing rules very simple - styles can be overwritten by styles further down in the file. It can also improve browser rendering performance.

Modifiers

Think of modifiers as the optional parameters of your stylesheet classes (blocks) and methods (elements).

Modifiers use the cascade to overshadow existing styles from the same block:

.button {
  backgroud-color: #666;
}

.buton.is_primary {
  backgroud-color: #35d;
}

The BEM prime directive: A block must never influence the style of other blocks.

Example tha violates BEM prime directive:

.article {

}

.sidebar .article {
  font-size: 12px;
}

.article is no longer an independent block, since changes in .sidebar can now break .article

Resolving violations

Make .article an elements of .sidebar

.sidebar {

}

.sidebar_article {
  font-size: 12px;
}

Give .article a well-named modifier like .is_summary

.article {

}

.article.is_summary {
  font-size: 12px;
}

.sidebar {

}

There is no coupling between the two selectors, they can both be used and refactored independently form each other.

Full BEM layout example

Organizing stylesheets

We recommend to use one file for each BEM block, to enforce the independence of blocks

normalize.css
blocks/
  button.css
  columns.css
  layout.css

Living style guides

A great way to make your team care for your library of BEM blocks is to use a living style guide.

Building applications to last

On following fashions

Social media is buzzing with praise for a new architectural pattern or new gem that promises to completely change the way you think about Ruby.

Before/After code comparisons

A good way to judge a new technique is to rewrite a small part of your application and compare the code before and after:

  • Does the code feel easier to read and change after the refactoring?

Understanding trafe-offs

One techinique might have prettier syntax, but make your classes harder to test. Another pattern might make your code a lot shorter, but at the price of too much magic behavior going on behind the scenes.

The value of consistency

MVC style of Rails will carry you a long way if you are well-organized and consistent in your decisions.

If you follow every fashion as soon as it arises, your code base will be a patchwork of different styles, making it very hard to understand or change.

Consider the cost of migration your complete application or bundering your team with the mental overhead of having to understand multiple code styles.

Surviving the upgrade pace of Rails

Once you get too too far behind the latest version of Rails you will not receive any further secutity updates.

Gems inscrease the cost of upgrades

Adding new gem dependency, consider the cost of upgrading that gem through the lifespan of your application.

Example: a library that supplies geographical calculations might not even have a dependency on Rails ans is unlikely to ever break during an upgrade.

Upgrades are when you pay for monkey patches

Monkey patches are usually the first thing that break when upgrading Rails.

If you find a bug in a gem that you like, consider forking the gem, commiting your fix a test and creating a pull request to the original author.

Don't live in the bleeding edge

There is no need to upgrade to a new version of Rails on the day it becomes available.

Owning your stack

When you add a gem or technology to your stack, you must understand that you now own that component

In order to decide whether adding a new library is worth the cost of ownership:

  • Take a look at the source code. Does the code look well-groomed or is it a mess of long classes or methods?
  • Does it have tests?
  • Is the library under active development?
  • Are you ready to maintain or replace this library if the maintainers lose interest?

The value of tests

Learning to test your applications effectively might be the most important skill you can learn in your entire career.

Some benefits:

  • The frequency of bugs is reduced to a point where you don't think about bugs at all anymore.
  • The ability to release often. Since you don't need to manualyy test every part of your application after a change.
  • The freedom to refactor without fear.
  • The ability to work on one part of the application without knowing the rest.

Some drawbacks:

  • Time to write.
  • Time to run.
  • Amount of code you need to maintain during changes.
  • Wrinting tests is a completly separate skill set you need to learn.

Chosing test types effectively

Unit test to describe the fine-grained behavior of classes and methods. Full-stack integration to test to a test the behavior of the user at some point at the application.

  • Try to cover as much ground as possible using unit tests, wich are much faster and easier to set-up
  • Every UI screen shoulb be "kissed" by at least one full-stack integration test (without cover every edge case)

Better design guided by test

Getting started with tests in legacy applications

Writing a full-stack integration test that walks along the "happy path"that is most central to your application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment