- Basics
- PR quality levels
- Must reads
- What goes where
- Testing
- Other useful and interesting reading material
- Recommended newsletters and subscriptions
- Post Scriptum
- Changelog
-
- 1. Naming.
- 2. Readability (ease of understanding)
- 3. Simplicity (number of parts of any contraption has the most direct impact on it's complexity)
- ... everything else.
-
Readability correlates with semantics, a lot.
Programs must be written for people to read, and only incidentally for machines to execute.
— Harold Abelson, professor of CS at MIT, founding director of both Creative Commons and the Free Software Foundation. (read more about him).
-
Use things according to their intended purpose. Do not use things in ways they were not intended to, just because you can. Unless there is absolutely no other (reasonable) way.
-
Entities are not to be introduced beyond necessity. "Every new abstraction and indirection will independently appear warranted. It’s not until you view the whole integrated system you see the 😱". Less is better.
-
Stick with the defaults. Do not divert, unless there's a really good reason to. Follow principle of least surprise in the context of an average Ruby/Rails developer who will read this code after you. You are not the first one working on this project, and you won't be the last either. Have some humbleness.
- that applies to gems and tools as well. Unless the change is really worth it.
-
It's not a question "will it break" — it will. It's only a question "when", and "how are you going to handle it". "Perfect" does not exist. Expect failure and Plan the recovery.
-
Do not create variables or constants that are used only once.
-
Prefer http://vanilla-js.com to jQuery
-
Tailwind is nice for prototyping, but PITA in the long run.
-
When using states in your models, use consistent naming. Avoid having "active/inactive" in one model, "enabled/disabled" in another, and "on/off" in third.
-
There should always be a single source of truth. That includes configurations, logic, data, and domain-specific information (like importer configuration, for example). Keep the spread and scatter as low as techincally possible.
-
Do not force your personal views and preferences upon others. If it's not backed by objective observations - it's personal, so keep it personal. Educate, not enforce.
-
And last, but not least,
- No code runs faster than no code.
- No code has fewer bugs than no code.
- No code uses less memory than no code.
- No code is easier to understand than no code.
- Unacceptable:
- it doesn't work
- or it doesn't do what it supposed to
- Barely acceptable, only in emergency cases:
- it works, and does what's expected
- it has at least some tests (unless it's a refactoring)
- Acceptable:
- readable
- understandable
- as simple as possible, or at least not overcomplicated
- level of WTF/minute is zero or close to zero
- Good:
- "Da kann man nicht meckern" ("There is nothing to complain about").
- Great:
- you've done a few benchmarks and went with the most optimal solution.
These materials are essential for any ruby/rails developer to read, understand, and accept. If you don't accept it, if you're using a technology while constantly struggling with fighting it - you're not going to have a good time, not at all.
(provided excerpts are only illustrative and are not a summary)
-
The Philosophy of Ruby. A Conversation with Yukihiro Matsumoto, Part I
...
Yukihiro Matsumoto: Ruby inherited the Perl philosophy of having more than one way to do the same thing. I inherited that philosophy from Larry Wall, who is my hero actually. I want to make Ruby users free. I want to give them the freedom to choose. People are different. People choose different criteria. But if there is a better way among many alternatives, I want to encourage that way by making it comfortable. So that's what I've tried to do.
... -
Dynamic Productivity with Ruby. A Conversation with Yukihiro Matsumoto, Part II
-
Blocks and Closures in Ruby. A Conversation with Yukihiro Matsumoto, Part III
-
Matz on Craftsmanship. A Conversation with Yukihiro Matsumoto, Part IV
...
Bill Venners: You also mentioned in your ten top tips: "Be nice to others. Consider interface first: man-to-man, man-to-machine, and machine-to-machine. And again remember the human factor is important." What do you mean by, "consider interface first?"Yukihiro Matsumoto: Interface is everything that we see as a user. If my computer is doing very complex things inside, but that complexity doesn't show up on the surface, I don't care. I don't care if the computer works hard on the inside or not. I just want the right result presented in a good manner. So that means the interface is everything, for a plain computer user at least, when they are using a computer. That's why we need to focus on interface.
...Side note: code is also an interface, prorammer-to-programmer and programmer-to-machine. Be nice to other programmers.
-
A common critique of Rails is that it encourages a poor separation of concerns. That when things get serious, you need an alternative that brings the missing pieces. We disagree.
-
...
Ruby includes a lot of sharp knives in its drawer of features. Not by accident, but by design.This power has frequently been derided as simply too much for mere mortal programmers to handle. People from more restrictive environments used to imagine all sorts of calamities that would doom Ruby because of the immense trust the language showed its speakers with this feature.
There’s nothing programmatically in Ruby to stop you using its sharp knives to cut ties with reason. We enforce such good senses by convention, by nudges, and through education. Not by banning sharp knives from the kitchen and insisting everyone use spoons to slice tomatoes.
It’s always about other programmers when the value of sharp knives is contested. I’ve yet to hear a single programmer put up their hand and say “I can’t trust myself with this power, please take it away from me!”. It’s always “I think other programmers would abuse this”. That line of paternalism has never appealed to me.
That brings us to Rails. The knives provided by the framework are not nearly as sharp as those offered with the language, but some are still plenty keen to cut. We will make no apologies for offering such tools as part of the kit. In fact, we should celebrate having enough faith in the aspirations of our fellow programmers to dare trust them.
... -
"The Parley Letter" — DHH, author of Ruby-on-Rails
...
there's a reason we try not to worry too much about The Future and that's because predicting it is hard. YAGNI and all that. Given that, I think the responsible thing is to make the best damn piece of software facing CURRENT DAY constraints and let tomorrow worry about tomorrow. When the actual changed requirements or new people with "new ideas" roll around, they'll have less baggage to move around. The predictions for what the app is going to look like a decade from now are pretty likely to be wrong anyway. This last part I know all too well because I, like all programmers, have occasionally succumbed to future coding. Only to learn later that what I thought I needed in the future wasn't it at all. And then my task was double because I had to rip out the predicted crap before I could implement the actual work.
...(honestly, the whole article is worth being quoted line for line, so just go there and read it.)
-
...
when people look at, say, Amazon or Google or whoever else might be commanding a fleet of services, and think, hey it works for The Most Successful, I’m sure it’ll work for me too. Bzzzzzzzzt!! Wrong!
... -
...
Best practices are, despite the name, not universally good.Consider this example: someone, through a lot of trial and error, found a good way to tackle a problem. Because of the learning process, they understand the nuances in how and when to apply it.
The solution works for them and they start sharing their lessons as best practice. This gets picked up by people who skipped the learning and went straight to applying it, missing out on some nuance. Those people share it again. A new cohort of people picks it up. They misunderstand it more and share it again.
Soon, all understanding of why the practice works is lost. People are parroting it as a simplified, absolute catchphrase. “Always write the tests before the implementation”.
...
Reading material:
- Read the "Must-reads" section before proceeding.
- At least read the https://guides.rubyonrails.org/initialization.html and https://guides.rubyonrails.org/configuring.html for some context.
- How DHH Organizes His Rails Controllers
- Vanilla Rails is plenty
Conventions:
-
The
Rails.configuration
(which is an alias forRails.application.config
) directives belong inconfig/application.rb
andconfig/environments/*.rb
, and not inconfig/inlializers/*
, except for:config/inlializers/*
files generated by the Rails- "Versioned Default Values"
- "configuration settings that should be made after all of the frameworks and gems are loaded". For example, gem settings, unless different approach is suggested in the library/gem's documentation.
- Do not put computation-heavy code into initializers. Use lazy/on-demand approach where necessary.
- Try to avoid naming initializers in a specific manner to affect their load order, as it leads to unobvious dependencies. Prefer explicit dependencies instead, if necessary, use
require
in the dependent initializer. According to Rails' Guides, "explicitly loading initializers withrequire
is not recommended, since it will cause the initializer to get loaded twice", but sometimes it is better to load the code, which merely sets a variable (hence the computation note), twice, than introduce an unobvious dependency.
-
Non-sensitive configuration options are set in
config/application.rb
(andconfig/environments/*.rb
if needed). Use custom configuration mechanism for big/complex/complicated data structures. -
For storing sensitive values (like tokens, passwords, keys, etc.) use
Rails.application.credentials
, unless it's something that is needed before Rails is able to decrypt it (examples are yet to be seen)- To access encrypted DB credentials in
config/database.yml
use<%= Rails.application.credentials.dig(:production, :database_password) %>
- All sensitive configuration should additionally be documented and stored in a secure manner (for example, LastPass, 1Pass, AWS Secrets, etc.)
- To access encrypted DB credentials in
-
Use
ENV
vars configuration mechanism only if credentials mechanism is not enough (examples are yet to be seen), not just because you've used to be doing it that way or because something did not worked out 10 years ago. -
Keep configuration organized, confined, and tidy.
- By increasing the number of configuration entries and/or number places where you have to look for configurations, you also increase the odds of forgetting to add something somwhere, make a mistake, and add to time spent trying to find where the hell is it defined and what is the current value of it.
- Think twice before making something a configuration option - does it really needs to be configurable?..
- Follow "convention over configuration" - it's Rails, after all.
-
Classes and modules that are ...
- copies from external sources - should be placed in
vendor/lib/
- copies from external sources that have been edited in-house should go to
lib/
and should contain appropriate comment at the very beginning of the file, indicating source (url, if available), date and time when copy was made, and timestamp & purpose of the changes. If possible, a comments wrapping the changed sections of the code (i.e.: "# edit start %reason%" ... # edit end) would be a nice touch. NOTE: Before editing such files always make a commit with initial unedited vanilla version first, this will make it easier to track and see all the changes when compared to the original. - in-house overrides, backports, hooks, tweaks, and patches of external libs, gems, or gem parts -
lib/
- non-application-specific (can be copy-pasted and reused in any other project without changes) - should go to
lib/
. Examples:Downloaders::Ftp
inlib/downloaders/ftp.rb
ApiClients::Discourse
inlib/api_clients/discourse.rb
TimeHelpers
inlib/time_helpers.rb
(but try not to clutter thelib/
too much. Consider alternatives, for exampleHelpers::Time
inlib/helpers/time.rb
)
- copies from external sources - should be placed in
-
Classes and modules that are application-specific (would not work outside of the app without modifying)... Now, there are two general approaches here:
- Basecamp team (guys behind Rails) consider
app/models/
to reflect the domain, and thus put everything intoapp/models/
. So all classes: AR models, importers, api clients, fetchers, utils, etc - all live together in one place, but scoped under the domain it belongs to, and all the model-related functionality is handled by PORO & concerns. Contrary to what you might think immediately after reading this, things do not actually look as bad as you would expect. See this link1 and link2 for more details. The idea is to model your code structure according to the data domain, and not by purpose. This will result in, for example,Offer::Importer
,Offer::Importer::SomeDataProvider
,Offer::SomeScraper
,Discourse::ApiClient
,Car::Search
,Car::Search::Attribute::Price
, and so on. - If you prefer to structure the code not by domain but by purpose, then consider the
app/lib/
structure:app/lib/utils/
,app/lib/helpers/
,app/lib/importers/
,app/lib/services/
, etc. For example:Importers::SomeDataProvider
inapp/lib/importers/some_data_provider.rb
ApiClients::Discourse
inapp/lib/api_clients/discourse.rb
Scrapers::SomeDataScraper
inapp/lib/scrapers/some_data_scraper.rb
Search::Car
inapp/lib/search/car.rb
Search::Attributes::Price
inapp/lib/search/attributes/price.rb
- and leave
app/models/
reserved for ActiveRecord models exclusively, to make it easier to navigate and declutter
- Third option would be putting everything in the
app/**
, akin to controllers, helpers, mailers, jobs, channels, etc.:app/importers/some_dataprovider_importer.rb
,app/api_clients/discourse_api_client.rb
,app/scrapers/some_data_scraper.rb
, and so on, but beware of the bloat.
Which one to chose - is a really tough question. Answer to which is: it depends.
If you're familiar with, and fond of, Domain Driven Design, go The Rails/Basecamp/DHH Way.
But if that concept is too complex to grasp and/or you can't imagine the structure of your app to be built in a way Basecamp is, go 2 or 3. The difference between those two is, essentially, in the class naming: if you'd preferImporters::SomeProvider
,Importers::Another
,ApiClients::Discourse
,DataScrapers::Someting
orSomeProviderImporter
,AnoterImporter
,DiscourseApiClient
,SomeDataScraper
. A matter of taste, really. - Basecamp team (guys behind Rails) consider
-
Migrations
-
"Is there any “official” way to organize one-off scripts?" (answered by DHH)
-
Use System tests if you need to test JS and/or user interaction with the application. It utilises Capybara and run a (optionally headless) browser.
-
Use Integration tests if you don't need to test JS, things like object viewport visibility, or itneraction with the application, and if checking plain html response is just about enough. It uses rack test driver and operates on plain html level, and does not run JS nor browser, which makes it quite faster that Capybara-and-browser-based.
-
In integration and system tests avoid visiting the same page more than once in the same test class/group. Visiting pages is slow. Do all necessary assertions in one visit.
-
Avoid using
wait_for_ajax
. Instead, write tests in a way that would make Capybara take care of it, by using assertions likeassert_text
,assert_current_path
,assert_[all|any|none]_of_selectors
,assert_selector
(or, basically, any assertion that is based on selectors), or any other assertion that accepts the:wait
kwarg. See [1], [2], [3] -
https://brandonhilkert.com/blog/7-reasons-why-im-sticking-with-minitest-and-fixtures-in-rails/ (Capybara & DatabaseCleaner part of the article is no longer relevant since Rails 5.1)
-
How Boeing learned that integration tests really are important, a $1'500'000'000 ($1.5 billion) lesson.
-
opinions and authority
- "People who created The Thing have the highest authority on any topic of that Thing. On all things Ruby - Matz is the Prophet, Lord, and Saviour in one. No opinion has higher value than his. On all things Rails - DHH. Perl - Larry Wall. Java - James Gosling. Crystal - Borenszweig, Wajnerman, Cardiff. For every and any technology out there - the creators of it have the highest argument weight in any question whatsoever - because nobody knows it better, than the ones who made it."
-
microservices vs monolith
-
Ruby
- "Ruby has been fast enough for 13 years" (13 at the time of writing in 2016. In 2023 it's 20)
-
Rails
- Rails is omakase
- N+1 in Rails is not a bug, it's a feature (mind blowing, I know, but do have a look)
-
work processes
-
application architecture
-
anti-patterns
- Anemic Domain Model by Martin Fowler
- Excessive Loose Coupling
- Why Service Objects are an Anti-Pattern by Jared White
- Beware of “service objects” in Rails by Jason Swett
-
code duplication and "Rule of Three" by Jason Swett
- Duplication exists when there’s a single behavior that’s specified in two or more places.
- <...> it leaves a program susceptible to developing logical inconsistencies.
- The rule of three/”write everything twice” makes little sense because it doesn’t take the factors into account that determine whether a piece of duplication is dangerous or innocuous.
-
on the topic of the so-called "Law" of Demeter
-
about "Single Responsibility Principle"
- "I don't love the single responsibility principle"
- a well said comment in discussion on Rails and Single Responsibility Principle:
[...] always take Uncle Bob’s rhetoric with a grain of salt. Nothing he says is backed by any empirical evidence. He’s just another extremely closed minded person giving his unsubstantiated but extremely strong opinions about software. I’m not saying I disagree with him. Just be cautious. He is unlikely to recognize or admit when he’s wrong.
- "I didn't think it was possible, but Single Responsibility Principle appears to be producing even more bullshit code than Law of Demeter." — DHH
-
about "SOLID"
-
other "best" practices, principles, and "rules"
- It's probably time to stop recommending "Clean Code" + discussion of the topic
- Don’t follow best programming practices. Do this instead.
- The Cult of Best Practice
- Principles Wiki - This wiki is a place to collect, examine, and discuss software design knowledge in a systematic way.
- "Every new abstraction and indirection will independently appear warranted. It’s not until you view the whole integrated system you see the 😱" — DHH
- "Rules are for the obedience of fools and the guidance of wise."
- “It’s not wise to violate rules until you know how to observe them.”, “Some rules are nothing but old habits that people are afraid to change.”, and more
-
the good stuff
- David Heinemeier Hansson
- This Week in Rails
- Ruby Weekly
- Short Ruby Newsletter
- Awesome Ruby Newsletter
...and most importantly, remember, take everything with a grain of salt, including these guidelines. They're not "laws". They're there to give you a general direction to move on, not tell you what to do and what not to, like some of the "laws" do. No one is going to jail nor murder you for not following them. Probably.
"The young man knows the rules, but the old man knows the exceptions" -- The New York Medical Journal, volume XIII, 1871. Harward University. School of Medicine and Public Health.