Community for developers to learn, share their programming knowledge. Register!
Design Patterns in Ruby

Structural Design Patterns in Ruby


If you're looking to deepen your understanding of software architecture, you can get valuable training from this article on Structural Design Patterns in Ruby. These patterns are crucial for developers aiming to create efficient, manageable, and scalable software applications. They help shape the relationships between objects and classes, ensuring that your code remains flexible and easy to maintain.

Understanding Structural Design Patterns

Structural design patterns are all about composing classes and objects in such a way that they form larger structures while keeping them flexible and efficient. Unlike creational patterns, which deal with object creation mechanisms, structural patterns focus on how classes and objects are composed to achieve desired functionalities.

In Ruby, a dynamic and object-oriented language, implementing these patterns can lead to elegant and expressive solutions. By leveraging Ruby's metaprogramming capabilities, developers can create highly dynamic applications while maintaining clear and understandable code.

Let's explore some of the most common structural design patterns and how they can be implemented in Ruby.

Adapter Pattern in Ruby

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible systems, enabling them to communicate without altering their existing code.

In Ruby, you can create an adapter by defining an interface and then implementing a class that conforms to that interface. Here's a simple example:

class OldPrinter
  def print_old_format(data)
    puts "Old Print: #{data}"
  end
end

class NewPrinter
  def print_new_format(data)
    puts "New Print: #{data}"
  end
end

class PrinterAdapter
  def initialize(old_printer)
    @old_printer = old_printer
  end

  def print(data)
    @old_printer.print_old_format(data)
  end
end

old_printer = OldPrinter.new
adapter = PrinterAdapter.new(old_printer)
adapter.print("Hello, World!")

In this example, the PrinterAdapter allows the OldPrinter to be used in a context where a different interface is expected, making it a powerful tool for maintaining backward compatibility.

Decorator Pattern Explained

The Decorator Pattern allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. This is particularly useful for adhering to the Single Responsibility Principle by allowing functionalities to be added in a flexible way.

Hereā€™s how you might implement the Decorator Pattern in Ruby:

class Coffee
  def cost
    5
  end
end

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

  def cost
    @coffee.cost + 1
  end
end

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

  def cost
    @coffee.cost + 0.5
  end
end

coffee = Coffee.new
puts "Cost of Coffee: #{coffee.cost}"

milk_coffee = MilkDecorator.new(coffee)
puts "Cost of Milk Coffee: #{milk_coffee.cost}"

sugar_milk_coffee = SugarDecorator.new(milk_coffee)
puts "Cost of Sugar Milk Coffee: #{sugar_milk_coffee.cost}"

In this example, decorators enhance the Coffee class with additional functionalities (milk and sugar) without modifying the original class. This pattern promotes code reusability and adheres to the Open/Closed Principle.

Facade Pattern Implementation

The Facade Pattern provides a simplified interface to a complex system of classes, libraries, or frameworks. This pattern is useful when you want to provide a higher-level interface that makes a system easier to use.

Here's an implementation of the Facade Pattern in Ruby:

class CPU
  def freeze; puts "CPU freezing..."; end
  def jump(position); puts "Jumping to #{position}."; end
  def execute; puts "Executing."; end
end

class Memory
  def load(position, data); puts "Loading #{data} at #{position}."; end
end

class HardDrive
  def read(position, size); puts "Reading #{size} bytes from #{position}."; end
end

class ComputerFacade
  def initialize
    @cpu = CPU.new
    @memory = Memory.new
    @hard_drive = HardDrive.new
  end

  def start_computer
    @cpu.freeze
    @memory.load(0, @hard_drive.read(0, 1024))
    @cpu.jump(0)
    @cpu.execute
  end
end

computer = ComputerFacade.new
computer.start_computer

In this example, the ComputerFacade class simplifies the process of starting a computer by encapsulating the interactions with the CPU, Memory, and HardDrive classes. This reduces complexity for the client code and enhances readability.

Proxy Pattern Use Cases

The Proxy Pattern involves creating a surrogate object that controls access to another object. It is useful for implementing lazy initialization, access control, logging, and more.

Here's a basic implementation of the Proxy Pattern in Ruby:

class RealImage
  def initialize(file_name)
    @file_name = file_name
    load_image_from_disk
  end

  def load_image_from_disk
    puts "Loading #{@file_name} from disk..."
  end

  def display
    puts "Displaying #{@file_name}."
  end
end

class ProxyImage
  def initialize(file_name)
    @file_name = file_name
    @real_image = nil
  end

  def display
    @real_image ||= RealImage.new(@file_name)
    @real_image.display
  end
end

image = ProxyImage.new("photo.jpg")
image.display

In this example, ProxyImage acts as a proxy for RealImage, delaying the loading of the image until it is actually needed. This can enhance performance, especially when dealing with large images.

Bridge Pattern Overview

The Bridge Pattern is designed to separate an abstraction from its implementation, allowing both to vary independently. This is particularly beneficial when you want to avoid a permanent binding between an abstraction and its implementation.

Hereā€™s a Ruby example illustrating the Bridge Pattern:

class RemoteControl
  def initialize(tv)
    @tv = tv
  end

  def turn_on
    @tv.turn_on
  end

  def turn_off
    @tv.turn_off
  end
end

class SonyTV
  def turn_on; puts "Sony TV is now ON."; end
  def turn_off; puts "Sony TV is now OFF."; end
end

class SamsungTV
  def turn_on; puts "Samsung TV is now ON."; end
  def turn_off; puts "Samsung TV is now OFF."; end
end

sony_tv = SonyTV.new
remote = RemoteControl.new(sony_tv)
remote.turn_on
remote.turn_off

In this example, the RemoteControl class can operate any TV type, allowing for flexibility in the implementation without needing to change the interface.

Composite Pattern in Ruby

The Composite Pattern allows you to compose objects into tree structures to represent part-whole hierarchies. This pattern lets clients treat individual objects and composites uniformly.

Here's how you can implement the Composite Pattern in Ruby:

class Component
  def operation
    raise NotImplementedError, 'You must implement the operation method'
  end
end

class Leaf < Component
  def operation
    puts "Leaf operation."
  end
end

class Composite < Component
  def initialize
    @children = []
  end

  def add(component)
    @children << component
  end

  def operation
    puts "Composite operation."
    @children.each(&:operation)
  end
end

leaf1 = Leaf.new
leaf2 = Leaf.new
composite = Composite.new
composite.add(leaf1)
composite.add(leaf2)

composite.operation

In this example, the Composite class contains Leaf objects, allowing for a unified interface to operate on both individual components and groups of components.

Summary

In conclusion, structural design patterns play a vital role in Ruby programming by promoting flexibility, scalability, and maintainability in software design. By understanding and applying patterns like Adapter, Decorator, Facade, Proxy, Bridge, and Composite, developers can create systems that are easier to understand and modify. The ability to compose objects and classes in various ways allows for cleaner architecture and encourages best practices in software development.

For more detailed discussions and implementations, you can refer to the Ruby documentation and various design pattern resources available online. Engaging with these concepts can significantly enhance your coding practices and architectural decisions in Ruby.

Last Update: 19 Jan, 2025

Topics:
Ruby