Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alfuken/d5b4dd5d5de0e898f1286a7ba1c0487f to your computer and use it in GitHub Desktop.
Save alfuken/d5b4dd5d5de0e898f1286a7ba1c0487f to your computer and use it in GitHub Desktop.

Ruby and Rails development guidelines and principles, draft 12.02.2023. RFC.

TOC

  1. Basics
  2. PR quality levels
  3. Must reads
  4. What goes where
  5. Testing
  6. Other useful and interesting reading material
  7. Recommended newsletters and subscriptions
  8. Post Scriptum
  9. Changelog

Basics

PR quality levels (each next includes all preceeding)

  1. Unacceptable:
    • it doesn't work
    • or it doesn't do what it supposed to
  2. Barely acceptable, only in emergency cases:
    • it works, and does what's expected
    • it has at least some tests (unless it's a refactoring)
  3. Acceptable:
    • readable
    • understandable
    • as simple as possible, or at least not overcomplicated
    • level of WTF/minute is zero or close to zero
  4. Good:
    • "Da kann man nicht meckern" ("There is nothing to complain about").
  5. Great:
    • you've done a few benchmarks and went with the most optimal solution.

Must-reads

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.

  • 10 Tips from the Ruby Creator

  • Vanilla Rails is plenty

    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.

  • The Rails Doctrine

    ...
    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.
    ...

  • guides.rubyonrails.org

  • "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.)

  • The Majestic Monolith

    ...
    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!
    ...

  • The Cult of Best Practice

    ...
    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”.
    ...

What goes where

Reading material:

Conventions:

  • The Rails.configuration (which is an alias for Rails.application.config) directives belong in config/application.rb and config/environments/*.rb, and not in config/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 with require 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 (and config/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.)
  • 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 in lib/downloaders/ftp.rb
      • ApiClients::Discourse in lib/api_clients/discourse.rb
      • TimeHelpers in lib/time_helpers.rb (but try not to clutter the lib/ too much. Consider alternatives, for example Helpers::Time in lib/helpers/time.rb)
  • Classes and modules that are application-specific (would not work outside of the app without modifying)... Now, there are two general approaches here:

    1. Basecamp team (guys behind Rails) consider app/models/ to reflect the domain, and thus put everything into app/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.
    2. 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 in app/lib/importers/some_data_provider.rb
      • ApiClients::Discourse in app/lib/api_clients/discourse.rb
      • Scrapers::SomeDataScraper in app/lib/scrapers/some_data_scraper.rb
      • Search::Car in app/lib/search/car.rb
      • Search::Attributes::Price in app/lib/search/attributes/price.rb
      • and leave app/models/ reserved for ActiveRecord models exclusively, to make it easier to navigate and declutter
    3. 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 prefer Importers::SomeProvider, Importers::Another, ApiClients::Discourse, DataScrapers::Someting or SomeProviderImporter, AnoterImporter, DiscourseApiClient, SomeDataScraper. A matter of taste, really.

  • Migrations

  • "Is there any “official” way to organize one-off scripts?" (answered by DHH)

Testing

  • 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 like assert_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://speakerdeck.com/ahawkins/bow-before-minitest

  • 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.

Other useful and interesting reading material

in addition to "Must-reads" section.
Articles, quotes, discussions, comments.
RFC.

Recommended newsletters and subscriptions

Post Scriptum

...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.

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