Introduction
Ruby on Rails is almost a decade old, and its community has developed a number of principles for building applications that are fast, fun, and easy to change: don't repeat yourself, keep your views dumb, keep your controllers skinny, and keep business logic in your models. These principles carry most applications to their first release or beyond.
However, these principles only get you so far. After a few releases, most applications begin to suffer. Models become fat, classes become few and large, tests become slow, and changes become painful. In many applications, there comes a day when the developers realize that there's no going back; the application is a twisted mess, and the only way out is a rewrite or a new job.
Fortunately, it doesn't have to be this way. Developers have been using object-oriented programming for several decades, and there's a wealth of knowledge out there which still applies to developing applications today. We can use the lessons learned by these developers to write good Rails applications by applying good object-oriented programming.
Ruby Science will outline a process for detecting emerging problems in code, and will dive into the solutions, old and new.
Code Reviews
Our first step towards better code is to review it.
Have you ever sent an email with typos? Did you review what you wrote before clicking "Send"? Reviewing your e-mails prevents mistakes and reviewing your code does the same.
To make it easier to review code, always work in a feature branch. The branch reduces the temptation to push unreviewed code or to wait too long to push code.
The first person who should review every line of your code is you. Before committing new code, read each changed line. Use git's diff
and --patch
features to examine code before you commit. Read more about these features using git help add
and git help commit
.
If you're working on a team, push your feature branch and invite your teammates to review the changes via git diff origin/master..HEAD
.
Team review reveals how understandable code is to someone other than the author. Your team members' understanding now is a good indicator of your understanding in the future.
However, what should you and your teammates look for during review?
Follow Your Nose
Code "smells" are indicators something may be wrong. They are useful because they are easy to see, sometimes easier than the root cause of a problem.
When you review code, watch for smells. Consider whether refactoring the code to remove the smell would result in better code. If you're reviewing a teammate's feature branch, share your best refactoring ideas with them.
Smells are associated with one or more refactorings (ex: remove the Long Method smell using the Extract Method refactoring). Learn these associations in order to quickly consider them during review, and whether the result (ex: several small methods) improves the code.
Don't treat code smells as bugs. It will be a waste of time to "fix" every smell. Not every smell is the symptom of a problem and despite your best intentions, you can accidentally introduce another smell or problem.
Removing Resistance
Another opportunity for refactoring is when you're having difficulty making a a change to existing code. This is called "resistance." The refactoring you choose depends on the type of resistance.
Is it hard to determine where new code belongs? The code is not readable enough. Rename methods and variables until it's obvious where your change belongs. This and every subsequent change will be easier. Refactor for readability first.
Is it hard to change the code without breaking existing code? Add extension points or extract code to be easier to reuse, and then try to introduce your change. Repeat this process until the change you want is easy to introduce.
Each change should be easy to introduce. If it's not, refactor.
When you are making your changes, you will be in a feature branch. Try to make your change without refactoring. If your meet resistance, make a "work in progress" commit, check out master, and create a new refactoring branch:
git commit -m 'wip: new feature'git pushgit checkout mastergit checkout -b refactoring-for-new-feature
Refactor until you fix the resistance you met on your feature branch. Then, rebase your feature branch on top of your refactoring branch:
git rebase -i new-featuregit checkout new-featuregit merge refactoring-for-new-feature --ff-only
If the change is easier now, continue in your feature branch. If not, check out your refactoring branch and try again.
Bugs and Churn
If you're spending a lot of time swatting bugs, remove smells in the methods or classes of the buggy code. You'll make it less likely that a bug will be reintroduced.
After you commit a bug fix to a feature branch, find out if the code you changed to fix the bug is in files which change often. If the buggy code changes often, find smells and eliminate them. Separate the parts that change often from the parts that don't.
Conversely, avoid refactoring areas with low churn. Refactoring changes code, and with each change, you risk introducing new bugs. If a file hasn't changed in six months, leave it alone. It may not be pretty, but you'll spend more time looking at it when you break it trying to fix something that wasn't broken.
Metrics
Various tools are available which can aid you in your search for code smells.
You can use flog to detect complex parts of code. If you look at the classes and methods with the highest flog score, you'll probably find a few smells worth investigating.
Duplication is one of the hardest problems to find by hand. If you're using diffs during code reviews, it will be invisible when you copy and paste existing methods. The original method will be unchanged and won't show up in the diff, so unless the reviewer knows and remembers that the original existed, they won't notice that the copied method isn't just a new addition. Use flay to find duplication. Every duplicated piece of code is a bug waiting to happen.
When looking for smells, reek can find certain smells reliably and quickly. Attempting to maintain a "reek free" code base is costly, but using reek once you discover a problematic class or method may help you find the solution.
To find files with a high churn rate, try out the aptly-named churn gem. This works best with Git, but will also work with Subversion.
You can also use Code Climate, a hosted tool which will scan your code for issues every time you push to Git. Code Climate attempts to locate hot spots for refactoring and assigns each class a simple A through F grade.
If you'd prefer not to use a hosted service, you can use MetricFu to run a large suite of tools to analyze your application.
Getting obsessed with the counts and scores from these tools will distract from the actual issues in your code, but it's worthwhile to run them continually and watch out for potential warning signs.
How To Read This Book
This book contains three catalogs: smells, solutions, and principles.
Start by looking up a smell that sounds familiar. Each chapter on smells explains the potential problems each smell may reveal and references possible solutions.