DEV Community

Davide Santangelo
Davide Santangelo

Posted on

Mastering Common and Advanced Ruby Design Patterns: A Comprehensive Guide with Code Examples

Ruby is a dynamic and flexible programming language known for its elegant syntax and powerful features. One of the strengths of Ruby is its ability to employ various programming patterns to solve problems effectively and elegantly. In this article, we will explore some of the most common and advanced patterns in Ruby, providing extensive code examples to illustrate how to use them.

What is a Pattern?

A "pattern" in computer science, particularly in the field of programming, is a general and reusable design solution for a common problem. This concept was initially introduced by the book "Design Patterns: Elements of Reusable Object-Oriented Software" written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, collectively known as the "Gang of Four" (GoF).

A design pattern is essentially a description or a template that can be applied to recurring situations in software design. These patterns offer a standardized approach to solving specific problems, helping programmers write more readable, maintainable, and efficient code. In other words, patterns represent best practices and accumulated experiences in the field of software design.

A design pattern typically includes:

  1. Pattern Name: The name that identifies the pattern, such as "Singleton" or "Factory."
  2. Problem: An explanation of the common problem that the pattern aims to address.
  3. Solution: A description of the general solution for addressing the problem, often illustrated with diagrams or pseudocode.
  4. Consequences: A list of the advantages and disadvantages associated with using the pattern.

Design patterns can be categorized into three main categories:

  1. Creational Patterns: These patterns focus on object creation and initialization. Examples include the Singleton, Factory, Abstract Factory, and Builder patterns.
  2. Structural Patterns: These patterns deal with object composition and relationships between them. Examples include the Decorator, Adapter, Composite, and Proxy patterns.
  3. Behavioral Patterns: These patterns concentrate on the behavior of objects and interactions between them. Examples include the Observer, Strategy, Command, and State patterns.

Using design patterns enables developers to write more robust and modular code, promoting code reuse and simplifying maintenance. However, it's important to note that design patterns are not always the best solution for every problem. The choice of the right pattern depends on the specific nature of the problem to be solved and the project's requirements.

Singleton Pattern

The Singleton pattern is a design pattern that restricts the instantiation of a class to one single instance. This ensures that there is only one instance of the class in the entire application. This pattern is useful when you want to control access to a shared resource, such as a database connection or a configuration manager.

class Singleton
  attr_accessor :data

  def self.instance
    @instance ||= new
  end

  private_class_method :new

  def initialize
    @data = []
  end
end

# Usage
singleton1 = Singleton.instance
singleton1.data << 'Item 1'

singleton2 = Singleton.instance
singleton2.data << 'Item 2'

puts singleton1.data  # Output: ["Item 1", "Item 2"]
puts singleton1 == singleton2  # Output: true
Enter fullscreen mode Exit fullscreen mode

In this example, the Singleton class ensures that only one instance is created, and any subsequent calls to instance return the same instance.

Factory Pattern

The Factory pattern is a creational design pattern that provides an interface for creating objects but allows subclasses to alter the type of objects that will be created. This pattern is useful when you want to abstract object creation and make it more flexible.

class AnimalFactory
  def create_animal(type)
    case type
    when 'dog'
      Dog.new
    when 'cat'
      Cat.new
    else
      raise "Unknown animal type: #{type}"
    end
  end
end

class Dog
  def speak
    'Woof!'
  end
end

class Cat
  def speak
    'Meow!'
  end
end

# Usage
factory = AnimalFactory.new
dog = factory.create_animal('dog')
cat = factory.create_animal('cat')

puts dog.speak  # Output: Woof!
puts cat.speak  # Output: Meow!
Enter fullscreen mode Exit fullscreen mode

In this example, the AnimalFactory class abstracts the creation of Dog and Cat objects, making it easy to add new animal types in the future.

Observer Pattern

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects, where one object (the subject) maintains a list of its dependents (observers) and notifies them of state changes. This pattern is useful for implementing event handling systems and decoupling components in an application.

class Subject
  attr_reader :observers, :state

  def initialize
    @observers = []
    @state = nil
  end

  def add_observer(observer)
    @observers << observer
  end

  def remove_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each { |observer| observer.update(self) }
  end

  def set_state(new_state)
    @state = new_state
    notify_observers
  end
end

class Observer
  def update(subject)
    puts "Observer received update with state: #{subject.state}"
  end
end

# Usage
subject = Subject.new
observer1 = Observer.new
observer2 = Observer.new

subject.add_observer(observer1)
subject.add_observer(observer2)

subject.set_state('New State')
Enter fullscreen mode Exit fullscreen mode

In this example, the Subject class maintains a list of observers and notifies them when its state changes. Observers, represented by the Observer class, can subscribe to the subject and react to changes.

Strategy Pattern

The Strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows you to select an algorithm at runtime without changing the client code. This pattern is useful when you want to switch between different algorithms or behaviors dynamically.

class PaymentContext
  attr_accessor :payment_strategy

  def initialize(payment_strategy)
    @payment_strategy = payment_strategy
  end

  def execute_payment(amount)
    @payment_strategy.pay(amount)
  end
end

class CreditCardPayment
  def pay(amount)
    puts "Paid $#{amount} using Credit Card"
  end
end

class PayPalPayment
  def pay(amount)
    puts "Paid $#{amount} using PayPal"
  end
end

# Usage
credit_card_payment = CreditCardPayment.new
paypal_payment = PayPalPayment.new

context1 = PaymentContext.new(credit_card_payment)
context1.execute_payment(100)

context2 = PaymentContext.new(paypal_payment)
context2.execute_payment(50)
Enter fullscreen mode Exit fullscreen mode

In this example, the PaymentContext class encapsulates payment strategies, and you can switch between different payment methods (e.g., credit card and PayPal) at runtime.

Decorator Pattern

The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It is useful for extending the functionality of classes without modifying their code.

class Coffee
  def cost
    5
  end

  def description
    'Coffee'
  end
end

class MilkDecorator
  def initialize(coffee)
    @coffee = coffee
  end

  def cost
    @coffee.cost + 2
  end

  def description
    @coffee.description + ', Milk'
  end
end

class SugarDecorator
  def initialize(coffee)
    @coffee = coffee
  end

  def cost
    @coffee.cost + 1
  end

  def description
    @coffee.description + ', Sugar'
  end
end

# Usage
coffee = Coffee.new
puts "Cost: $#{coffee.cost}, Description: #{coffee.description}"

coffee_with_milk = MilkDecorator.new(coffee)
puts "Cost: $#{coffee_with_milk.cost}, Description: #{coffee_with_milk.description}"

coffee_with_milk_and_sugar = SugarDecorator.new(coffee_with_milk)
puts "Cost: $#{coffee_with_milk_and_sugar.cost}, Description: #{coffee_with_milk_and_sugar.description}"
Enter fullscreen mode Exit fullscreen mode

In this example, we use decorators (MilkDecorator and SugarDecorator) to add additional functionality to a base object (Coffee) without modifying its code.

Command Pattern

The Command pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations. It is useful when you want to decouple senders from receivers, supporting undoable operations and queuing requests.

class Light
  def on
    puts 'Light is on'
  end

  def off
    puts 'Light is off'
  end
end

class Command
  def execute
    raise NotImplementedError, "#{self.class} has not implemented method 'execute'"
  end
end

class LightOnCommand < Command
  def initialize(light)
    @light = light
  end

  def execute
    @light.on
  end
end

class LightOffCommand < Command
  def initialize(light)
    @light = light
  end

  def execute
    @light.off
  end
end

class RemoteControl
  def initialize
    @commands = []
  end

  def add_command(command)
    @commands << command
  end

  def execute_commands
    @commands.each(&:execute)
  end
end

# Usage
light = Light.new
light_on = LightOnCommand.new(light)
light_off = LightOffCommand.new(light)

remote = RemoteControl.new
remote.add_command(light_on)
remote.add_command(light_off)

remote.execute_commands
Enter fullscreen mode Exit fullscreen mode

In this example, the Command pattern allows you to encapsulate requests (LightOnCommand and LightOffCommand) and execute them through a remote control (RemoteControl), providing flexibility and decoupling between senders and receivers.

Conclusion

Ruby's flexibility and object-oriented nature make it a great language for implementing various design patterns. In this article, we've explored some of the most common and advanced design patterns in Ruby, including the Singleton, Factory, Observer, Strategy, Decorator, and Command patterns. These patterns can help you write more maintainable, flexible, and efficient code by promoting best practices in object-oriented design.

By understanding and applying these patterns in your Ruby projects, you can improve the structure and maintainability of your code while leveraging the power of Ruby's elegant syntax and dynamic features. Design patterns are valuable tools in any programmer's toolbox, and mastering them can elevate your skills as a Ruby developer.

Top comments (0)