# Core Architecture Standards for Ruby
This document outlines the core architectural standards for Ruby projects, focusing on maintainability, performance, and security. These standards are designed to guide development teams and provide context for AI coding assistants to generate high-quality, consistent Ruby code. This standard is tailored specifically to the latest version of Ruby and emphasizes modern best practices.
## 1. Architectural Patterns
### 1.1 Model-View-Controller (MVC)
**Standard:** Adopt the MVC architectural pattern for web applications.
* **Do This:** Structure your application into Models (data and logic), Views (presentation), and Controllers (handling user input and interactions).
* **Don't Do This:** Avoid tightly coupling Models, Views, and Controllers. Fat models, skinny controllers are generally preferred.
**Why:** MVC promotes separation of concerns, making applications more maintainable, testable, and scalable.
**Code Example (Rails MVC):**
"""ruby
# app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
has_many :posts
end
# app/views/users/show.html.erb
<%= @user.email %>
<% @user.posts.each do |post| %>
<p><%= post.title %></p>
<% end %>
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
end
"""
**Anti-Pattern:** Placing business logic directly within views or controllers, leading to code duplication and making the application difficult to reason about.
### 1.2 Service Objects
**Standard:** Encapsulate complex business logic into Service Objects.
* **Do This:** Create dedicated classes to handle specific business operations, keeping controllers lean.
* **Don't Do This:** Put complex logic directly within models or controllers.
**Why:** Service Objects promote code reusability, testability, and maintainability by isolating specific responsibilities.
**Code Example:**
"""ruby
# app/services/user_creator.rb
class UserCreator
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
# Send welcome email
UserMailer.welcome_email(user).deliver_later
user
else
false
end
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = UserCreator.new(params[:user]).call
if user
redirect_to user_path(user), notice: 'User created successfully'
else
render :new, status: :unprocessable_entity
end
end
end
"""
**Anti-Pattern:** Overloading models with unrelated business logic, leading to "fat models" that are hard to maintain.
### 1.3 Event-Driven Architecture
**Standard:** Use an event-driven architecture for asynchronous tasks and decoupled components.
* **Do This:** Implement background processing using tools such as Sidekiq, Resque, or ActiveJob. Utilize Ruby's "concurrent" gem or standard "Thread" class for threading. Utilize "Hanami::Events" or "wisper´ for in-process events.
* **Don't Do This:** Perform long-running tasks within the request-response cycle.
**Why:** Event-driven architecture improves application responsiveness and scalability by offloading tasks to background processes.
**Code Example (Sidekiq):**
"""ruby
# app/workers/email_worker.rb
class EmailWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome_email(user).deliver_now
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = User.new(params[:user])
if user.save
EmailWorker.perform_async(user.id) # Enqueue the email sending
redirect_to user_path(user), notice: 'User created successfully'
else
render :new, status: :unprocessable_entity
end
end
end
"""
**Anti-Pattern:** Blocking the main application thread with synchronous tasks, leading to poor user experience.
### 1.4 API-First Design
**Standard:** Design applications with an API-first approach.
* **Do This:** Define clear API contracts (e.g., using OpenAPI/Swagger) before implementing the UI or other client-facing components.
* **Don't Do This:** Build APIs as an afterthought or directly tied to the UI.
**Why:** API-first design promotes reusability, interoperability, and scalability by decoupling the backend from the frontend.
**Code Example (Rails API mode):**
"""ruby
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:index, :show, :create, :update, :destroy]
end
end
end
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApplicationController
def index
@users = User.all
render json: @users
end
# ... other actions
end
end
end
"""
**Anti-Pattern:** Creating tightly coupled APIs that are difficult to evolve independently.
## 2. Project Structure and Organization
### 2.1 Directory Structure
**Standard:** Follow a consistent and logical directory structure.
* **Do This:** Organize code into "app", "lib", "config", "db", "spec" (or "test") directories.
* **Don't Do This:** Scatter files randomly throughout the project.
**Why:** A well-defined directory structure improves code discoverability, maintainability, and collaboration.
**Example:**
"""
my_project/
├── app/
│ ├── models/
│ ├── views/
│ ├── controllers/
│ ├── services/
│ ├── mailers/
│ └── helpers/
├── lib/
│ └── my_gem/ # External libraries & utilities
├── config/
│ ├── routes.rb
│ ├── application.rb
│ └── database.yml
├── db/
│ ├── migrate/
│ └── seeds.rb
├── spec/ # or test/
│ ├── models/
│ ├── controllers/
│ └── support/
├── Gemfile
├── README.md
└── .gitignore
"""
**Anti-Pattern:** Putting unrelated code in a single directory, making it difficult to find and understand.
### 2.2 Module Organization
**Standard:** Use modules to group related classes and functions.
* **Do This:** Create modules to encapsulate related functionality and prevent namespace collisions.
* **Don't Do This:** Define global functions or classes without proper namespacing.
**Why:** Modules improve code organization, readability, and maintainability.
**Code Example:**
"""ruby
# lib/payment_gateway.rb
module PaymentGateway
class Client
def self.charge(amount, credit_card)
# ... implementation
end
end
module Adapters
class Stripe
def self.charge(amount, token)
# ... Stripe specific impl
end
end
end
end
# Usage
PaymentGateway::Client.charge(100, credit_card_info)
PaymentGateway::Adapters::Stripe.charge(100, stripe_token)
"""
**Anti-Pattern:** Defining global classes or functions that can clash with other libraries or code.
### 2.3 Dependency Injection
**Standard:** Utilize dependency injection to decouple components.
* **Do This:** Pass dependencies into classes or functions instead of hardcoding them.
* **Don't Do This:** Create tight coupling through global variables or direct instantiation of dependencies within classes.
**Why:** Dependency injection increases code testability and flexibility by allowing dependencies to be easily swapped or mocked.
**Code Example:**
"""ruby
# Injecting a logger dependency
class MyService
def initialize(logger:)
@logger = logger
end
def do_something
@logger.info "Doing something..."
# ... implementation
end
end
# Usage with different loggers
my_service = MyService.new(logger: Logger.new($stdout))
my_service.do_something
my_service = MyService.new(logger: MockLogger.new) # For testing Purposes
my_service.do_something
"""
**Anti-Pattern:** Hardcoding dependencies within classes, making it difficult to test or reuse them in different contexts.
## 3. Code-Level Standards
### 3.1 Naming Conventions
**Standard:** Follow consistent naming conventions.
* **Do This:** Use descriptive names for classes, methods, and variables. Use snake_case for variables and methods, PascalCase for classes and modules.
* **Don't Do This:** Use cryptic or ambiguous names.
**Why:** Clear naming improves code readability and understanding.
**Code Example:**
"""ruby
class UserProfile # Use PascalCase for Class Names
def calculate_age # Use snake_case for method names
@date_of_birth # Example of a variable name
end
end
# Avoid:
# class UP
# def calc
# x = ...
"""
**Anti-Pattern:** Using single-letter variable names or unclear abbreviations.
### 3.2 Modularity
**Standard:** Write modular code.
* **Do This:** Break down complex methods into smaller, self-contained functions.
* **Don't Do This:** Write long, monolithic functions that are difficult to understand and test.
**Why:** Modular code improves readability, testability, and reusability.
**Code Example:**
"""ruby
def process_order(order)
validate_order(order)
calculate_total(order)
charge_customer(order)
send_confirmation_email(order)
end
private
def validate_order(order)
# ... validation logic
end
def calculate_total(order)
# ... calculation logic
end
# ... other private methods
"""
**Anti-Pattern:** Writing large, complex methods that perform multiple unrelated tasks.
### 3.3 Error Handling
**Standard:** Implement robust error handling mechanisms.
* **Do This:** Use "begin...rescue...end" blocks to handle exceptions. Raise exceptions when necessary.
* **Don't Do This:** Ignore exceptions or swallow errors silently.
**Why:** Proper error handling prevents application crashes and provides meaningful feedback to users.
**Code Example:**
"""ruby
def process_file(filename)
begin
file = File.open(filename)
# ... process file
rescue Errno::ENOENT => e
Rails.logger.error "File not found: #{e.message}"
# Raise a custom exception or return an error status.
raise CustomFileNotFoundError, "File not found: #{filename}"
rescue StandardError => e
Rails.logger.error "An unexpected error occurred: #{e.message}"
# Handle other errors appropriately
ensure
file.close if file # Ensure file close to prevent resource leaks
end
end
"""
**Anti-Pattern:** Ignoring exceptions or using bare "rescue" blocks without specifying the exception type.
### 3.4 Performance Optimization
**Standard:** Optimize code for performance. Use tools such as "benchmark", "memory_profiler" or "flamegraph" to assist!
* **Do This:** Use efficient data structures and algorithms. Avoid unnecessary database queries. Consider using caching strategies via Rails built-in caching or external caches like Redis.
* **Don't Do This:** Write code that performs poorly or consumes excessive resources. Premature optimization!
**Why:** Performance optimization improves application responsiveness and scalability.
**Code Example:**
"""ruby
# Instead of:
users.each { |user| puts user.name }
# Use map(&:name) for performance:
names = users.map(&:name) # More efficient as you pre-allocate memory for a dedicated array
# SQL Optimization
# Instead of:
orders.each { |order| puts order.customer.name } # N+1 query problem
# Use includes to eager load associations:
orders = Order.includes(:customer)
orders.each { |order| puts order.customer.name } # Reduces database queries
"""
**Anti-Pattern:** Ignoring performance issues or writing inefficient code with little to no thought of optimization.
### 3.5 Security Considerations
**Standard:** Implement security best practices.
* **Do This:** Sanitize user input. Protect against SQL injection, cross-site scripting (XSS), and other common vulnerabilities. Utilize tools like Brakeman for static analysis.
* **Don't Do This:** Store sensitive data in plain text.
**Why:** Security best practices protect the application and its users from threats.
**Code Example:**
"""ruby
# Sanitize user input
params[:search_term] = ActionController::Base.helpers.sanitize(params[:search_term])
# Use parameterized queries to prevent SQL injection
User.where("email = ?", params[:email])
# Use bcrypt for password hashing
def password=(new_password)
@password = BCrypt::Password.create(new_password)
self.password_hash = @password
end
"""
**Anti-Pattern:** Storing passwords in plain text or failing to sanitize user input.
## 4. Specific Ruby Features (Latest Version)
### 4.1 Pattern Matching
**Standard:** Utilize pattern matching for concise and expressive code.
* **Do This:** Use "case...in" statements to match complex data structures.
* **Don't Do This:** Rely solely on traditional "if...else" statements for complex conditional logic.
**Why:** Pattern matching simplifies complex conditional logic and makes code more readable.
**Code Example:**
"""ruby
result = {status: :ok, data: {name: "John", age: 30}}
case result
in {status: :ok, data: {name: String => name, age: Integer => age}}
puts "Name: #{name}, Age: #{age}"
in {status: :error, message: String => message}
puts "Error: #{message}"
else
puts "Unknown result"
end
"""
**Anti-Pattern:** Missing opportunities to use powerful pattern matching features in suitable situations.
### 4.2 Fiber Scheduler/Concurrency
**Standard:** Use fibers for concurrent code execution
* **Do This:** Use "Fiber.schedule" to execute functions concurrently without native threads.
* **Don't Do This:** Use many OS-level threads unless specifically needed.
**Why:** Fibers greatly improve concurrency performance.
**Code Example:**
"""ruby
Fiber.schedule do
# concurrent code!
puts "Running in a fiber!"
end
"""
**Anti-Pattern:** Ignoring the existance and performance benefits of fibers.
### 4.3 Ractor
**Standard**: Use Ractor for parallel code execution.
* **Do This**: Isolate state between "Ractor"s to avoid data races.
* **Don't Do This**: Share mutable state directly between "Ractor"s.
**Why**: Helps achieve true parallelism in Ruby code.
**Code Example:**
"""ruby
r = Ractor.new { puts "Hello from a ractor!" }
r.take #=> "Hello from a ractor!"
"""
**Anti-Pattern**: Sharing mutable data between actors without synchronization.
## 5. Tooling and Automation
### 5.1 Linters and Code Analysis
**Standard:** Use Linters (Rubocop), Security Scanners (Brakeman) and code analysis tools
* **Do This:** Configure Rubocop with a configuration file (".rubocop.yml") to enforce the coding standards defined in this document.
* **Do This:** Integrate Brakeman into the CI/CD pipeline.
* **Don't Do This:** Ignore warnings from the linter and static analysis tools.
### 5.2 Testing Framework
**Standard**: Implement automated test suites using RSpec or Minitest
* **Do This**: Aim for high test coverage. Write unit, integration and end-to-end tests.
* **Don't Do This**: Skip writing tests or commit code with failing tests.
### 5.3 Continuous Integration & Continuous Deployment (CI/CD)
**Standard:** Use CI/CD pipelines for automated builds, tests, and deployments.
* **Do This:** Integrate the CI/CD pipeline with code repositories (e.g., GitHub, GitLab).
* **Don't Do This:** Deploy code manually without automated testing or validation.
**Why:** CI/CD automates the software development lifecycle, resulting in faster delivery, fewer bugs, and higher quality.
**Code Example (GitHub Actions):**
"""yaml
# .github/workflows/main.yml
name: Ruby CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
ports: ['5432:5432']
env:
POSTGRES_USER: ruby
POSTGRES_PASSWORD: password
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2 # specify version of Ruby to use (eg: 2.7.2)
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Configure application
run: cp config/database.yml.example config/database.yml
- name: Create and Migrate Database
run: |
bundle exec rails db:create
bundle exec rails db:migrate
- name: Run tests
run: bundle exec rspec
"""
This comprehensive document provides a foundation for building robust, maintainable, and high-performing Ruby applications, aligning with the latest features and best practices of the language.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Ruby This document outlines the component design standards for Ruby. It aims to provide guidance on creating reusable, maintainable, and testable components within Ruby applications. These standards leverage modern Ruby idioms, the latest language features, and generally accepted best practices to ensure high-quality, robust code. ## 1. Principles of Component Design ### 1.1. Single Responsibility Principle (SRP) * **Do This:** Ensure each component has one, and only one, reason to change. A component should be focused on performing one specific task. * **Don't Do This:** Create "god classes" or modules that perform a wide variety of unrelated functions. * **Why:** SRP enhances maintainability and reduces the risk of unintended side effects when modifying code. """ruby # Good: Separate classes for calculating price and applying discount class PriceCalculator def calculate(item, quantity) item.price * quantity end end class DiscountApplicator def apply_discount(price, discount) price * (1 - discount) end end # Bad: Single class handling both price calculation and discount class OrderProcessor def calculate_price(item, quantity) item.price * quantity end def apply_discount(price, discount) price * (1 - discount) end end """ ### 1.2. Open/Closed Principle (OCP) * **Do This:** Design components that are open for extension but closed for modification. Use inheritance or composition to add new functionality without altering existing code. * **Don't Do This:** Modify existing classes directly to add new features if unintended effects for existing users may occur. * **Why:** Prevents introducing bugs and ensures that new functionality doesn't break existing features. """ruby # Good: Using inheritance for different notification types class Notifier def send(message, recipient) raise NotImplementedError, "Subclasses must implement the send method" end end class EmailNotifier < Notifier def send(message, recipient) # Implementation for sending email puts "Sending email to #{recipient}: #{message}" end end class SMSNotifier < Notifier def send(message, recipient) # Implementation for sending SMS puts "Sending SMS to #{recipient}: #{message}" end end # Usage email_notifier = EmailNotifier.new email_notifier.send("Hello!", "user@example.com") sms_notifier = SMSNotifier.new sms_notifier.send("Hello!", "+15551234567") # Bad: Modifying the original notifier class for each new method class Notifier def send(message, recipient, type = :email) if type == :email # Email Logic elsif type == :sms # SMS Logic end end end """ ### 1.3. Liskov Substitution Principle (LSP) * **Do This:** Ensure that subtypes are substitutable for their base types without altering the correctness of the program. * **Don't Do This:** Introduce subtypes with behavior that violates the expectations set by the base type. * **Why:** Maintains predictable behavior and simplifies code reasoning. """ruby # Good: Rectangle and Square adhering to LSP class Rectangle attr_accessor :width, :height def initialize(width, height) @width = width @height = height end def area @width * @height end end class Square < Rectangle def initialize(side) super(side, side) # Call the parent class's initialization @width = side @height = side end def width=(value) @width = value @height = value end def height=(value) @height = value @width = value end end def process_rectangle(rectangle) rectangle.width = 5 rectangle.height = 4 puts "Area: #{rectangle.area}" # Expected: 20 end rectangle = Rectangle.new(2, 3) process_rectangle(rectangle) # Output: Area: 20 square = Square.new(2) process_rectangle(square) # Output: Area: 20 # Bad: Square not adhering to LSP # If Square only had a single @side property and overrode width= and height= to set @side, then # the process_rectangle expectation would be violated. """ ### 1.4. Interface Segregation Principle (ISP) * **Do This:** Favor many client-specific interfaces over one general-purpose interface. * **Don't Do This:** Force components to implement methods they don't need. * **Why:** Reduces dependencies and improves cohesion within components. """ruby # Good: Separate interfaces for different operations module Readable def read raise NotImplementedError end end module Writable def write(data) raise NotImplementedError end end class FileReader include Readable def read # Implementation of reading from a file puts "Reading from file" end end class FileWriter include Writable def write(data) # Implementation of writing to a file puts "Writing to file: #{data}" end end class ReadWriter include Readable include Writable def read puts "Reading from ReadWriter" end def write(data) puts "Writing to ReadWriter: #{data}" end end # Bad: Single interface with unnecessary methods module FileOperations def read raise NotImplementedError end def write(data) raise NotImplementedError end def execute raise NotImplementedError end end """ ### 1.5. Dependency Inversion Principle (DIP) * **Do This:** Depend on abstractions (interfaces/abstract classes) rather than concrete implementations. * **Don't Do This:** Create tightly coupled components that directly depend on each other. * **Why:** Promotes flexibility, testability, and reusability by decoupling components. """ruby # Good: Depending on an abstraction class PaymentProcessor def initialize(payment_gateway) @payment_gateway = payment_gateway end def process_payment(amount) @payment_gateway.charge(amount) end end # Abstraction (Interface) class PaymentGateway def charge(amount) raise NotImplementedError end end # Concrete implementations class StripeGateway < PaymentGateway def charge(amount) puts "Charging #{amount} via Stripe" end end class PayPalGateway < PaymentGateway def charge(amount) puts "Charging #{amount} via PayPal" end end stripe_gateway = StripeGateway.new payment_processor = PaymentProcessor.new(stripe_gateway) payment_processor.process_payment(100) paypal_gateway = PayPalGateway.new payment_processor = PaymentProcessor.new(paypal_gateway) payment_processor.process_payment(50) # Bad: Directly depending on a concrete implementation class PaymentProcessor def process_payment(amount) stripe_gateway = StripeGateway.new # Tight coupling stripe_gateway.charge(amount) end end """ ## 2. Component Definition and Structure ### 2.1. Classes vs. Modules * **Do This:** Use classes for defining objects with state and behavior. Use modules for grouping related methods and constants or to implement mixins. * **Don't Do This:** Use classes for purely static methods or constants. Use modules to create objects with independent state. * **Why:** Follows Ruby conventions and improves code clarity. """ruby # Good: Class for an object, module for functions class User attr_accessor :name, :email def initialize(name, email) @name = name @email = email end end module StringUtils def self.titleize(string) string.split.map(&:capitalize).join(' ') end end puts StringUtils.titleize("hello world") # Output: Hello World # Bad: Class for static methods class MathUtils def self.add(a, b) a + b end end """ ### 2.2. Namespaces * **Do This:** Use namespaces (modules) to organize related classes and avoid naming conflicts. * **Don't Do This:** Define classes in the global namespace carelessly. It leads to naming collisions. * **Why:** Enhances code organization and maintainability, especially in large projects. """ruby # Good: Using namespaces module MyLibrary class User attr_accessor :name end class Product attr_accessor :price end end user = MyLibrary::User.new user.name = "Alice" puts user.name # Output: Alice # Bad: No namespace class User attr_accessor :name end """ ### 2.3. Component Interfaces * **Do This:** Define clear public interfaces for components using well-documented methods. Use private and protected methods to encapsulate implementation details. * **Don't Do This:** Expose internal implementation details as public methods, or improperly use "private" which affects inheritance. * **Why:** Increases component usability and reduces the risk of breaking code when internal implementations change. """ruby # Good: Clear public interface with private helper methods class Calculator def add(a, b) validate_numbers(a, b) perform_addition(a, b) end private def validate_numbers(a, b) raise ArgumentError, "Inputs must be numbers" unless a.is_a?(Numeric) && b.is_a?(Numeric) end def perform_addition(a, b) a + b end end # Usage: calculator = Calculator.new puts calculator.add(5, 3) # => 8 # Attempting to call private methods directly will raise errors: # calculator.validate_numbers(5, 3) # NoMethodError: private method "validate_numbers' called """ **Notes on Private Methods:** In Ruby, "private" methods can only be called within the instances of the same class. This means that derived classes cannot directly call "private" methods of the base class. Use "protected" methods if access needs to be granted to instances of derived classes. ### 2.4. Attributes and Encapsulation * **Do This:** Use "attr_accessor", "attr_reader", and "attr_writer" judiciously to control access to component attributes. Prefer immutability when appropriate. * **Don't Do This:** Expose all attributes as read-write without considering the implications. * **Why:** Controls state changes and maintains component integrity. """ruby # Good: Controlled attribute access class Book attr_reader :title, :author # Only readable attr_accessor :price # Read and write def initialize(title, author, price) @title = title @author = author @price = price end def expensive? @price > 50 end end book = Book.new("The Ruby Way", "Hal Fulton", 60) puts book.title book.price = 70 # OK #book.title = "New Title" # Error: No writer method #Immutability example (using .freeze in Ruby) class ImmutablePoint attr_reader :x, :y def initialize(x, y) @x = x @y = y freeze # Make the object immutable end end immutable_point = ImmutablePoint.new(1, 2) # immutable_point.x = 3 # Raises a TypeError because the object is frozen. """ ### 2.5. Initializers * **Do This:** Use initializers ("initialize" method) to set up the initial state of a component. Use keyword arguments for clarity and readability. * **Don't Do This:** Perform heavy computation or I/O operations in the initializer. Use factory methods or lazy initialization for complex setup. * **Why:** Provides a predictable way to instantiate components. """ruby # Good: Keyword arguments for clarity class Order attr_accessor :order_id, :customer_id, :items def initialize(order_id:, customer_id:, items: []) @order_id = order_id @customer_id = customer_id @items = items end end order = Order.new(order_id: 123, customer_id: 456, items: ['A', 'B']) # Bad: Long list of positional arguments without clear meaning. class OldOrder def initialize(a, b, c, d, e) #Hard to grok what these parameters are without documentation. end end """ ## 3. Component Interactions and Design Patterns ### 3.1. Composition over Inheritance * **Do This:** Prefer composition (has-a relationship) over inheritance (is-a relationship) for code reuse and flexibility. * **Don't Do This:** Create deep inheritance hierarchies, as these can lead to the fragile base class problem. * **Why:** Composition promotes loose coupling and allows components to be easily combined and reused. """ruby # Good: Composition class Engine def start puts "Engine started" end end class Wheels def rotate puts "Wheels rotating" end end class Car def initialize(engine, wheels) @engine = engine @wheels = wheels end def start @engine.start @wheels.rotate puts "Car moving" end end engine = Engine.new wheels = Wheels.new car = Car.new(engine, wheels) car.start # Bad: Deep inheritance hierarchy class Vehicle def start puts "Vehicle starting" end end class Car < Vehicle #Inherits and extends Vehicle def rotate_wheels #Specific to car puts "Rotating wheels" end def start super() #Call the parent version rotate_wheels() end end """ ### 3.2. Observer Pattern * **Do This:** Use the Observer pattern for loosely coupled communication between components. Define a subject (observable) and observers that react to state changes. * **Don't Do This:** Create tight dependencies where components directly call each other's methods across distinct logical boundaries. * **Why:** Decouples components and allows for flexible event handling. """ruby # Good: Observer Pattern require 'observer' #Using built-in Ruby library. class Employee include Observable attr_reader :name, :salary def initialize(name, salary) @name = name @salary = salary end def salary=(new_salary) old_salary = @salary @salary = new_salary changed # Tell the observer notify_observers(self, old_salary, new_salary) end end class Payroll def update(employee, old_salary, new_salary) puts "Cut a new check for #{employee.name}! from #{old_salary} to #{new_salary}" end end class TaxMan def update(employee, old_salary, new_salary) puts "Send #{employee.name} a new tax form!" end end fred = Employee.new("Fred", 50000) payroll = Payroll.new tax_man = TaxMan.new fred.add_observer(payroll) fred.add_observer(tax_man) fred.salary = 60000 """ ### 3.3. Strategy Pattern * **Do This:** Use the Strategy pattern to encapsulate interchangeable algorithms or behaviors. Define a context that uses a strategy object to perform a task. * **Don't Do This:** Embed conditional logic directly within components to handle different behaviors. * **Why:** Provides a flexible way to switch algorithms at runtime. """ruby # Good: Strategy Pattern class Report attr_accessor :formatter def initialize(formatter) @formatter = formatter end def output_report(title, text) @formatter.output_report(title, text) end end class HTMLFormatter def output_report(title, text) puts "<html><head><title>#{title}</title></head><body><h1>#{title}</h1><p>#{text}</p></body></html>" end end class PlainTextFormatter def output_report(title, text) puts "*** #{title} ***\n#{text}" end end html_formatter = HTMLFormatter.new report = Report.new(html_formatter) report.output_report("My Report", "This is the report content.") plain_text_formatter = PlainTextFormatter.new report.formatter = plain_text_formatter report.output_report("My Report", "This is the report content.") """ ### 3.4. Factory Pattern * **Do This:** Use factory patterns to encapsulate object creation logic. Use a factory method or factory class to create instances of components. * **Don't Do This:** Directly instantiate classes throughout the code, especially when the creation logic is complex. * **Why:** Decouples object creation from usage improves code maintainability and adheres to the Dependency Inversion Principle. """ruby # Good: Factory Pattern class DatabaseConnection def self.create(type) case type when :mysql MySQLConnection.new when :postgresql PostgreSQLConnection.new else raise ArgumentError, "Unknown database type: #{type}" end end end class MySQLConnection def connect puts "Connecting to MySQL database" end end class PostgreSQLConnection def connect puts "Connecting to PostgreSQL database" end end connection = DatabaseConnection.create(:mysql) connection.connect connection = DatabaseConnection.create(:postgresql) connection.connect # Bad: Direct instantiation spread throughout the code def connect_to_database(type) if type == "mysql" MySQLConnection.new.connect elsif type == "postgresql" PostgreSQLConnection.new.connect end end """ ## 4. Error Handling and Logging ### 4.1. Exception Handling * **Do This:** Use "begin...rescue...end" blocks to handle exceptions gracefully. Raise specific exceptions rather than generic "Exception" or "StandardError". * **Don't Do This:** Swallow exceptions without proper handling or logging. * **Why:** Prevents application crashes and provides meaningful error messages for debugging. """ruby # Good: Specific exception handling def process_file(filename) begin file = File.open(filename, 'r') data = file.read # Process data rescue Errno::ENOENT => e puts "File not found: #{e.message}" rescue IOError => e puts "I/O error: #{e.message}" ensure file.close if file end end # Bad: Catching generic exceptions def process_file(filename) begin file = File.open(filename, 'r') data = file.read # Process data rescue => e puts "An error occurred: #{e.message}" #Too generic and unhelpful ensure file.close if file end end """ ### 4.2. Logging * **Do This:** Use a logging library (e.g., "Logger", "Rails.logger") to record important events, errors, and warnings. Use appropriate log levels (debug, info, warn, error, fatal). * **Don't Do This:** Rely solely on "puts" statements for logging. * **Why:** Facilitates debugging and monitoring of application behavior. """ruby # Good: Using Logger require 'logger' logger = Logger.new(STDOUT) #Or a file. logger.level = Logger::DEBUG #Setting the level. logger.debug("This is a debug message") logger.info("This is an info message") logger.warn("This is a warning message") logger.error("This is an error message") logger.fatal("This is a fatal message") begin # Risky operation result = 10 / 0 rescue => e logger.error("Error during division: #{e.message}") end # Bad: Using puts def some_method puts "Starting some_Method" #Lack of context, level, etc. end """ ## 5. Testing ### 5.1. Unit Testing * **Do This:** Write unit tests for all components to verify their behavior in isolation. Use a testing framework like RSpec or Minitest. Aim for high test coverage. * **Don't Do This:** Skip writing unit tests or write tests that are superficial and don't thoroughly exercise the component. * **Why:** Increases confidence in code quality and simplifies debugging. """ruby # Example RSpec Unit test require 'rspec' require_relative 'calculator' #Referring to the Calculator class from earlier RSpec.describe Calculator do describe '#add' do it 'returns the sum of two numbers' do calculator = Calculator.new expect(calculator.add(2, 3)).to eq(5) end it 'raises an error if inputs are not numbers' do calculator = Calculator.new expect { calculator.add(2, 'a') }.to raise_error(ArgumentError) end end end """ ### 5.2. Integration Testing * **Do This:** Write integration tests to verify the interaction between components. * **Don't Do This:** Assume that components will work together correctly without integration testing. * **Why:** Ensures that components function properly as a system. """ruby #Example interaction test that exercises interactions. require 'rspec' require_relative 'order_processing_service' require_relative 'payment_gateway' require_relative 'inventory_service' RSpec.describe OrderProcessingService do let(:payment_gateway) { instance_double(PaymentGateway, charge: true) } let(:inventory_service) { instance_double(InventoryService, reserve_items: true) } let(:order_processing_service) { OrderProcessingService.new(payment_gateway: payment_gateway, inventory_service: inventory_service) } let(:order_details) { { customer_id: 1, items: [{ product_id: 101, quantity: 1 }] } } describe '#process_order' do it 'processes the order successfully' do order_processing_service.process_order(order_details) expect(payment_gateway).to have_received(:charge) expect(inventory_service).to have_received(:reserve_items).with(order_details[:items]) end it 'raises an error if payment fails' do allow(payment_gateway).to receive(:charge).and_raise(PaymentError) expect { order_processing_service.process_order(order_details) }.to raise_error(PaymentError) expect(inventory_service).not_to have_received(:reserve_items) # Inventory should not be reserved end end end """ ## 6. Concurrency and Parallelism For Ruby 3+, consider using "Ractor"s for true parallelism. Note that the older "Thread" class has limitations due to the Global Interpreter Lock (GIL). ### 6.1 Ractors (Ruby 3+) * **Do This:** Use Ractors for CPU-intensive tasks to achieve true parallel execution. * **Don't Do This:** Rely solely on Threads for computationally intensive tasks that require true parallelism. * **Why:** Ractors allow for isolating state and improve concurrency while avoiding GIL limitations. """ruby #Ractor example: require 'ractor' def expensive_calculation(n) result = 0 (1..n).each do |i| result += Math.sqrt(i) end result end numbers = [100_000, 200_000, 300_000] ractors = numbers.map do |n| Ractor.new n do |number| expensive_calculation(number) end end results = ractors.map(&:take) #Collect the results puts "Results: #{results}" """ ### 6.2 Threads * **Do This:** Use threads for I/O-bound operations to improve responsiveness * **Don't Do This:** Create threads indiscriminately, as this can lead to performance problems due to context switching overhead. * **Why:** Allows concurrency and efficient use of resources when waiting for I/O. """ruby # Using threads for concurrent I/O operations threads = [] ['file1.txt', 'file2.txt', 'file3.txt'].each do |filename| threads << Thread.new(filename) do |f| begin content = File.read(f) puts "Processed: #{f}" #Process the contents rescue => e puts "Error processing #{f}: #{e.message}" end end end threads.each(&:join) #Wait for all threads to complete """ ## 7. Documentation ### 7.1. Code Comments * **Do This:** Write clear, concise, and up-to-date comments to explain complex logic, non-obvious decisions, and component interfaces. * **Don't Do This:** Write redundant comments that simply restate the code. * **Why:** Facilitates understanding and maintaining the code. ### 7.2. YARD Documentation * **Do This:** Use YARD syntax to document classes, methods, and modules. Generate documentation using the "yard" command. * **Don't Do This:** Neglect documenting code or provide incomplete or outdated documentation. * **Why:** Creates consistent, standardized documentation that can updated and shared. """ruby # Example YARD Documentation # Adds two numbers. # @param a [Integer] The first number. # @param b [Integer] The second number. # @return [Integer] The sum of a and b. def add(a, b) a + b end """ This document is intended to serve as a comprehensive guide for Ruby component design. By following these standards, development teams can create robust, maintainable, and scalable Ruby applications. Remember to stay up-to-date with the latest Ruby versions and best practices to continuously improve code quality.
# Deployment and DevOps Standards for Ruby This document outlines the deployment and DevOps standards for Ruby projects, aiming to create robust, maintainable, and scalable applications. It covers build processes, CI/CD pipelines, production considerations, and relevant technologies specific to the Ruby ecosystem. ## 1. Build Processes and Dependency Management ### 1.1 Dependency Management with Bundler Bundler is the standard dependency manager for Ruby projects. Using it correctly ensures consistent environments across development, testing, and production. **Do This:** Always use Bundler to manage your project's dependencies. **Don't Do This:** Manually install gems or rely on system-installed gems for production deployments. **Why:** Using Bundler guarantees that all environments use the exact same gem versions, preventing "it works on my machine" issues. **Example:** """ruby # Gemfile source 'https://rubygems.org' gem 'rails', '~> 7.0' gem 'pg' gem 'puma' group :development, :test do gem 'rspec-rails' gem 'factory_bot_rails' end group :development do gem 'web-console' end """ * **Actionable Standard:** Ensure a "Gemfile" and "Gemfile.lock" are present in your project. The "Gemfile.lock" MUST be committed to version control. * **Actionable Standard:** Use "bundle install" for installing dependencies. Run "bundle update" judiciously to update gem versions and test thoroughly. * **Actionable Standard:** Configure Bundler to install gems into a "vendor/bundle" directory or use a system-wide gemset via "rvm" or "rbenv". * **Actionable Standard:** Employ "bundle exec" when running executables like "rails server" or "rake db:migrate" to ensure they use the gems specified in the "Gemfile". **Anti-pattern:** Forgetting to run "bundle install" after cloning a repository or after modifying the "Gemfile". This can lead to missing dependencies and runtime errors. ### 1.2 Build Automation with Rake Rake is a Ruby-based build tool, similar to Make in other languages. Use it for automating common tasks such as database migrations, asset compilation, and code analysis. **Do This:** Define Rake tasks for common deployment and maintenance tasks. **Don't Do This:** Manually execute deployment steps or rely on scripts scattered throughout the project. **Why:** Rake provides a consistent and repeatable way to perform tasks, reducing the risk of human error during deployments. **Example:** """ruby # Rakefile require 'rake' require 'rake/tasklib' namespace :deploy do desc "Deploy to production" task :production do puts "Deploying to production..." # Add deployment steps here end desc "Migrate the database" task :migrate do puts "Migrating the database..." sh 'bundle exec rails db:migrate' # Correct use of bundle exec end end """ * **Actionable Standard:** Create a "Rakefile" to define your project's build and deployment tasks. * **Actionable Standard:** Use namespaces to organize related tasks (e.g., "deploy:production", "db:migrate"). * **Actionable Standard:** Utilize shell commands (e.g., "sh", "exec") within Rake tasks to execute system commands. Always use "bundle exec" prefix. * **Actionable Standard:** Document each Rake task with a descriptive "desc" for clarity. **Anti-Pattern:** Manually running database migrations on production servers can lead to inconsistencies and errors, especially in multi-server environments. Automate these tasks using Rake. ## 2. Continuous Integration and Continuous Deployment (CI/CD) ### 2.1 CI/CD Pipeline Setup A CI/CD pipeline automates the process of building, testing, and deploying your Ruby application. Popular CI/CD tools include Jenkins, GitLab CI, GitHub Actions, CircleCI, and Travis CI. **Do This:** Integrate your Ruby project with a CI/CD pipeline. **Don't Do This:** Manually build, test, and deploy your application each time you make changes. **Why:** CI/CD reduces deployment risks, speeds up the development process, and ensures that code is always in a deployable state. **Example (GitHub Actions):** """yaml # .github/workflows/main.yml name: Ruby CI/CD on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest services: postgres: image: postgres:13 ports: ['5432:5432'] env: POSTGRES_USER: postgres POSTGRES_DB: your_app_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.2 # Example Ruby version bundler-cache: true - name: Install dependencies run: bundle install - name: Set up database run: | bundle exec rails db:create bundle exec rails db:schema:load - name: Run tests run: bundle exec rspec deploy: needs: build if: github.ref == 'refs/heads/main' # Only deploy from main runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to Heroku uses: akhileshns/heroku-deploy@v3.12.12 # Use specific version with: heroku_api_key: ${{secrets.HEROKU_API_KEY}} heroku_app_name: your-heroku-app heroku_email: your-email@example.com """ * **Actionable Standard:** Define CI/CD pipelines in YAML files (e.g., ".gitlab-ci.yml", ".github/workflows/main.yml"). * **Actionable Standard:** Automate the following steps in the pipeline: code checkout, dependency installation, code linting (using RuboCop), running tests (unit, integration, system), and deployment. * **Actionable Standard:** Use environment variables for sensitive information like API keys, database passwords, and deployment credentials. Store these secrets securely via the CI/CD platform's secrets management. * **Actionable Standard:** Implement automated rollbacks in case of deployment failures. **Anti-pattern:** Skipping automated tests in the CI/CD pipeline can lead to deploying broken code to production, causing downtime and user frustration. ### 2.2 Code Analysis and Linting RuboCop is a popular static code analyzer and formatter for Ruby. Using it helps enforce coding standards and identify potential code quality issues. **Do This:** Integrate RuboCop into your CI/CD pipeline. **Don't Do This:** Ignore RuboCop warnings or manually fix code style issues. **Why:** RuboCop ensures consistent coding style, catches potential bugs, and maintains code quality, which is essential for maintainability. **Example:** """yaml # .github/workflows/main.yml (modified) name: Ruby CI/CD on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.2 bundler-cache: true - name: Install dependencies run: bundle install - name: Run RuboCop run: bundle exec rubocop - name: Set up database run: | bundle exec rails db:create bundle exec rails db:schema:load - name: Run tests run: bundle exec rspec """ * **Actionable Standard:** Include a ".rubocop.yml" file in your project to configure RuboCop's rules and exclusions. * **Actionable Standard:** Customize RuboCop rules according to your team's coding conventions (e.g., line length, indentation). Airbnb's Ruby style guide is also a good reference point. * **Actionable Standard:** Automatically fix RuboCop violations where possible using "rubocop -a". * **Actionable Standard:** If you choose to ignore specific RuboCop offenses, document the reason clearly in the code or ".rubocop.yml". **Anti-pattern:** Disabling RuboCop entirely or ignoring its warnings undermines code quality and consistency. ## 3. Production Considerations ### 3.1 Application Servers – Puma and Unicorn Puma and Unicorn are popular application servers for Ruby on Rails applications. Puma supports multi-threading due to Ruby's Global VM Lock (GVL) limitations, while Unicorn is a pre-forking server. **Do This:** Choose an application server based on your application's requirements and infrastructure. Puma is generally recommended when using modern Ruby versions. **Don't Do This:** Rely on the WEBrick server (the default in Rails development) for production deployments. **Why:** Production-grade application servers like Puma and Unicorn are designed to handle high traffic, concurrency, and resource management, which WEBrick is not. **Example (Puma Configuration):** """ruby # config/puma.rb workers Integer(ENV['WEB_CONCURRENCY'] || 2) # Number of worker processes threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5) threads threads_count, threads_count preload_app! rackup DefaultRackup port ENV['PORT'] || 3000 # Utilize environment variable for port. environment ENV['RAILS_ENV'] || 'development' # Set Rails environment # On worker boot, add connections that pre-exist to the pool on_worker_boot do ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end before_fork do require 'puma_worker_killer' PumaWorkerKiller.config do |config| config.ram = 1024 # mb config.frequency = 5 # seconds config.percent_usage = 0.98 config.rolling_restart_frequency = 6 * 3600 # 6 hours in seconds config.reaper_status_logs = true # setting this to false will not log lines like: # PumaWorkerKiller: Consuming 54.34765625 mb with master and 2 workers. end PumaWorkerKiller.start end """ * **Actionable Standard:** Configure Puma with the appropriate number of workers and threads based on your server's CPU cores and memory. * **Actionable Standard:** Use environment variables (e.g., "WEB_CONCURRENCY", "RAILS_MAX_THREADS", "PORT") to configure Puma in different environments. * **Actionable Standard:** Integrate Puma with a process manager like Systemd or Supervisor to ensure automatic restarts in case of crashes. * **Actionable Standard:** Consider using "puma-worker-killer" to automatically restart Puma workers that consume excessive memory. Helps prevent memory leaks from crashing the entire server. **Anti-pattern:** Statically defining the number of workers and threads in the Puma configuration file makes it difficult to adapt to different environments and server sizes. ### 3.2 Database Connection Pooling Database connection pooling improves the performance of Ruby applications by reusing database connections instead of creating new ones for each request. **Do This:** Configure database connection pooling in your "database.yml" file. **Don't Do This:** Use a single database connection for all requests. **Why:** Connection pooling reduces the overhead of establishing new database connections, which can be a significant bottleneck for high-traffic applications. **Example:** """yaml # config/database.yml default: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: your_username password: your_password development: <<: *default database: your_app_development test: <<: *default database: your_app_test production: <<: *default database: your_app_production url: <%= ENV['DATABASE_URL'] %> # Use database URL from environment variables """ * **Actionable Standard:** Set the "pool" size in "database.yml" to an appropriate value based on your application's concurrency and database server capacity. The pool size generally should be the same as the maximum number of threads you have configured for Puma or the expected total connections from all unicorn workers. * **Actionable Standard:** Use a database URL format instead of hardcoding credentials directly inside "database.yml". * **Actionable Standard:** Monitor your database connection usage to identify potential connection leaks or bottlenecks. **Anti-pattern:** Not configuring database connection pooling can lead to slow response times and database server overload. ### 3.3 Logging and Monitoring Proper logging and monitoring are crucial for understanding application behavior, identifying performance issues, and diagnosing errors. **Do This:** Implement comprehensive logging and monitoring in your Ruby applications. **Don't Do This:** Rely on "puts" statements for logging or ignore application metrics. **Why:** Logging provides valuable insights into application behavior, while monitoring helps identify and diagnose performance issues and errors. **Example (Logging with Rails):** """ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :log_request private def log_request Rails.logger.info "Request: #{request.method} #{request.path}" # Detailed logging Rails.logger.debug "Parameters: #{params.inspect}" end end # Using logger inside model class User < ApplicationRecord after_create :log_creation private def log_creation Rails.logger.info "User created with id: #{self.id}" end end # In the console to log directly: Rails.logger.info "Something happened here important" """ * **Actionable Standard:** Use Rails' built-in logger ( "Rails.logger" ) for structured logging at different levels ( "debug", "info", "warn", "error", "fatal" ). * **Actionable Standard:** Log essential events, such as requests, database queries, errors, and background job executions. * **Actionable Standard:** Integrate with a centralized logging system like ELK Stack (Elasticsearch, Logstash, Kibana), Splunk, or Papertrail. * **Actionable Standard:** Implement application performance monitoring (APM) using tools like New Relic, Datadog, or Scout APM. Focus on metrics like response time, throughput, error rates, and database query performance. * **Actionable Standard:** Use structured logging (e.g., JSON format) to faciliter parsing and analysis. * **Actionable Standard:** Monitor system-level metrics, such as CPU usage, memory usage, disk I/O, and network traffic. **Anti-pattern:** Logging sensitive information like passwords or API keys can create security vulnerabilities. Implement proper redaction or masking of sensitive data in logs. ### 3.4 Security Best Practices Security is paramount in production environments. The following security best practices are essential: * **Actionable Standard:** Keep Ruby, Rails, and all gems up-to-date with the latest security patches. Automate this process where possible. * **Actionable Standard:** Follow secure coding practices to prevent common vulnerabilities like SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF). * **Actionable Standard:** Use strong and unique passwords for all user accounts. Implement multi-factor authentication (MFA) where possible. * **Actionable Standard:** Enforce HTTPS for all communication to protect data in transit. Configure HSTS (HTTP Strict Transport Security) to prevent man-in-the-middle attacks. * **Actionable Standard:** Regularly review and update security configurations, including firewalls, intrusion detection systems, and access controls. **Anti-pattern:** Ignoring security vulnerabilities or failing to apply security patches exposes applications to potential attacks. Use tools like Bundler Audit to check for known vulnerabilities in your dependencies. ### 3.5 Environment Variables Using environment variables for configuration ensures flexibility and security in different environments. **Do This:** Store configuration settings (e.g., database credentials, API keys) in environment variables. **Don't Do This:** Hardcode configuration settings in code or configuration files. **Why:** Environment variables provide a secure and flexible way to configure applications without modifying code. **Example:** """ruby # config/database.yml (as shown previously) production: <<: *default database: your_app_production url: <%= ENV['DATABASE_URL'] %> # Rails secret key # config/secrets.yml or config/credentials.yml.enc secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> #Example in older version of rails """ * **Actionable Standard:** Use a gem like "dotenv" in development to load environment variables from a ".env" file. *Never* commit the ".env" file to version control. * **Actionable Standard:** Configure your deployment environment (e.g., Heroku, AWS) to set environment variables for production. * **Actionable Standard:** Employ a secrets management solution like HashiCorp Vault or AWS Secrets Manager to securely store and access sensitive environment variables. **Anti-pattern:** Committing sensitive credentials to version control poses a significant security risk. ## 4. Containerization and Infrastructure as Code (IaC) ### 4.1 Docker and Containerization Docker allows you to package your Ruby application and its dependencies into a standardized unit for software development. This provides consistency across different environments. **Do This:** Containerize your Ruby application using Docker. **Don't Do This:** Deploy your application directly to virtual machines without containerization. **Why:** Docker simplifies deployments, ensures consistency across environments, and improves resource utilization. **Example (Dockerfile):** """dockerfile # Dockerfile FROM ruby:3.2-slim # Use a slim Ruby image WORKDIR /app COPY Gemfile Gemfile.lock ./ RUN bundle install --jobs 4 --retry 3 --without development test COPY . . # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Example port CMD ["rails", "server", "-b", "0.0.0.0"] #Or another production-appropriate command """ * **Actionable Standard:** Create a "Dockerfile" to define the environment for your Ruby application. * **Actionable Standard:** Use multi-stage builds to reduce the size of the final Docker image by separating build dependencies from runtime dependencies when using interpreted languages like Ruby. * **Actionable Standard:** Utilize a ".dockerignore" file to exclude unnecessary files and directories from the Docker image (e.g., "log/", "tmp/", "test/"). * **Actionable Standard:** Use Docker Compose for local development to manage multi-container applications (e.g., Rails app with a PostgreSQL database). **Anti-pattern:** Building large Docker images with unnecessary dependencies increases deployment time and resource consumption. ### 4.2 Infrastructure as Code Infrastructure as Code (IaC) involves managing and provisioning infrastructure through code rather than manual processes. Terraform and CloudFormation are popular IaC tools. **Do This:** Manage your infrastructure using IaC tools. **Don't Do This:** Manually provision and configure servers. **Why:** IaC automates infrastructure management, improves consistency, and enables infrastructure to be version controlled along with application code. * **Actionable Standard:** Define your infrastructure resources (e.g., servers, databases, networks) as code using Terraform, CloudFormation, or similar tools. * **Actionable Standard:** Store your IaC code in version control along with your application code. * **Actionable Standard:** Use CI/CD pipelines to automate infrastructure deployments and updates. **Anti-pattern:** Manually configuring servers can lead to inconsistencies and configuration drift, making it difficult to manage and maintain infrastructure. These standards provide a comprehensive guide for deploying and managing Ruby applications in production, ensuring robustness, scalability, and maintainability. By following these guidelines, development teams can improve their deployment processes, reduce risks, and deliver high-quality software.
# State Management Standards for Ruby This document outlines the standards for managing state in Ruby applications. It covers approaches to managing application state, data flow, reactivity, and how these principles apply specifically to Ruby. It also includes modern approaches and patterns based on the latest Ruby version and features. ## 1. Introduction to State Management in Ruby State management involves controlling and maintaining the application's data over time. In Ruby, this can range from simple variable assignments within a class to complex data flows in web applications using frameworks like Rails. Effective state management enables predictable application behavior, easier debugging, and improved performance. ### 1.1 Types of State Understanding different types of state is crucial: * **Local State:** State confined to a specific method, block, or object instance. * **Application State:** State shared across multiple parts of the application, accessible by different objects. * **Persistent State:** State stored in a database or other form of persistent storage. * **UI State:** State that manages the visual elements and user interactions in a graphical interface. ### 1.2 Managing Complexity As applications grow, state management becomes more complex. Poorly managed state can lead to: * **Bugs:** Difficult to reproduce or trace due to unpredictable state changes. * **Performance Issues:** Unnecessary state updates or inefficient data access. * **Maintainability Problems:** Code that is hard to understand, modify, or extend. ## 2. General Principles These principles guide state management in Ruby applications. * **Single Source of Truth:** Define and maintain a single, authoritative source for each piece of data. * **Do This:** Ensure that a particular piece of information has one clearly defined origin. Avoid duplication of data. * **Don't Do This:** Store the same information in multiple, unsynchronized locations. * **Why:** Reduces inconsistencies and simplifies updates. Improves data integrity. * **Immutability:** Prefer immutable data structures when possible. * **Do This:** Use "freeze" to make objects immutable after initialization. * **Don't Do This:** Mutate objects directly when the original value is still needed. * **Why:** Simplifies reasoning about state changes and prevents unintended side effects. * **Explicit State Transitions:** Clearly define how the application moves from one state to another. * **Do This:** Use state machines or similar patterns to model state transitions explicitly. * **Don't Do This:** Implicitly change state through loosely coupled actions. * **Why:** Improves traceability and control over application behavior. * **Minimize Shared State:** Reduce the amount of state shared between different parts of the application. * **Do This:** Use dependency injection to isolate components and pass necessary state explicitly. * **Don't Do This:** Rely on global variables or shared mutable objects. * **Why:** Reduces coupling and makes components more independent and reusable. ## 3. State Management Techniques in Ruby ### 3.1 Local State Local state resides within methods or object instances. Proper scoping and encapsulation are crucial. * **Variables:** Use local variables for temporary data. """ruby def calculate_area(width, height) area = width * height # local variable puts "Area: #{area}" area end """ * **Instance Variables:** Store object-specific data. """ruby class Rectangle def initialize(width, height) @width = width # instance variable @height = height # instance variable end def area @width * @height end end """ * **Encapsulation:** Restrict access to internal state using "private" and "protected" access modifiers. """ruby class BankAccount def initialize(balance) @balance = balance end def deposit(amount) @balance += amount end def withdraw(amount) @balance -= amount if amount <= @balance end private # Restrict direct access def current_balance @balance end end """ ### 3.2 Application State Management Managing global state requires careful consideration. * **Global Variables:** Avoid direct use of "$global_variables". They can lead to naming conflicts and unpredictable behavior. * **Don't Do This:** """ruby $app_name = "My Application" # Avoid """ * **Constants:** Suitable for immutable configuration values. """ruby APP_VERSION = "1.2.3" """ * **Singleton Pattern:** Ensure a single instance of a class with global access. """ruby require 'singleton' class Configuration include Singleton attr_accessor :settings def initialize @settings = {} end end # Usage Configuration.instance.settings[:api_key] = "YOUR_API_KEY" puts Configuration.instance.settings[:api_key] """ * **Why:** Provides a controlled way to access shared state. * **Dependency Injection:** Pass dependencies explicitly to avoid tight coupling and make classes testable. """ruby class ReportGenerator def initialize(data_source) @data_source = data_source end def generate data = @data_source.fetch_data # ... generate report end end # Usage data_source = DatabaseDataSource.new report_generator = ReportGenerator.new(data_source) report_generator.generate """ * **Service Objects:** Encapsulate business logic and manage related state transitions. """ruby class CreateUserService def call(params) user = User.new(params) if user.save # ... send welcome email, etc. Result.new(success: true, user: user) else Result.new(success: false, errors: user.errors) end end end # Usage result = CreateUserService.new.call(name: "John Doe", email: "john@example.com") if result.success? puts "User created: #{result.user.name}" else puts "Error: #{result.errors.full_messages.join(', ')}" end """ ### 3.3 Persistent State Management For data that needs to be stored and retrieved, databases are typically used. * **Active Record:** Rails' ORM handles much of the persistence logic. * **Do This:** Define models with appropriate attributes and validations. * **Don't Do This:** Bypass the ORM for common database operations without a compelling reason. """ruby class User < ApplicationRecord validates :email, presence: true, uniqueness: true has_many :posts end """ * **Data Migrations:** Use migrations to manage database schema changes. * **Do This:** Write idempotent migrations that can be run multiple times without errors. * **Don't Do This:** Manually modify database schemas without a migration. ### 3.4 Concurrency and State Managing state concurrently requires special attention. * **Threads:** Ruby's native threads are limited by the Global Interpreter Lock (GIL). Prefer using them for I/O-bound tasks. For CPU-bound parallel processing, consider using processes. """ruby threads = [] 10.times do |i| threads << Thread.new(i) do |thread_num| puts "Thread #{thread_num}: Starting" sleep(rand(1..3)) # Simulate some work puts "Thread #{thread_num}: Finishing" end end threads.each(&:join) puts "All threads finished." """ * **Mutexes:** Use mutexes to protect shared state from concurrent access. """ruby require 'thread' class Counter def initialize @count = 0 @mutex = Mutex.new end def increment @mutex.synchronize do @count += 1 end end def value @count end end counter = Counter.new threads = [] 10.times do threads << Thread.new do 1000.times do counter.increment end end end threads.each(&:join) puts "Counter value: #{counter.value}" """ * **Actors:** The "Celluloid" gem provides an actor model for concurrent state management. * **Why:** Actors provide a higher-level abstraction for concurrency, reducing the risk of race conditions and deadlocks. ## 4. State Management in Rails Rails offers several mechanisms for managing state. ### 4.1 ActiveRecord Models Models encapsulate the state of individual records in the database. * **Attributes:** Define attributes to represent the column values of a database table. * **Associations:** Manage relationships between different models. * **Callbacks:** Trigger actions before or after state changes (e.g., before_save, after_create). Use these judiciously as they can introduce hidden side effects and make debugging harder. """ruby class Article < ApplicationRecord belongs_to :author has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } before_save :normalize_title private def normalize_title self.title = title.titleize end end """ ### 4.2 Sessions Sessions store data related to a specific user across multiple requests. * **Cookies:** Sessions are typically implemented using cookies. * **Security:** Protect session data by setting secure and HttpOnly flags on cookies. * **Storage:** Rails supports various session storage options (e.g., cookies, database, memcached). Consider the performance and security implications of each option. """ruby # Setting a session value session[:user_id] = user.id # Retrieving a session value user_id = session[:user_id] # Clearing a session value session[:user_id] = nil # Resetting the entire session reset_session """ ### 4.3 Caching Caching improves performance by storing frequently accessed data in memory. * **Fragment Caching:** Cache parts of a view. """erb <% cache @article do %> <%= render @article %> <% end %> """ * **Action Caching:** Cache the entire response of an action. * **Low-Level Caching:** Use the "Rails.cache" API for more granular control. """ruby Rails.cache.fetch("article:#{@article.id}", expires_in: 12.hours) do @article.content end """ ### 4.4 Query Objects Encapsulate complex database queries to keep controllers and models clean. """ruby class ArticlesByAuthorQuery def initialize(author) @author = author end def call Article.where(author: @author).published.order(created_at: :desc) end end # Usage articles = ArticlesByAuthorQuery.new(current_user).call """ ### 4.5 State Machines For models with complex state transitions, use a state machine gem like "aasm" or "statesman". """ruby class Order < ApplicationRecord include AASM aasm column: :state do # default column: aasm_state state :pending, initial: true state :processing state :shipped state :delivered state :cancelled event :process do transitions from: :pending, to: :processing end event :ship do transitions from: :processing, to: :shipped end event :deliver do transitions from: :shipped, to: :delivered end event :cancel do transitions from: [:pending, :processing], to: :cancelled end end end """ ## 5. Modern Approaches Using more contemporary design patterns can drastically improve state in Ruby web applications and APIs. ### 5.1 Redux/Flux-inspired patterns While direct ports of Javascript frameworks like Redux or Flux are not common in Ruby (largely because Ruby is often server-side), the underlying principles of unidirectional data flow and centralized state management can be valuable. * **Centralized Store:** A single source of truth for application state. This is often implemented as a Ruby class or module. * **Actions:** Plain Ruby objects (or classes representing commands) that describe an intent to change the state. * **Reducers:** Pure functions that take the current state and an action and return the new state. These should be free of side effects. * **Subscriptions:** Mechanisms for components to be notified when the state changes. This allows parts of the application to react to state updates. """ruby # Example (simplified) # Central Store class AppStore attr_reader :state def initialize @state = { count: 0 } # Initial state @listeners = [] end def dispatch(action) @state = reducer(@state, action) publish_changes end def subscribe(listener) @listeners << listener end private def reducer(state, action) case action[:type] when 'INCREMENT' { count: state[:count] + 1 } when 'DECREMENT' { count: state[:count] - 1 } else state # Default: return current state end end def publish_changes @listeners.each { |listener| listener.call(@state) } end end # Example usage store = AppStore.new # Subscribe to state changes store.subscribe(lambda { |state| puts "Count updated: #{state[:count]}" }) # Dispatch actions store.dispatch({ type: 'INCREMENT' }) # Output: Count updated: 1 store.dispatch({ type: 'DECREMENT' }) # Output: Count updated: 0 """ * **Benefits:** Highly predictable state updates, easier debugging, improved testability. * **Drawbacks:** More boilerplate code, can be overkill for very simple applications. ### 5.2 Event Sourcing Instead of storing the current state of an application entity directly, event sourcing stores a sequence of events that represent changes to the state. The current state can be derived by replaying these events. * **Events:** Immutable records of something that happened in the application (e.g., "OrderCreated", "ItemAddedToCart"). * **Event Store:** A database or specialized storage system for persisting events. * **Projections:** Code that consumes events to build a read-optimized view of the data. This decouples the write (event) side from the read side. * **Benefits:** Auditability, temporal queries (e.g., "What was the state of the order 2 days ago?"), easier debugging of complex state transitions, ability to rebuild state accurately * **Drawbacks:** Increased complexity, requires a different mindset, potentially more read-side performance challenges ### 5.3 CQRS (Command Query Responsibility Segregation) Separates read and write operations, improving performance and scalability. * **Commands:** Objects that represent an intent to change the state. * **Queries:** Objects that retrieve data without modifying the state. * **Benefits:** Optimized read and write paths, improved performance, better scalability, simplified data models. * **Drawbacks:** Increased complexity, requires careful design. ## 6. Anti-Patterns Avoid these common pitfalls. * **God Classes:** Classes that manage too much state and logic. * **Do This:** Break down large classes into smaller, more focused classes. * **Don't Do This:** Keep adding responsibilities to a single class. * **Shotgun Surgery:** Making changes to multiple unrelated parts of the code to achieve a single goal. * **Do This:** Refactor code to reduce dependencies and improve cohesion. * **Don't Do This:** Scatter changes across the codebase. * **Global State Abuse:** Over-reliance on global variables or mutable shared objects. * **Do This:** Use dependency injection or service objects to manage dependencies. * **Don't Do This:** Directly access global state from multiple parts of the application. * **Ignoring Immutability:** Mutating objects when the original value is still needed. * **Do This:** Use immutable data structures or defensive copying. * **Don't Do This:** Modify objects in place. ## 7. Conclusion Effective state management is crucial for building maintainable, scalable, and reliable Ruby applications. By following these standards and best practices, developers can manage state effectively, reduce bugs, and improve application performance. Use the right tool for the job, and continuously refine your state management strategies as your application evolves.
# Performance Optimization Standards for Ruby This document outlines performance optimization standards for Ruby code. It aims to improve application speed, responsiveness, and resource usage by providing actionable guidelines and examples. We'll focus on modern Ruby practices and patterns, with an emphasis on the latest Ruby versions. ## I. Architectural Considerations ### 1. Choosing the Right Ruby Implementation **Do This:** Use Ruby MRI (CRuby) or optimize your selection for specific performance needs based on your workload. **Don't Do This:** Blindly use an implementation without considering its strengths and weaknesses. **Why:** Different Ruby implementations offer varying performance characteristics. * **CRuby (MRI):** The standard implementation, generally well-optimized. Most gem compatibility. * **JRuby:** Runs on the JVM, providing benefits from the JVM's JIT compiler and garbage collector, and integration with Java libraries. Well-suited for I/O heavy operations and scenarios requiring Java interop. * **TruffleRuby:** An optimizing just-in-time (JIT) compiler built on GraalVM. Delivers significant speed improvements in CPU-bound applications but may have compatibility issues with some gems. * **RubyMotion:** (Not actively maintained) Compiles Ruby to native iOS and macOS applications. Not suitable for general purpose backend development. **Code Example (Checking Ruby Implementation):** """ruby puts RUBY_ENGINE # => "ruby" (for MRI), "jruby", "truffleruby" etc. """ ### 2. Load Testing and Benchmarking **Do This:** Load test your application under expected production traffic and benchmark critical code paths. **Don't Do This:** Make performance assumptions without empirical evidence. **Why:** Identifying bottlenecks early is crucial. Tools like "wrk", "ab", and "Benchmark" help find those areas. **Code Example (Benchmarking):** """ruby require 'benchmark' n = 50000 Benchmark.bm do |x| x.report("string interpolation:") { n.times { "result: #{1 + 1}" } } x.report("string concatenation: ") { n.times { "result: " + (1 + 1).to_s } } end # Expected output: # user system total real # string interpolation: 0.040485 0.000000 0.040485 ( 0.040626) # string concatenation: 0.072514 0.000000 0.072514 ( 0.072675) """ ### 3. Choosing the Right Framework **Do This:** Carefully select a framework (e.g., Rails, Sinatra, Hanami) that aligns with your application's complexity and performance needs. **Don't Do This:** Use a heavyweight solution for a lightweight job. **Why:** Frameworks provide structure but impose overhead. Rails, the most popular framework, has conventions and features which can aid speed of development at the expense of runtime performance. For simple API applications, Sinatra or Hanami might be more performant choices. ### 4. Microservices vs. Monolith **Do This:** For large complex systems, consider breaking down the system into microservices to improve scalability and fault isolation. **Don't Do This:** Prematurely break down an application into microservices if this adds excessive overhead of network traffic and inter-service dependencies. **Why:** Decoupling components enables independent scaling and deployment, optimizing resource allocation. ## II. Coding Practices ### 1. Minimize Object Creation **Do This:** Reuse objects whenever possible, especially within loops. Use object pooling techniques for frequently used objects. **Don't Do This:** Create excessive temporary objects, especially inside loops which can trigger frequent garbage collection. **Why:** Object allocation is relatively expensive in Ruby. Reducing it can significantly improve performance. **Code Example (Avoiding unnecessary object creation):** """ruby # Anti-pattern: Creating a new array in each iteration def anti_pattern(data) result = [] data.each { |item| result << item.to_s } result end # Correct: Pre-allocate the array def correct_pattern(data) result = Array.new(data.size) data.each_with_index { |item, index| result[index] = item.to_s } result end """ ### 2. String Manipulation **Do This:** Use efficient string manipulation methods. Prefer "<<" or "concat" over "+" for appending. Consider using frozen strings where appropriate to avoid duplication. **Don't Do This:** Use inefficient string concatenation methods, especially within loops. **Why:** String operations can be performance-intensive. **Code Example:** """ruby # Efficient string = "hello" string << " world" # Less efficient (creates a new string object each time) string = "hello" string = string + " world" # Ruby 2.3+, use frozen string literals when possible # frozen_string_literal: true string = "hello".freeze #Avoids duplicating this string in memory """ ### 3. Iteration **Do This:** Choose the right iteration method based on the task. Use "each" for simple iteration, "map" for transforming elements, and "select" for filtering. For performance-critical loops, consider using "while" or "for" loops, or the C-accelerated versions when operating on "NArray" **Don't Do This:** Use "each" when "map" or "select" is more appropriate, or vice-versa. **Why:** Using correct iterators improves readability and offers performance gains. "while" and "for" loops can offer marginal performance increases in very hot loops. **Code Example:** """ruby # Efficient mapping numbers = [1, 2, 3, 4, 5] squares = numbers.map { |n| n * n } # Efficient filtering even_numbers = numbers.select { |n| n.even? } # Efficient each numbers.each { |n| puts n } """ ### 4. Regular Expressions **Do This:** Compile regular expressions and reuse them. Use non-capturing groups "(?:...)" when capturing is not needed. Be mindful of backtracking complexity. **Don't Do This:** Create regular expressions every time they are needed or neglect complex regexes that can lead to excessive backtracking. **Why:** Compiling regular expressions is expensive. Backtracking can catastrophically degrade performance in complex regular expressions. **Code Example:** """ruby # Compile and reuse EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.freeze def valid_email?(email) EMAIL_REGEX.match?(email) end """ ### 5. Memoization **Do This:** Memoize the results of expensive function calls, especially pure functions. Use "||=" operator or dedicated memoization libraries. **Don't Do This:** Memoize excessively or without a strategy to invalidate the cache when the underlying data changes. **Why:** Avoid recomputing values already calculated. **Code Example:** """ruby def expensive_calculation(n) @cache ||= {} return @cache[n] if @cache.key?(n) puts "Calculating..." result = (1..n).sum # Simulate heavy computation @cache[n] = result result end puts expensive_calculation(10) # Calculates puts expensive_calculation(10) # Retrieves from cache """ ### 6. Lazy Evaluation **Do This:** Use lazy evaluation (e.g., using "Enumerator::Lazy") for large collections and complex operations to avoid unnecessary computations. **Don't Do This:** Use lazy evaluation indiscriminately; it adds overhead that can outweigh its benefits for small datasets. **Why:** Only compute results when and if they are actually needed. **Code Example:** """ruby numbers = (1..Float::INFINITY).lazy.select { |n| n.even? }.map { |n| n * 2 }.first(10) puts numbers # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] """ ### 7. Concurrency and Parallelism **Do This:** Use threads, fibers, or processes for concurrent tasks. Employ parallel processing for CPU-bound tasks using gems like "concurrent-ruby" or "Parallel". Consider using actor-based concurrency (e.g., celluloid). **Don't Do This:** Introduce concurrency without proper synchronization, which can lead to race conditions and deadlocks. Ignore the limitations of MRI's Global Interpreter Lock (GIL). **Why:** Leverage multi-core processors to perform tasks simultaneously. **Code Example (using "concurrent-ruby"):** """ruby require 'concurrent' executor = Concurrent::FixedThreadPool.new(4) # 4 threads futures = 10.times.map do |i| executor.post do puts "Processing task #{i} on thread: #{Thread.current.name}" sleep(rand(1..3)) # Simulate work "Result #{i}" end end results = futures.map(&:value) # Wait for all tasks to complete puts "Results: #{results}" executor.shutdown executor.wait_for_termination """ ### 8. Database Interactions **Do This:** Use efficient database queries, indexes, and caching. Optimize N+1 queries. Utilize connection pooling. **Don't Do This:** Perform inefficient queries or ignore database-level optimizations. **Why:** Database interactions often represent a major performance bottleneck. **Code Example (Addressing N+1 Query with "includes" in Rails):** """ruby # Inefficient # @orders = Order.all # @orders.each { |order| puts order.customer.name } # N+1 queries # Efficient @orders = Order.includes(:customer).all # Eager loads customer data @orders.each { |order| puts order.customer.name } # Single query """ ### 9. Garbage Collection **Do This:** Be aware of the impact of garbage collection. Trigger garbage collection manually when necessary (e.g., after large data processing). Control object allocation, prefer immutable objects or using object pools. Consider using garbage collection tuning options. **Don't Do This:** Ignore garbage collection and let it become a source of performance problems due to excessive pauses. **Why:** Excessive garbage collection pauses can significantly impact performance. **Code Example:** """ruby # Force garbage collection GC.start # Disable garbage collection GC.disable # Enable garbage collection GC.enable """ However, manually triggering GC is generally best avoided unless you *know* there has been a lot of dead object creation and the GC hasn't kicked in. ### 10. Use Profilers **Do This:** Use tools like RubyProf, stackprof, or flamegraph to identify hotspots in your code. **Don't Do This:** Guess where performance bottlenecks lie; use profiling to locate them. **Why:** Pinpoint the precise location of performance bottlenecks for focused optimization. **Code Example (using RubyProf):** """ruby require 'ruby-prof' RubyProf.start # Your code here def my_slow_method 100000.times { Math.sqrt(rand) } end my_slow_method result = RubyProf.stop printer = RubyProf::FlatPrinter.new(result) printer.print(STDOUT) # Prints profiling results """ ## III. Memory Management ### 1. Object Allocation **Do This:** Minimize unnecessary object allocations, especially within loops or frequently called methods. Use object pooling or reuse existing objects when possible. **Don't Do This:** Create many short-lived objects. These will be collected frequently & cause overhead on the garbage collector. **Why:** Allocating objects is time-consuming. Minimizing it contributes directly to performance improvements. **Code example:** """ruby # Inefficient def generate_strings(n) n.times.map { |i| "String #{i}" } # Each iteration creates a string end # Efficient def generate_strings_efficient(n) base_string = "String ".dup # Create one string n.times.map { |i| base_string.dup << i.to_s } # Copy and append the index number end """ ### 2. Immutable Objects **Do This**: Use immutable objects whenever applicable. Since they can't be modified, they can be safely shared and cached without concern for unintended modifications. **Don't Do This**: Avoid using mutable objects when you need to share state or data as they need to be duplicated to prevent issues. **Why**: Immutable objects simplify reasoning about program state and allow optimizations such as sharing or reusing them. Consider using libraries like "hamster" for immutable data structures. ### 3. Weak References **Do This**: Use Weak References if memory is low and some objects are not always necessary, but cached 'just in case'. **Don't Do This**: Do it without the WeakRef support. **Why**: Weak References are garbage collected if memory is needed by ruby and it won't complain about it. Code Example: """ruby require 'weakref' obj = Object.new weak_ref = WeakRef.new(obj) puts weak_ref.weakref_alive? # => true obj = nil # Remove reference to the object GC.start # Force garbage collection puts weak_ref.weakref_alive? # => false """ ## IV. Specific Concerns: Rails Applications ### 1. Asset Pipeline **Do This:** Precompile assets for production, use a CDN for static assets, and minimize asset size (e.g., using minification and compression). **Don't Do This:** Serve uncompiled assets in production, or include large and unoptimized assets. **Why:** Optimize the delivery of assets significantly improves page load times. ### 2. Query Optimization **Do This:** Optimize database queries using indexes, eager loading ("includes"), and avoiding N+1 queries. Utilize database-specific features (e.g., PostgreSQL's JSONB indexes). **Don't Do This:** Ignore slow queries or rely on inefficient database access patterns. ### 3. Caching **Do This:** Implement caching strategies at various levels (e.g., fragment caching, page caching, low-level caching with "Rails.cache"). Use a cache store like Redis or Memcached. **Don't Do This:** Cache inappropriately (e.g., caching personal data), or use overly aggressive or non-expiring caches. ### 4. Background Jobs **Do This:** Offload long-running tasks to background jobs (e.g., using Sidekiq, Resque, or Delayed Job) to improve web response times. **Don't Do This:** Perform complex and time-consuming operations within request-response cycles. ### 5. Middleware **Do This:** Carefully select and configure middleware components. Removing unnecessary middleware can reduce request processing overhead. **Don't Do This:** Add a lot of middlewares without actually seeing the benefits, as they can impact the performance negatively. **Conclusion** These performance optimization standards serve as a comprehensive guide for Ruby developers. By adhering to these guidelines, developers can create efficient, responsive, and scalable Ruby applications. Remember that continuous profiling, benchmarking, and monitoring are essential for identifying and addressing performance bottlenecks. Regularly revisiting and updating these standards will ensure they remain relevant with evolving Ruby versions and best practices.
# Testing Methodologies Standards for Ruby This document outlines the standards for testing methodologies in Ruby projects. Adhering to these standards promotes code quality, reduces bugs, improves maintainability, and facilitates collaboration. These standards are designed to be compatible with modern Ruby (3.x+) and leverage modern libraries and features. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) Philosophy * **Do This:** Embrace TDD by writing tests *before* implementing the corresponding code. Follow the Red-Green-Refactor cycle. This forces you to think about the design and expected behavior of your code. * **Don't Do This:** Write tests as an afterthought or skip them entirely. This leads to unverified code, increased bug rates, and difficulty in refactoring. * **Why:** TDD leads to more robust, well-designed code with higher test coverage and better separation of concerns. It also provides a safety net for future changes. ### 1.2. Testing Pyramid * **Do This:** Follow the testing pyramid principle: a broad base of unit tests, a smaller layer of integration tests, and an even smaller layer of end-to-end (E2E) tests. This provides a good balance between speed, cost, and coverage. * **Don't Do This:** Rely solely on E2E tests or have a disproportionate number of integration tests compared to unit tests. This makes tests slow, brittle, and expensive to maintain. * **Why:** Unit tests are fast and isolate problems efficiently. End-to-end tests are slow and brittle but verify the system as a whole. Finding the right balance reduces the overall cost of testing and improves confidence. ### 1.3. Test Coverage * **Do This:** Aim for high test coverage (e.g., >80%), but don't treat this as the sole goal. Focus on testing critical paths, edge cases, and potential failure points. Use tools like "SimpleCov" to measure test coverage. * **Don't Do This:** Strive for 100% coverage at the expense of meaningful tests. Having tests that simply execute the code without asserting specific behavior is wasteful. * **Why:** Test coverage is a useful metric, but quality of tests matters more. Focus on writing meaningful tests that verify the behavior of your code under different scenarios. ### 1.4. Test Clarity and Maintainability * **Do This:** Write tests that are clear, concise, and easy to understand. Use descriptive names for tests, and follow a consistent structure. Ensure tests are independent and don't rely on side effects from other tests. * **Don't Do This:** Write cryptic tests with unclear assertions or dependencies on external state. Long, convoluted tests are difficult to debug and maintain. * **Why:** Maintainable tests are as important as maintainable code. Readable tests allow developers to understand the intended behavior and easily identify failures. ## 2. Unit Testing ### 2.1. Unit Testing Frameworks * **Do This:** Use a modern unit testing framework like "Minitest" (Ruby's built-in framework) or RSpec. "Minitest" is lightweight and comes standard, while RSpec offers a more expressive DSL. * **Don't Do This:** Roll your own unit testing framework or use outdated libraries. Leverage well-established, community-maintained tools instead. * **Why:** Established frameworks provide the necessary features (assertions, test runners, mocks, stubs) for effective unit testing, while offering a standardized structure and convention. ### 2.2. Assertions * **Do This:** Use appropriate assertion methods to verify the expected behavior of units of code. Minitest provides a range of assertions (e.g., "assert_equal", "assert_raises", "assert_instance_of"). * **Don't Do This:** Rely solely on print statements or exceptions to check for correctness. Assertions provide a clear and reliable way to verify behavior. * **Why:** Assertions clearly express the expected outcome of a test and provide detailed error messages when failures occur, making debugging much easier. **Example (Minitest):** """ruby require 'minitest/autorun' class Calculator def add(a, b) a + b end end class TestCalculator < Minitest::Test def setup @calculator = Calculator.new end def test_add_positive_numbers assert_equal 5, @calculator.add(2, 3) end def test_add_negative_numbers assert_equal -5, @calculator.add(-2, -3) end def test_add_mixed_numbers assert_equal 1, @calculator.add(3, -2) end end """ **Example (RSpec):** """ruby require 'rspec' class Calculator def add(a, b) a + b end end RSpec.describe Calculator do let(:calculator) { Calculator.new } it 'adds positive numbers correctly' do expect(calculator.add(2, 3)).to eq(5) end it 'adds negative numbers correctly' do expect(calculator.add(-2, -3)).to eq(-5) end it 'adds mixed numbers correctly' do expect(calculator.add(3, -2)).to eq(1) end end """ ### 2.3. Mocking and Stubbing * **Do This:** Use mocking and stubbing to isolate the unit under test by replacing its dependencies with controlled substitutes. Use mocking libraries like "mocha", "rspec-mocks", or "test_double". * **Don't Do This:** Directly interact with external resources (databases, APIs) in unit tests. This makes tests slow, brittle, and dependent on external factors. * **Why:** Mocking and stubbing enable you to test units independently, ensuring that failures are localized and that your test doesn't rely on the availability or behavior of external resources. **Example (Minitest with Mocha):** """ruby require 'minitest/autorun' require 'mocha/minitest' class PaymentProcessor def charge(credit_card, amount) # Calls an external payment gateway PaymentGateway.new.process_payment(credit_card, amount) end end class TestPaymentProcessor < Minitest::Test def test_charge_calls_payment_gateway processor = PaymentProcessor.new gateway_mock = Minitest::Mock.new gateway_mock.expect(:process_payment, true, [String, Integer]) # expected call with arguments PaymentGateway.stub :new, gateway_mock do processor.charge("1234-5678-9012-3456", 100) end gateway_mock.verify # verify the invocation end end class PaymentGateway def process_payment(credit_card, amount) # Imagine actual payment processing happens here true # Simulate success end end """ **Example (RSpec with RSpec Mocks):** """ruby require 'rspec' class PaymentProcessor def charge(credit_card, amount) # Calls an external payment gateway PaymentGateway.new.process_payment(credit_card, amount) end end RSpec.describe PaymentProcessor do it 'calls the payment gateway with the correct arguments' do payment_gateway = instance_double(PaymentGateway) allow(PaymentGateway).to receive(:new).and_return(payment_gateway) expect(payment_gateway).to receive(:process_payment).with('1234-5678-9012-3456', 100) PaymentProcessor.new.charge('1234-5678-9012-3456', 100) end end class PaymentGateway def process_payment(credit_card, amount) # Imagine actual payment processing happens here true # Simulate success end end """ ### 2.4. Test Data * **Do This:** Use clear and relevant test data. Consider using factories (like "FactoryBot" or "Fabricate") to generate test data in a consistent and maintainable manner. * **Don't Do This:** Hardcode data directly into tests. This makes tests brittle and difficult to understand as the system evolves. * **Why:** Factories provide a centralized way to create test data, ensuring consistency and reducing duplication. This also simplifies test setup and maintenance. **Example(FactoryBot):** """ruby # spec/factories/users.rb FactoryBot.define do factory :user do first_name { "John" } last_name { "Doe" } email { "john.doe@example.com" } end end """ """ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do it "is valid with valid attributes" do user = FactoryBot.build(:user) expect(user).to be_valid end end """ ## 3. Integration Testing ### 3.1. Integration Testing Scope * **Do This:** Focus on testing the interaction between different components or modules of your application. For example, test how a controller interacts with a model, or how two services communicate with each other. * **Don't Do This:** Use integration tests to test individual units of code. That is the domain of unit tests. Also, don't use them to test the entire system in one go; this should be reserved for end-to-end tests. * **Why:** Integration tests verify that the different parts of your application work together as expected, uncovering issues that may not be apparent in unit tests. ### 3.2. Database Interactions * **Do This:** Use database transactions and rollbacks to keep tests isolated and ensure a clean database state. Consider using database cleaner libraries like "DatabaseCleaner" to manage database setup and teardown. * **Don't Do This:** Directly manipulate the database without proper cleanup. This leads to test pollution and unpredictable results. * **Why:** Database interactions are inherently stateful and can easily lead to test interference. Using transactions and cleanup mechanisms guarantees that each test runs in isolation and the database is always in a consistent state. **Example (Rails with DatabaseCleaner):** """ruby # spec/rails_helper.rb RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end config.before(:each) do DatabaseCleaner.strategy = :transaction end config.before(:each, js: true) do DatabaseCleaner.strategy = :truncation end config.before(:each) do DatabaseCleaner.start end config.after(:each) do DatabaseCleaner.clean end end """ ### 3.3. API Integrations * **Do This:** Use tools like "VCR" or "Webmock" to record and replay HTTP interactions with external APIs. This ensures tests are fast, reliable, and don't depend on the availability or behavior of external services. * **Don't Do This:** Directly call external APIs during integration tests. This is slow, unreliable, and potentially costly. * **Why:** Replaying recorded interactions speeds up tests, avoids hitting external APIs unnecessarily, and allows you to simulate different API responses (success, error, timeout). **Example (VCR):** """ruby # spec/support/vcr.rb require 'vcr' VCR.configure do |config| config.cassette_library_dir = "spec/fixtures/vcr_cassettes" config.hook_into :webmock # or :fakeweb config.configure_rspec_metadata! end """ """ruby # spec/services/external_api_service_spec.rb require 'rails_helper' RSpec.describe ExternalApiService do describe '#get_data', vcr: { cassette_name: 'external_api_service/get_data' } do it 'fetches data from the external API' do service = ExternalApiService.new data = service.get_data expect(data).to be_an(Hash) # Add more specific assertions as needed end end end """ ## 4. End-to-End (E2E) Testing ### 4.1. E2E Testing Scope * **Do This:** Focus on testing the critical user workflows through the entire application stack, from the user interface to the database. Simulate real user interactions as closely as possible. * **Don't Do This:** Attempt to test every single feature with E2E tests. This leads to slow, brittle, and unmaintainable tests. E2E tests should be limited to mission-critical workflows. * **Why:** E2E tests provide the highest level of confidence that the system works as a whole and that user workflows are not broken. They catch integration issues that might be missed by unit and integration tests. ### 4.2. E2E Testing Frameworks * **Do This:** Use a dedicated E2E testing framework like Selenium, Capybara, or Cypress. These tools provide features for automating browser interactions and verifying application behavior. For Rails applications, system tests using Capybara are a good starting point. * **Don't Do This:** Manually test every user workflow or rely on ad-hoc testing methods. Automated E2E tests are essential for continuous integration and continuous deployment. * **Why:** E2E testing frameworks provide a robust and automated way to simulate user interactions, verify UI elements, and assert application behavior. **Example (Rails System Test with Capybara):** """ruby # spec/system/users_spec.rb require 'rails_helper' RSpec.describe "Users", type: :system do before do driven_by(:rack_test) # Or :selenium, :headless_chrome end it "allows a user to sign up" do visit new_user_registration_path fill_in "Email", with: "user@example.com" fill_in "Password", with: "password" fill_in "Password confirmation", with: "password" click_button "Sign up" expect(page).to have_content("Welcome! You have signed up successfully.") end end """ ### 4.3. Test Data Management * **Do This:** Use separate test environments with dedicated databases and configurations for E2E tests. Seed the database with realistic test data to simulate production scenarios. * **Don't Do This:** Run E2E tests against production environments or use production data. This is risky and can lead to data corruption or security breaches. * **Why:** Running E2E tests in isolated environments ensures that tests don't interfere with production systems and that you can safely manipulate test data without affecting real users. ### 4.4. Asynchronous Operations and Timing * **Do This:** Be mindful of asynchronous operations and timing issues in E2E tests. Use explicit waits to allow elements to appear on the page or API calls to complete before making assertions. * **Don't Do This:** Rely on implicit waits or hardcoded delays. These are unreliable and can cause tests to fail intermittently. * **Why:** Web applications often involve asynchronous operations, such as AJAX calls or JavaScript animations. Explicit waits ensure that tests wait for these operations to complete before making assertions, preventing false positives and flaky tests. """ruby # Example using Capybara's "have_content" with a timeout: expect(page).to have_content("Success!", wait: 10) """ ## 5. Continuous Integration and Delivery (CI/CD) * **Do This:** Integrate your tests into your CI/CD pipeline. Run all tests (unit, integration, E2E) automatically on every commit or pull request. Use tools like Jenkins, CircleCI, GitHub Actions, or GitLab CI. * **Don't Do This:** Manually run tests or skip testing in your CI/CD pipeline. This increases the risk of introducing bugs into production. * **Why:** Automated testing in CI/CD provides continuous feedback on code quality and prevents regressions from being deployed to production. ## 6. Performance Testing * **Do This:** Integrate performance testing into your development process, ideally as part of your CI/CD pipeline. Use tools like ApacheBench, JMeter, or specialized Ruby libraries for benchmarking. * **Don't Do This:** Neglect performance testing until late in the development cycle. Addressing performance issues early is much easier and cheaper than fixing them in production. * **Why:** Performance testing helps identify bottlenecks and ensures that your application can handle the expected load. ## 7. Security Testing * **Do This:** Incorporate security testing into your development workflow. Use static analysis tools like Brakeman to identify potential security vulnerabilities. * **Don't Do This:** Ignore security concerns until a security breach occurs. Proactive security testing is essential for protecting your application and user data. * **Why:** Security testing helps identify and mitigate vulnerabilities before they can be exploited by attackers. Static analysis tools automate the process of finding common security flaws. ## 8. Code Review * **Do This:** Include tests as a crucial part of your code review process. Ensure that tests are clear, comprehensive, and cover all critical aspects of the code. * **Don't Do This:** Approve code changes without reviewing the associated tests. This is a recipe for disaster. * **Why:** Code review is an opportunity to ensure that tests meet the required standards and effectively protect against potential issues. By following these guidelines, you can create a robust and reliable testing strategy for your Ruby projects, resulting in higher quality code, reduced bug rates, and improved maintainability. Remember to adapt these standards to the specific needs of your project and team.