Overview
KISS (Keep It Simple, Stupid) is a design principle stating that systems work best when kept simple rather than made complicated. The principle originated in the U.S. Navy in 1960, coined by aircraft engineer Kelly Johnson, who challenged his design team to create aircraft systems that average mechanics could repair in combat conditions with minimal tools. The principle translates directly to software development: code should be simple enough that any competent developer can understand and maintain it.
The core premise asserts that simplicity should be a key goal in design, and unnecessary complexity should be avoided. This does not advocate for simplistic or naive solutions, but rather for choosing the most straightforward approach that solves the problem effectively. A simple solution reduces cognitive load, minimizes potential bugs, and facilitates maintenance and extension.
In software development, KISS manifests in multiple dimensions: algorithmic complexity, architectural design, code readability, API design, and system integration. A function that accomplishes its task in 10 lines of clear code adheres to KISS better than a 50-line function that achieves the same result with clever abstractions. Similarly, a system built with three components adheres to KISS better than one built with ten components, assuming both satisfy the requirements.
# KISS violation: Over-engineered solution
class StringReverser
def initialize(string)
@string = string
@cache = {}
end
def reverse
return @cache[@string] if @cache.key?(@string)
result = []
@string.chars.each { |char| result.unshift(char) }
@cache[@string] = result.join
end
end
reverser = StringReverser.new("hello")
reverser.reverse
# => "olleh"
# KISS adherent: Simple solution
"hello".reverse
# => "olleh"
The first example introduces unnecessary complexity through caching (premature optimization), a class wrapper for a single operation, and manual character manipulation. The second example uses Ruby's built-in method, achieving the same result with clarity and maintainability.
Key Principles
KISS operates on several fundamental principles that guide decision-making in software design. These principles help developers identify when solutions diverge from simplicity.
Minimize Moving Parts: Each component, class, method, or abstraction in a system represents a moving part that can fail, needs testing, requires documentation, and adds cognitive overhead. KISS dictates including only components that directly contribute to solving the problem. A system with fewer parts has fewer failure modes and requires less mental effort to understand.
Prefer Clarity Over Cleverness: Clever code often exploits language features or algorithmic tricks that, while impressive, obscure intent. KISS prioritizes code that clearly expresses what it does over code that impresses with technical sophistication. A developer reading the code six months later should understand the logic without decoding clever constructs.
# Clever but unclear
result = (1..100).reduce([]) { |a, n| n % 15 == 0 ? a << "FizzBuzz" : n % 3 == 0 ? a << "Fizz" : n % 5 == 0 ? a << "Buzz" : a << n; a }
# Clear and simple
result = (1..100).map do |n|
if n % 15 == 0
"FizzBuzz"
elsif n % 3 == 0
"Fizz"
elsif n % 5 == 0
"Buzz"
else
n
end
end
The first version combines reduce with nested ternary operators, requiring significant parsing to understand. The second version makes the logic explicit with familiar conditionals.
Avoid Premature Abstraction: Abstraction creates complexity by introducing indirection. While abstraction serves important purposes, introducing it before understanding the problem domain or identifying genuine patterns leads to unnecessary complexity. KISS recommends starting with concrete implementations and abstracting only when duplication or complexity justifies it.
Use Proven Patterns Before Inventing New Ones: Established patterns, algorithms, and architectures exist because they solve common problems effectively. KISS favors these proven approaches over novel solutions unless the novel solution significantly improves the outcome. A developer familiar with the pattern can understand the implementation immediately.
Optimize for Readability: Code is read far more often than written. KISS prioritizes readability over write-time convenience, character count, or perceived elegance. Descriptive variable names, explicit logic, and clear structure all contribute to simplicity even if they require more keystrokes initially.
Solve the Actual Problem: KISS insists on solving the problem at hand rather than anticipated future problems. Building infrastructure for hypothetical future requirements adds complexity that may never provide value. Each feature, method, or component should address a current, concrete need.
# Over-engineering for hypothetical futures
class ConfigurationManager
def initialize
@providers = {}
@cache = {}
@observers = []
end
def register_provider(name, provider)
@providers[name] = provider
notify_observers(:provider_registered, name)
end
def get_value(key, provider: :default)
cache_key = "#{provider}:#{key}"
return @cache[cache_key] if @cache.key?(cache_key)
value = @providers[provider]&.fetch(key)
@cache[cache_key] = value
value
end
def add_observer(observer)
@observers << observer
end
private
def notify_observers(event, data)
@observers.each { |obs| obs.notify(event, data) }
end
end
# Current actual requirement: read values from hash
config = { database_url: "postgres://localhost" }
database_url = config[:database_url]
The ConfigurationManager introduces multiple providers, caching, and an observer pattern when the actual requirement involves reading values from a single hash. This complexity imposes maintenance burden without solving any real problem.
Design Considerations
Applying KISS requires recognizing when simplicity serves the project and when additional complexity provides necessary value. The principle does not mandate simplicity at all costs, but rather challenges developers to justify complexity.
When Simplicity Takes Priority: Early project stages benefit most from KISS. When requirements remain fluid, simple implementations allow rapid iteration and easy modification. Prototypes, minimum viable products, and exploratory code should prioritize simplicity to maintain development velocity. Internal tools, scripts, and one-off utilities rarely justify complex architectures.
Projects with small teams or limited maintenance resources benefit from simple systems. A team of two developers cannot maintain a microservices architecture as effectively as a monolithic application. Similarly, projects with infrequent changes need minimal abstraction since the cost of duplication remains low.
When Complexity Provides Value: Performance-critical systems sometimes require complex algorithms or optimizations. A simple O(n²) algorithm may fail where a complex O(n log n) algorithm succeeds. However, the performance requirement must be measured and real, not hypothetical.
Systems with high concurrency, distributed components, or fault-tolerance requirements often need sophisticated architectures. A simple synchronous system may prove inadequate where an event-driven architecture with message queues succeeds. The complexity becomes justified when failure to implement it prevents the system from meeting requirements.
Trade-off Analysis: Each design decision involves trade-offs between simplicity and other qualities. Adding a caching layer increases complexity but may provide necessary performance. Introducing a dependency injection framework adds abstraction but may enable testability. KISS requires evaluating whether the benefits justify the complexity cost.
# Simple but limited: single database connection
class Database
def self.connection
@connection ||= PG.connect(dbname: 'myapp')
end
def self.query(sql)
connection.exec(sql)
end
end
# Complex but necessary for high concurrency
class Database
def self.pool
@pool ||= ConnectionPool.new(size: 5, timeout: 5) do
PG.connect(dbname: 'myapp')
end
end
def self.query(sql)
pool.with { |conn| conn.exec(sql) }
end
end
The first implementation provides sufficient functionality for low-traffic applications. The second adds complexity through connection pooling, justified only when concurrent requests exceed single-connection capacity.
Measuring Complexity: Objective metrics help identify when solutions violate KISS. Cyclomatic complexity measures the number of independent paths through code. Methods with complexity above 10 typically indicate unnecessarily complex logic. Lines of code per method, depth of inheritance trees, and number of dependencies all signal potential KISS violations.
Incremental Complexity: KISS does not prevent systems from becoming complex over time. As requirements expand, systems naturally grow. The principle requires that each addition be justified by genuine need rather than speculative future-proofing. A system that grows from simple to complex through incremental, justified additions respects KISS more than a system that starts complex to accommodate hypothetical requirements.
Ruby Implementation
Ruby's design philosophy aligns closely with KISS principles. Yukihiro Matsumoto created Ruby to prioritize programmer happiness and productivity through simplicity and expressiveness. The language provides simple, intuitive syntax and follows the principle of least astonishment.
Simple Syntax: Ruby minimizes syntactic noise. Methods can be called without parentheses, blocks provide clean iteration syntax, and implicit returns eliminate unnecessary keywords.
# Ruby's simple syntax
users.select { |u| u.active? }
.sort_by(&:name)
.first(10)
# Equivalent in more verbose languages would require
# explicit returns, semicolons, and type declarations
Built-in Methods: Ruby's standard library provides methods for common operations, eliminating the need for custom implementations. The Array, Hash, String, and Enumerable classes include comprehensive functionality.
# Using Ruby's built-in methods (KISS adherent)
numbers = [1, 2, 3, 4, 5]
sum = numbers.sum
average = numbers.sum / numbers.size.to_f
# => 3.0
# Avoiding built-ins (unnecessary complexity)
numbers = [1, 2, 3, 4, 5]
sum = numbers.reduce(0) { |acc, n| acc + n }
average = sum / numbers.length.to_f
# => 3.0
Duck Typing: Ruby's duck typing eliminates complex type hierarchies and interfaces. Objects need only respond to the required methods rather than inheriting from specific classes.
# Simple duck typing
class FileLogger
def log(message)
File.write('log.txt', "#{message}\n", mode: 'a')
end
end
class ConsoleLogger
def log(message)
puts message
end
end
# Both work with any code expecting a logger
def process_data(data, logger)
logger.log("Processing #{data}")
# process data
logger.log("Completed")
end
process_data(data, FileLogger.new)
process_data(data, ConsoleLogger.new)
No abstract logger class or interface exists. Objects simply implement the log method, demonstrating KISS through simplicity.
Gems Over Custom Code: Ruby's gem ecosystem provides solutions for common problems. Using established gems aligns with KISS by avoiding custom implementations of solved problems.
# Using a gem (KISS adherent)
require 'http'
response = HTTP.get('https://api.example.com/data')
data = JSON.parse(response.body)
# Custom HTTP implementation (unnecessary complexity)
require 'socket'
def http_get(url)
uri = URI.parse(url)
socket = TCPSocket.new(uri.host, uri.port || 80)
socket.print("GET #{uri.path} HTTP/1.1\r\n")
socket.print("Host: #{uri.host}\r\n")
socket.print("Connection: close\r\n")
socket.print("\r\n")
response = socket.read
socket.close
# Parse HTTP response...
response
end
Convention Over Configuration: Rails exemplifies KISS through convention over configuration. Following naming conventions eliminates configuration code.
# Rails conventions eliminate configuration
class User < ApplicationRecord
# Automatically maps to 'users' table
# Automatically includes id, created_at, updated_at
has_many :posts # Finds Post class and foreign key user_id
end
class Post < ApplicationRecord
belongs_to :user # Finds User class and foreign key user_id
end
# No XML configuration, no explicit mappings needed
Blocks and Iterators: Ruby's blocks provide simple iteration without boilerplate.
# Simple block iteration
File.open('data.txt') do |file|
file.each_line do |line|
puts line.upcase
end
end
# File automatically closed
# More complex without blocks
file = File.open('data.txt')
begin
line = file.gets
while line
puts line.upcase
line = file.gets
end
ensure
file.close
end
Principle of Least Surprise: Ruby methods behave intuitively. The empty? method returns a boolean, the size method returns an integer, and the map method returns a new array. This consistency reduces complexity by meeting developer expectations.
Practical Examples
Example 1: User Authentication
Many applications need user authentication. The simple approach checks credentials directly against stored values.
# Simple authentication (KISS adherent)
class User < ApplicationRecord
has_secure_password
def self.authenticate(email, password)
user = find_by(email: email)
user&.authenticate(password) ? user : nil
end
end
# Usage
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
redirect_to dashboard_path
else
flash[:error] = "Invalid credentials"
render :login
end
This implementation uses Rails' built-in has_secure_password, provides a straightforward authentication method, and handles the common case simply. Contrast with an over-engineered approach:
# Over-engineered authentication
class AuthenticationService
def initialize(strategy_factory)
@strategy_factory = strategy_factory
end
def authenticate(credentials)
strategy = @strategy_factory.create_strategy(credentials.type)
result = strategy.authenticate(credentials)
if result.success?
TokenGenerator.new(result.user).generate
else
raise AuthenticationError.new(result.error_code)
end
end
end
class PasswordAuthenticationStrategy
def authenticate(credentials)
user = UserRepository.new.find_by_email(credentials.email)
return AuthenticationResult.failure(:user_not_found) unless user
hasher = PasswordHasher.new(user.salt)
return AuthenticationResult.failure(:invalid_password) unless hasher.verify(credentials.password, user.password_hash)
AuthenticationResult.success(user)
end
end
# Multiple additional classes needed: StrategyFactory, TokenGenerator,
# AuthenticationResult, UserRepository, PasswordHasher, etc.
The complex version introduces eight classes where one sufficed. The abstraction provides no current value and significantly increases maintenance burden.
Example 2: Data Processing Pipeline
Processing data from a CSV file requires reading, transforming, and saving results.
# Simple pipeline (KISS adherent)
require 'csv'
def process_sales_data(filename)
results = []
CSV.foreach(filename, headers: true) do |row|
next if row['amount'].to_f < 100 # Filter small sales
results << {
customer: row['customer'],
amount: row['amount'].to_f,
taxed_amount: row['amount'].to_f * 1.08
}
end
File.write('processed_sales.json', JSON.generate(results))
results.size
end
processed_count = process_sales_data('sales.csv')
puts "Processed #{processed_count} records"
This straightforward implementation reads the CSV, filters and transforms data inline, and writes output. Compare with an over-abstracted approach:
# Over-abstracted pipeline
class DataPipeline
def initialize
@stages = []
end
def add_stage(stage)
@stages << stage
end
def execute(input)
@stages.reduce(input) { |data, stage| stage.process(data) }
end
end
class CSVReaderStage
def process(filename)
CSV.foreach(filename, headers: true).map { |row| row.to_h }
end
end
class FilterStage
def initialize(field, threshold)
@field = field
@threshold = threshold
end
def process(data)
data.select { |row| row[@field].to_f >= @threshold }
end
end
class TransformStage
def initialize(transformations)
@transformations = transformations
end
def process(data)
data.map do |row|
@transformations.each { |field, fn| row[field] = fn.call(row) }
row
end
end
end
class JSONWriterStage
def initialize(filename)
@filename = filename
end
def process(data)
File.write(@filename, JSON.generate(data))
data
end
end
# Usage requires extensive setup
pipeline = DataPipeline.new
pipeline.add_stage(CSVReaderStage.new)
pipeline.add_stage(FilterStage.new('amount', 100))
pipeline.add_stage(TransformStage.new({
'taxed_amount' => ->(row) { row['amount'].to_f * 1.08 }
}))
pipeline.add_stage(JSONWriterStage.new('processed_sales.json'))
result = pipeline.execute('sales.csv')
The pipeline abstraction introduces five classes and complex setup for a three-step process. This complexity becomes justified only when the pipeline requires dynamic configuration or many different combinations of stages.
Example 3: API Client
Building an API client to fetch user data from a REST endpoint.
# Simple API client (KISS adherent)
require 'net/http'
require 'json'
class UserClient
BASE_URL = 'https://api.example.com'
def initialize(api_key)
@api_key = api_key
end
def get_user(user_id)
uri = URI("#{BASE_URL}/users/#{user_id}")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{@api_key}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
return nil unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
end
end
client = UserClient.new(ENV['API_KEY'])
user = client.get_user(123)
puts user['name'] if user
This implementation provides the needed functionality with minimal abstraction. The over-engineered alternative:
# Over-engineered API client
class APIClient
def initialize(config)
@config = config
@middleware_stack = MiddlewareStack.new
@cache = RequestCache.new
@rate_limiter = RateLimiter.new(config.rate_limit)
end
def get(path, options = {})
request = Request.new(:get, path, options)
execute_request(request)
end
private
def execute_request(request)
@rate_limiter.wait_if_needed
cache_key = request.cache_key
return @cache.get(cache_key) if @cache.has?(cache_key) && request.cacheable?
response = @middleware_stack.execute(request) do |req|
HTTPAdapter.new(@config).execute(req)
end
@cache.set(cache_key, response) if request.cacheable?
response
end
end
# Requires: MiddlewareStack, RequestCache, RateLimiter,
# Request, HTTPAdapter, plus various middleware classes
The complex version adds caching, rate limiting, middleware, and extensive configuration for a single API call. These features may become necessary, but implementing them prematurely violates KISS.
Common Patterns
Several patterns consistently demonstrate KISS principles in practice.
Flat Over Nested: Flat structures reduce complexity by eliminating hierarchy. A flat list of functions or modules proves easier to navigate than deeply nested namespaces.
# Flat structure (KISS adherent)
module Reports
def self.generate_sales_report(data)
# ...
end
def self.generate_inventory_report(data)
# ...
end
def self.generate_user_report(data)
# ...
end
end
Reports.generate_sales_report(data)
# Unnecessary nesting
module Reports
module Generators
module Sales
class Report
def self.generate(data)
# ...
end
end
end
end
end
Reports::Generators::Sales::Report.generate(data)
Composition Over Inheritance: Composition creates simpler relationships than deep inheritance hierarchies.
# Composition (KISS adherent)
class TextProcessor
def initialize
@spell_checker = SpellChecker.new
@grammar_checker = GrammarChecker.new
end
def process(text)
text = @spell_checker.check(text)
text = @grammar_checker.check(text)
text
end
end
# Complex inheritance
class Processor
def process(text)
text
end
end
class SpellCheckingProcessor < Processor
def process(text)
check_spelling(super)
end
end
class GrammarCheckingProcessor < SpellCheckingProcessor
def process(text)
check_grammar(super)
end
end
Direct Over Indirect: Direct method calls prove simpler than multiple layers of indirection.
# Direct (KISS adherent)
class OrderProcessor
def process(order)
validate_order(order)
calculate_total(order)
charge_payment(order)
send_confirmation(order)
end
private
def validate_order(order)
# ...
end
end
# Indirect
class OrderProcessor
def process(order)
execute_step(:validate, order)
execute_step(:calculate, order)
execute_step(:charge, order)
execute_step(:confirm, order)
end
private
def execute_step(step_name, order)
method_name = "#{step_name}_order"
send(method_name, order) if respond_to?(method_name, true)
end
end
Explicit Over Implicit: Explicit behavior proves easier to understand than magic behavior.
# Explicit (KISS adherent)
class User
attr_reader :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
# Implicit (too clever)
class User
def initialize(attributes = {})
attributes.each do |key, value|
instance_variable_set("@#{key}", value)
self.class.send(:attr_reader, key)
end
end
end
Standard Library First: Using standard library methods demonstrates KISS by avoiding custom implementations.
# Using standard library (KISS adherent)
words = text.split.map(&:downcase).uniq.sort
# Custom implementation
def unique_sorted_words(text)
words = []
current_word = ''
text.each_char do |char|
if char == ' '
words << current_word unless current_word.empty?
current_word = ''
else
current_word += char
end
end
words << current_word unless current_word.empty?
# Manual deduplication and sorting
unique_words = []
words.each { |w| unique_words << w.downcase unless unique_words.include?(w.downcase) }
sorted = []
while unique_words.any?
min_word = unique_words.first
unique_words.each { |w| min_word = w if w < min_word }
sorted << min_word
unique_words.delete(min_word)
end
sorted
end
Common Pitfalls
Developers frequently violate KISS through specific mistakes.
Premature Optimization: Optimizing before measuring performance creates unnecessary complexity.
# Premature optimization
class DataCache
def initialize
@cache = {}
@access_counts = Hash.new(0)
@max_size = 1000
end
def get(key)
@access_counts[key] += 1
@cache[key]
end
def set(key, value)
if @cache.size >= @max_size
# Remove least accessed item
min_key = @access_counts.min_by { |k, v| v }.first
@cache.delete(min_key)
@access_counts.delete(min_key)
end
@cache[key] = value
end
end
# Simple solution sufficient for most cases
cache = {}
value = cache[key] ||= expensive_operation
The complex cache introduces LRU eviction and access tracking without evidence that simple hash lookup proves insufficient.
Abstraction for One Use Case: Creating abstractions for single use cases adds complexity without benefit.
# Unnecessary abstraction
class QueryBuilder
def initialize(table)
@table = table
@conditions = []
end
def where(condition)
@conditions << condition
self
end
def build
"SELECT * FROM #{@table} WHERE #{@conditions.join(' AND ')}"
end
end
query = QueryBuilder.new('users')
.where("age > 18")
.where("active = true")
.build
# Simple alternative when used once
query = "SELECT * FROM users WHERE age > 18 AND active = true"
Overusing Design Patterns: Applying design patterns where simpler solutions suffice.
# Pattern overuse
class UserFactory
def create_user(type)
case type
when :admin
AdminUser.new
when :regular
RegularUser.new
end
end
end
class AdminUser < User
def permissions
[:read, :write, :delete]
end
end
class RegularUser < User
def permissions
[:read]
end
end
factory = UserFactory.new
user = factory.create_user(:admin)
# Simple alternative
class User
attr_reader :role
def initialize(role)
@role = role
end
def permissions
case role
when :admin
[:read, :write, :delete]
when :regular
[:read]
end
end
end
user = User.new(:admin)
Speculative Generality: Building features for hypothetical future requirements.
# Speculative generality
class ReportGenerator
def generate(type, format, options = {})
data = fetch_data(type, options)
case format
when :pdf
generate_pdf(data)
when :html
generate_html(data)
when :json
generate_json(data)
when :xml
generate_xml(data)
when :csv
generate_csv(data)
end
end
# Multiple format methods implemented
# but only HTML actually used
end
# Simple solution for actual requirement
class ReportGenerator
def generate_html(type)
data = fetch_data(type)
generate_html(data)
end
end
Configuration Overload: Providing excessive configuration options increases complexity.
# Excessive configuration
class Logger
attr_accessor :level, :output, :format, :timestamp_format,
:colored_output, :log_rotation, :max_file_size,
:compression, :async_logging, :buffer_size
def initialize
@level = :info
@output = STDOUT
@format = :text
@timestamp_format = '%Y-%m-%d %H:%M:%S'
@colored_output = true
@log_rotation = :daily
@max_file_size = 10_485_760
@compression = false
@async_logging = false
@buffer_size = 1024
end
def log(message)
# Complex logic handling all configuration
end
end
# Simple logger with sensible defaults
class Logger
def initialize(output = STDOUT)
@output = output
end
def info(message)
@output.puts "[#{Time.now}] INFO: #{message}"
end
def error(message)
@output.puts "[#{Time.now}] ERROR: #{message}"
end
end
Deep Nesting: Multiple levels of nesting increase cognitive load.
# Deep nesting
def process_order(order)
if order.valid?
if order.customer.active?
if order.items.any?
if order.payment_method.present?
if order.payment_method.authorized?
if order.shipping_address.complete?
# Actually process order
order.process!
else
order.errors.add(:shipping, "incomplete address")
end
else
order.errors.add(:payment, "not authorized")
end
else
order.errors.add(:payment, "method missing")
end
else
order.errors.add(:items, "no items")
end
else
order.errors.add(:customer, "inactive")
end
else
order.errors.add(:base, "invalid order")
end
end
# Flat with early returns
def process_order(order)
return order.errors.add(:base, "invalid") unless order.valid?
return order.errors.add(:customer, "inactive") unless order.customer.active?
return order.errors.add(:items, "none") if order.items.empty?
return order.errors.add(:payment, "missing") unless order.payment_method.present?
return order.errors.add(:payment, "not authorized") unless order.payment_method.authorized?
return order.errors.add(:shipping, "incomplete") unless order.shipping_address.complete?
order.process!
end
Excessive Metaprogramming: Ruby's metaprogramming capabilities enable powerful abstractions but often sacrifice clarity.
# Excessive metaprogramming
class DynamicModel
def initialize(attributes = {})
attributes.each do |key, value|
self.class.send(:define_method, key) { instance_variable_get("@#{key}") }
self.class.send(:define_method, "#{key}=") { |val| instance_variable_set("@#{key}", val) }
send("#{key}=", value)
end
end
end
# Simple, clear alternative
class Model
attr_accessor :name, :email, :age
def initialize(name:, email:, age:)
@name = name
@email = email
@age = age
end
end
Reference
KISS Principle Quick Reference
| Aspect | Simple Approach | Complex Approach |
|---|---|---|
| Method Length | 5-15 lines focused on one task | 50+ lines doing multiple things |
| Class Responsibility | One clear purpose | Multiple unrelated responsibilities |
| Dependencies | 2-3 direct dependencies | 10+ coupled dependencies |
| Abstraction Layers | 1-2 layers | 5+ layers of indirection |
| Configuration | Sensible defaults, minimal options | Extensive configuration required |
| Conditionals | 1-3 levels of nesting | 5+ levels of nesting |
Complexity Indicators
| Indicator | Simple | Warning | Complex |
|---|---|---|---|
| Cyclomatic Complexity | 1-5 | 6-10 | 11+ |
| Lines Per Method | Under 15 | 15-30 | Over 30 |
| Method Parameters | 0-3 | 4-5 | 6+ |
| Class Dependencies | 0-3 | 4-6 | 7+ |
| Inheritance Depth | 1-2 | 3 | 4+ |
Decision Framework
| Question | Choose Simple | Choose Complex |
|---|---|---|
| Is performance measured and insufficient? | No | Yes |
| Will this code be reused in multiple places? | No | Yes, 3+ places |
| Do requirements change frequently? | Yes | No |
| Is the problem well-understood? | No | Yes |
| Is team size small? | Yes | No |
| Is this a prototype? | Yes | No |
Common Refactorings to Simplify
| Refactoring | Before | After |
|---|---|---|
| Extract Method | Long method | Several short methods |
| Inline Method | Unnecessary delegation | Direct call |
| Remove Dead Code | Unused code present | Only used code remains |
| Replace Conditional with Guard Clauses | Deep nesting | Early returns, flat structure |
| Replace Loop with Pipeline | Manual iteration | Built-in methods |
| Replace Temp with Query | Temporary variables | Direct method calls |
Ruby Simplicity Patterns
| Pattern | Description | Example Use |
|---|---|---|
| Built-in Methods | Use standard library | Array#sum instead of reduce |
| Blocks | Clean iteration | File.open with block |
| Duck Typing | No explicit interfaces | Any object with required methods |
| Convention | Follow established patterns | Rails naming conventions |
| Gems | Reuse existing solutions | Use http gem for requests |
| Early Returns | Reduce nesting | Guard clauses |
| Nil Safety | Safe navigation | object&.method |
Warning Signs of Unnecessary Complexity
| Sign | Description | Action |
|---|---|---|
| High Test Complexity | Tests harder to write than code | Simplify implementation |
| Difficult Naming | Cannot find clear method/class name | Rethink responsibility |
| Frequent Bugs | Same area breaks repeatedly | Reduce complexity |
| Slow Feature Addition | Changes require modifying many files | Reduce coupling |
| Documentation Needs | Requires extensive comments | Make code self-documenting |
| Reluctance to Change | Fear of modifying code | Simplify and add tests |