Ruby 💎

Hero image for Ruby 💎

Linting

The team uses RuboCop for automated code checks, and their guidelines largely comply with the default guidelines set by the analyzer.

Formatting

  • Use soft-tabs with a two space indent.
  • Limit each line of code to fewer than 130 characters.
  • Use a single empty line to break between statements to organize logical chunks of code.
  • End each file with a newline.

Naming

  • Use snake_case for symbols, methods and variables.

    :someSymbol
    someVar = 5
    
    def someMethod
      # some code
    end
    
    :some_symbol
    
    some_var = 5
    
    def some_method
      # some code
    end
    
  • Do not separate numbers from letters on symbols, methods and variables.

    :some_sym_1
    some_var_1 = 1
    
    def some_method_1
      # some code
    end
    
    :some_sym1
    
    some_var1 = 1
    
    def some_method1
      # some code
    end
    
  • Use CamelCase for classes and modules.

    class Someclass
      # some code
    end
    
    class Some_Class
      # some code
    end
    
    class SomeClass
      # some code
    end
    
  • Use snake_case for naming files.

    HelloWorld.rb
    
    hello_world.rb
    
  • Use snake_case for naming directories.

    lib/HelloWorld/hello_world.rb
    
    lib/hello_world/hello_world.rb
    
  • Aim to have just a single class/module per source file. Name the file name as the class/module, but replacing CamelCase with snake_case.

  • Use SCREAMING_SNAKE_CASE for other constants.

    SomeConst = 5
    
    SOME_CONST = 5
    
  • The names of predicate methods (methods that return a boolean value) should end in a question mark ?.

    def is_true
    end
    
    def true?
    end
    
  • The names of method which could raise exception should end in bang !.

    def authenticate
      raise UserNotAuthenticated unless authenticate_user(current_user)
    end
    
    def authenticate!
      raise UserNotAuthenticated unless authenticate_user(current_user)
    end
    

Syntax

  • Use the proc invocation shorthand when the invoked method is the only operation of a block.

    names.map { |name| name.upcase }
    
    names.map(&:upcase)
    
  • Prefer {...} over do...end for single-line blocks. Avoid using {...} for multi-line blocks.

    names = %w[Bozhidar Steve Sarah]
    
    names.each do |name|
      puts name
    end
    
    names.each { |name| puts name }
    
  • Use .call explicitly when calling lambdas.

    lambda.(x, y)
    
    lambda.call(x, y)
    
  • Use ... operator when forwarding method arguments to a method.

    def call(*args, **kwargs, &block)
      process(*args, **kwargs, &block)
    end
    
    def call(...)
      process(...)
    end
    

    Ruby 3.0 has added support passing leading arguments along with ... operator which allows writing more flexible methods.

    def create_github_repo(project_type, ...)
      if project_type == 'open_source'
        initialize_public_repo(...)
      else
        initialize_private_repo(...)
      end
    end
    
  • When defining an object of any mutable type meant to be a constant, make sure to call freeze on it. Common examples are strings, arrays, and hashes.

    class Color
      RED = 'red'
      BLUE = 'blue'
      GREEN = 'green'
    
      ALL_COLORS = [
        RED,
        BLUE,
        GREEN,
      ]
    
      COLOR_TO_RGB = {
        RED => 0xFF0000,
        BLUE => 0x0000FF,
        GREEN => 0x00FF00,
      }
    end
    
    class Color
      RED = 'red'.freeze
      BLUE = 'blue'.freeze
      GREEN = 'green'.freeze
    
      ALL_COLORS = [
        RED,
        BLUE,
        GREEN,
      ].freeze
    
      COLOR_TO_RGB = {
        RED => 0xFF0000,
        BLUE => 0x0000FF,
        GREEN => 0x00FF00,
      }.freeze
    end
    

Conditional Expressions

  • Never negate the conditional of an if block. Use unless instead.

    if !false then
      #...
    end
    
    unless false
      #...
    end
    
  • Never use the return value of = in conditionals.

    if is_branded = array.include? 'brand'
      #...
    end
    
    is_branded = array.include? 'brand'
    
    if is_branded
      #...
    end
    
  • Prefer using guard clauses over if/else and ternary operators.

    def message
      if valid?
        'success'
      else
        'error'
      end
    end
    
    def message
      valid? ? 'success' : 'error'
    end
    
    def message
      return 'success' if valid?
    
      'error'
    end
    
  • Never use nested conditionals for flow of control.

    The general principles boil down to:

    • Return immediately if the function cannot do anything more.
    • Reduce nesting and indentation in the code by returning early. This makes the code easier to read and requires less mental bookkeeping on the part of the reader to keep track of else branches.
    • The core or most important flows should be the least indented.
    def compute
      server = find_server
      if server
        client = server.client
        if client
          request = client.make_request
          if request
            process_request(request)
          end
        end
      end
    end
    
    def compute
      server = find_server
      return unless server
    
      client = server.client
      return unless client
    
      request = client.make_request
      return unless request
    
      process_request(request)
    end
    

    Refer to the section “Guard Clause”, p68-70 in Beck, Kent. Implementation Patterns. Upper Saddle River: Addison-Wesley, 2008.

Collections

  • Prefer %w to the literal array syntax for an array of raw strings.

    STATES = ['draft', 'open', 'closed']
    
    # Less Preferred
    STATES = %w(draft open closed)
    
    STATES = %w[draft open closed]
    
  • Use symbols instead of strings as hash keys.

    hash = { 'one' => 1, 'two' => 2, 'three' => 3 }
    
    hash = { one: 1, two: 2, three: 3 }
    
  • Use Hash#key? instead of Hash#has_key? and Hash#value? instead of Hash#has_value?. According to Matz, the longer forms are considered deprecated.

    hash.has_key?(:test)
    hash.has_value?(value)
    
    hash.key?(:test)
    hash.value?(value)
    

Strings

  • Prefer single quotes over double quotes.

    ruby = "Ruby is an interpreted, high-level, general-purpose programming language."
    
    ruby = 'Ruby is an interpreted, high-level, general-purpose programming language.'
    
  • Prefer string interpolation over appending to strings.

    full_name = user.first_name + ' ' + user.last_name
    
    full_name = "#{user.first_name} #{user.last_name}"
    
  • Prefer symbols over strings where applicable.

    person = {
      'name' => 'Yukihiro Matsumoto',
      'age' => 55
    }
    
    person = {
      name: 'Yukihiro Matsumoto',
      age: 55
    }
    
    class User
      attr_accessor 'first_name'
      ...
    end
    
    class User
      attr_accessor :first_name
      ...
    end
    
  • Prefer heredoc to multiline strings.

    notice = "Subscription expiring soon!.\n Your free trial will expire in #{days_until_expiration} days.\n Please update your billing information."
    
    notice = <<~HEREDOC
      Subscription expiring soon!.
      Your free trial will expire in #{days_until_expiration} days.
      Please update your billing information.
    HEREDOC
    
  • Use format when formatting string to a certain format.

    formatted_card_number = "#{card_brand} ******#{card_last_digits}"
    
    CARD_NUMBER_FORMAT = '%{card_brand} ******%{card_last_digits}'.freeze
    
    formatted_card_number = format(CARD_NUMBER_FORMAT, card_brand: card_brand, card_last_digits: card_last_digits)
    

Methods

  • Use parentheses in method definitions only if it accepts any parameters.

    def method_without_params
      # body omitted
    end
    
    def method_with_parameters(param1, param2)
      # body omitted
    end
    
  • Avoid usage of positional arguments. Use keyword arguments or an options hash instead.

    def positional_method(a = 1, b = 2, c, d)
      # method body
    end
    
    def method_with_keyword_arguments(a:, b:, c:, d:)
    end
    
    def method_with_options(options = {})
    end
    
  • Use endless method for writing single liner method (This convention only applicable for Ruby 3 or later).

    def square(x)
      x * x
    end
    
    def square(x) = x * x
    

Modules

  • Use modules to organize a chunk of common stateless operations.

  • Use :: only to reference constants and constructors. Do not use :: for regular method invocation.

    Klass::some_method
    object::some_method
    
    Klass.some_method
    object.some_method
    
  • Properly choose between def self, extend self and module_function.

Let’s consider a module Math implementing a method pi.

Use def self.pi

include Math will not give any sort of accessor to .pi, not YourClass.pi, not instance.pi, nothing. Use Math.pi instead.

Use extend self with public methods

include Math will give instances a public instance method .pi.

Use extend self with private methods

include Math will give a private instance method .pi but Math.pi will say “private method pi called”

Use module_function :pi

include Math will give instances a private .pi method and Math.pi still works just fine.

Classes

  • Code in Ruby classes must follow the following structure:

    # requires
    require 'dependency_filename'
    
    class KlassName
      # extend and include
      extend SomeModule
      include AnotherModule
    
      # constants
      SOME_CONSTANT = '20'.freeze
    
      # attribute macros
      attr_reader :name
    
      # additional macros (if any)
      validates :name
    
      # public class methods
      class << self
        def some_method
          #...
        end
      end
    
      # initialization
      def initialize
      end
    
      # public instance methods
      def some_method
      end
    
      # Group alias methods at the end of public methods
      alias alias_name some_method
      alias alias_alternative_name some_method
    
      # protected methods
      protected
    
      def some_protected_method
      end
    
      # private methods
      private
    
      def some_private_method
      end
    end
    
  • Avoid nesting multiple classes within classes. Instead maintain each in their own file in a folder named like the containing class.

    
    # parent.rb
    class Parent
      class Child
      end
    end
    
    
    # parent.rb
    class Parent
    end
    
    # parent/child.rb
    class Parent
      class Child
      end
    end
    
  • Classes should be used only when it makes sense to create instances out of them. Prefer modules to classes with only class methods.

    class SomeClass
      class << self
        def self.some_method
          #...
        end
    
        def self.some_other_method
          #...
        end
      end
    end
    
    module SomeModule
      def some_method
        #...
      end
    
      def some_other_method
        #...
      end
    end
    
  • Use class << self to define class methods instead of self..

    class SomeKlass
      def self.method_name
        #...
      end
    end
    
    class SomeKlass
      class << self
        def method_name
          #...
        end
      end
    end
    
  • Try to make the classes as SOLID as possible.

  • Use the attr family of functions to define trivial accessors.

    class Klass
      def initialize(a, b)
        @a = a
        @b = b
      end
    
      def a
        @a
      end
    
      def b
        @b
      end
    end
    
    class Klass
      attr_reader :a, :b
    
      def initialize(a, b)
        @a = a
        @b = b
      end
    end
    

Exceptions

  • Never rescue the Exception class. Rescue specific error classes.

    begin
      1/0
    rescue Exception
      # exception handling
    end
    
    begin
      1/0
    rescue ZeroDivisionError
      # exception handling
    end
    

Monkey Patching/Refinements

  • Avoid monkey patching built-in classes, use refinements instead

    class Hash
      def deep_transform_camelize_keys
        deep_transform_keys { |key| key.to_s.camelize(:lower) }
      end
    end
    
    module HashExtensions
      refine Hash do
        def deep_transform_camelize_keys
          deep_transform_keys { |key| key.to_s.camelize(:lower) }
        end
      end
    end