Quick Summary: Ruby design patterns create reusable and maintainable code. They help solve common problems faced in software development. Ruby design patterns focus on object-oriented programming principles, such as polymorphism, inheritance, Abstraction, and encapsulation. Design patterns in Ruby provide a way to structure code and make it more maintainable and reusable. Design patterns also provide a way to optimize code performance and reduce code complexity. By understanding and implementing design patterns, developers can create more efficient and easier-to-maintain applications.
You can’t just discover a pattern and copy this into your program like you can with ready-made functions or libraries. The pattern is not a single code but a general notion for dealing with a given situation. You can use the pattern details to design a solution appropriate to your software’s circumstances.
Patterns and algorithms are sometimes misconstrued since both represent common solutions to well-known situations. A pattern is a much more elevated solution description than an algorithm. The code for the same pattern may differ when applied to two separate programs. On the one hand, a pattern is like a blueprint in that you can see the consequences and features. However, the Implementation is entirely up to you.
So, what exactly are Ruby Design Patterns? Let’s have a peek.
What Are The Design Patterns in Ruby?
Design Patterns are popular solutions to challenges in software design. They are like pre-made layouts that you can change in code to tackle a reoccurring design challenge. You cannot discover a pattern and replicate it in your application like you can with functions or libraries. A design pattern in Ruby is a general notion for tackling a specific problem rather than a specific piece of code. You can follow the pattern specifics and implement a solution appropriate to your program’s circumstances.
What Does The Pattern Consist Of?
Ruby design patterns are very carefully documented so they can be reproduced in various scenarios. The following are among the components always contained in a ruby design pattern.
- Intent: The pattern’s intent briefly outlines the problem and possible remedies.
- Motivation: Motivation clarifies the issue and aids in discovering patterns that allow for a solution.
- Structure: of classes depicts each Component of the design and how well they are connected.
- Code Example: It is amongst the most frequently used techniques, which helps to understand the design simpler.
Why Should I Learn Ruby Design Patterns?
Even if you’ve been a developer for several years, probably, you don’t know about every single pattern. You can still use them even if you haven’t spent time learning them. So, why should you invest time studying Ruby design patterns?
Design patterns are a collection of tried-and-true answers to typical program design difficulties. Knowing patterns is useful even if you never experience these challenges since it informs you how to tackle all kinds of problems using object-oriented design concepts.
Ruby Design patterns provide a common language that your teammates and you may utilize to communicate more effectively. If you understand the pattern name, you don’t need to explain it. Now that you understand what Ruby design patterns are let’s look at the categories of design patterns in ruby.
Categories of Ruby Design Pattern
The intricacy, depth of detail, and applicable scalability to the overall built system differentiate design patterns. In 1994, four authors, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, wrote a book titled Design Patterns – Elements of Reusable Object-Oriented Software, which launched the Design Pattern concept in software development. This group of authors is known as the Gang of Four (GOF). According to these writers, design patterns are mostly based on the object-oriented design concepts listed below.
- Program to an interface, not an implementation
- Favor object composition over inheritance
The GoF design patterns are divided into three categories:
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
Creational Patterns
The fist category in ruby on rails design patterns is Creational Patterns. These patterns provide an object creation mechanism that increases flexibility and reuse of existing code.
How to identify Ruby Design Patterns?
There are 5 design patterns in the creational design patterns category. And there are techniques which you can use to identify this Ruby on Rails Design Pattern, which are as follows:
Pattern Name | Rating | Identification |
Abstract Factory | Complexity: Popularity: | Techniques that provide a factory object enable detecting the pattern simply. After that, the factory is used to manufacture sub-components. |
Builder | Complexity: Popularity: | A class illustrates the Builder pattern with only one method for creating objects but several methods for tweaking the product. |
Prototype | Complexity: Popularity: | Cloning and copying methods make it simple to recognize the Prototype. |
Factory Method | Complexity: Popularity: | Identifying “factory methods” for constructing objects from concrete classes is possible. Concrete classes are used for generating an object; however, the return type of factory methods is frequently defined as either an abstract class or an interface. |
Singleton | Complexity: Popularity: | Singletons can be identified using static creation techniques that produce comparable cached objects. |
Creational Design Pattern #1: Abstract Factory
Creating complete product families without identifying their concrete classes is a challenge that the creational design pattern known as Abstract Factory addresses. All different goods can be created using the interface defined, but the actual manufacturing of the products is left to the concrete factory classes. There is a certain product variety for each type of factory. Instead of producing goods by calling a constructor with the new operator, the client code instead invokes the creation methods of a factory object. All of the items produced by a factory will be compatible because each product variant corresponds to a certain factory.
Through their abstract interface, client code only interacts with factories and products. As a result, the client code can use every product created using a factory object. The client code is given a new concrete factory class that you created.
Why use Abstract Factory?
- The goods you purchase from a factory will be compatible so you can be certain of that.
- Be careful not to tightly couple client code with concrete items.
- Single-responsibility rule The code can be centralized and readily supported if the product generation code is extracted.
- You can offer new product variants using the open/closed paradigm without modifying the existing client code.
Code Example
# The Abstract Factory interface declares a set of methods that return different
# abstract products. These products are called a family and are related by a
# high-level theme or concept. Products of one family are usually able to
# collaborate among themselves. A family of products may have several variants,
# but the products of one variant are incompatible with products of another.
class AbstractFactory
# @abstract
def create_product_a
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
def create_product_b
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Factories produce a family of products that belong to a single
# variant. The factory guarantees that resulting products are compatible. Note
# that signatures of the Concrete Factory's methods return an abstract product,
# while inside the method a concrete product is instantiated.
class ConcreteFactory1 < AbstractFactory
def create_product_a
ConcreteProductA1.new
end
def create_product_b
ConcreteProductB1.new
end
end
# Each Concrete Factory has a corresponding product variant.
class ConcreteFactory2 < AbstractFactory
def create_product_a
ConcreteProductA2.new
end
def create_product_b
ConcreteProductB2.new
end
end
# Each distinct product of a product family should have a base interface. All
# variants of the product must implement this interface.
class AbstractProductA
# @abstract
#
# @return [String]
def useful_function_a
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Products are created by corresponding Concrete Factories.
class ConcreteProductA1 < AbstractProductA
def useful_function_a
'The result of the product A1.'
end
end
class ConcreteProductA2 < AbstractProductA
def useful_function_a
'The result of the product A2.'
end
end
# Here's the the base interface of another product. All products can interact
# with each other, but proper interaction is possible only between products of
# the same concrete variant.
class AbstractProductB
# Product B is able to do its own thing...
def useful_function_b
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# ...but it also can collaborate with the ProductA.
#
# The Abstract Factory makes sure that all products it creates are of the same
# variant and thus, compatible.
def another_useful_function_b(_collaborator)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Products are created by corresponding Concrete Factories.
class ConcreteProductB1 < AbstractProductB
# @return [String]
def useful_function_b
'The result of the product B1.'
end
# The variant, Product B1, is only able to work correctly with the variant,
# Product A1. Nevertheless, it accepts any instance of AbstractProductA as an
# argument.
def another_useful_function_b(collaborator)
result = collaborator.useful_function_a
"The result of the B1 collaborating with the (#{result})"
end
end
class ConcreteProductB2 < AbstractProductB
# @return [String]
def useful_function_b
'The result of the product B2.'
end
# The variant, Product B2, is only able to work correctly with the variant,
# Product A2. Nevertheless, it accepts any instance of AbstractProductA as an
# argument.
def another_useful_function_b(collaborator)
result = collaborator.useful_function_a
"The result of the B2 collaborating with the (#{result})"
end
end
# The client code works with factories and products only through abstract types:
# AbstractFactory and AbstractProduct. This lets you pass any factory or product
# subclass to the client code without breaking it.
def client_code(factory)
product_a = factory.create_product_a
product_b = factory.create_product_b
puts product_b.useful_function_b.to_s
puts product_b.another_useful_function_b(product_a).to_s
end
# The client code can work with any concrete factory class.
puts 'Client: Testing client code with the first factory type:'
client_code(ConcreteFactory1.new)
puts "\n"
puts 'Client: Testing the same client code with the second factory type:'
client_code(ConcreteFactory2.new)
Creational Design Pattern #2: Builder Pattern
A creational design pattern called Builder enables the gradual development of complicated items. Unlike other creational designs, the Builder does not demand that products have a common interface. Because of this, different products can be produced using the same construction method.
Following the builder pattern, you should relocate the object building code from an object’s class into independent objects referred to as builders. The patterns break down the process of building an object into a series of steps. These operations are carried out on a builder object to generate an object. Not all steps need to be called, which is an essential point. You are limited to calling the steps required to produce a specific configuration of an object. Some construction processes may require distinct implementations when building alternative representations of the product.
Why Use Builder Pattern?
- Construction phases can be postponed or repeated recursively when developing things step-by-step.
- Build numerous product representations using the same construction code.
- Solitary-Responsibility Rule From The Product’s Business Logic, You Can Separate complicated building code.
Code Example
# The Builder interface specifies methods for creating the different parts of
# the Product objects.
class Builder
# @abstract
def produce_part_a
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
def produce_part_b
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
def produce_part_c
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# The Concrete Builder classes follow the Builder interface and provide specific
# implementations of the building steps. Your program may have several
# variations of Builders, implemented differently.
class ConcreteBuilder1 < Builder
# A fresh builder instance should contain a blank product object, which is
# used in further assembly.
def initialize
reset
end
def reset
@product = Product1.new
end
# Concrete Builders are supposed to provide their own methods for retrieving
# results. That's because various types of builders may create entirely
# different products that don't follow the same interface. Therefore, such
# methods cannot be declared in the base Builder interface (at least in a
# statically typed programming language).
#
# Usually, after returning the end result to the client, a builder instance is
# expected to be ready to start producing another product. That's why it's a
# usual practice to call the reset method at the end of the `getProduct`
# method body. However, this behavior is not mandatory, and you can make your
# builders wait for an explicit reset call from the client code before
# disposing of the previous result.
def product
product = @product
reset
product
end
def produce_part_a
@product.add('PartA1')
end
def produce_part_b
@product.add('PartB1')
end
def produce_part_c
@product.add('PartC1')
end
end
# It makes sense to use the Builder pattern only when your products are quite
# complex and require extensive configuration.
#
# Unlike in other creational patterns, different concrete builders can produce
# unrelated products. In other words, results of various builders may not always
# follow the same interface.
class Product1
def initialize
@parts = []
end
# @param [String] part
def add(part)
@parts << part
end
def list_parts
print "Product parts: #{@parts.join(', ')}"
end
end
# The Director is only responsible for executing the building steps in a
# particular sequence. It is helpful when producing products according to a
# specific order or configuration. Strictly speaking, the Director class is
# optional, since the client can control builders directly.
class Director
# @return [Builder]
attr_accessor :builder
def initialize
@builder = nil
end
# The Director works with any builder instance that the client code passes to
# it. This way, the client code may alter the final type of the newly
# assembled product.
def builder=(builder)
@builder = builder
end
# The Director can construct several product variations using the same
# building steps.
def build_minimal_viable_product
@builder.produce_part_a
end
def build_full_featured_product
@builder.produce_part_a
@builder.produce_part_b
@builder.produce_part_c
end
end
# The client code creates a builder object, passes it to the director and then
# initiates the construction process. The end result is retrieved from the
# builder object.
director = Director.new
builder = ConcreteBuilder1.new
director.builder = builder
puts 'Standard basic product: '
director.build_minimal_viable_product
builder.product.list_parts
puts "\n\n"
puts 'Standard full featured product: '
director.build_full_featured_product
builder.product.list_parts
puts "\n\n"
# Remember, the Builder pattern can be used without a Director class.
puts 'Custom product: '
builder.produce_part_a
builder.produce_part_b
builder.product.list_parts
Creational Design Pattern #3: Factory Method
The factory method, also known as the Virtual Constructor, is a creational design pattern that solves the problem of creating product objects without specifying their concrete classes. The Factory Method defines a method that should be used to create objects instead of a direct constructor call (new operator). Subclasses can override this method to change the class of objects that will be created.
The factory method pattern suggests that you replace direct object construction calls with a special factory method. The objects are still created via the new operator, but it’s being called from within the factory method. Objects returned by a factory method are often referred to as products.
Why use the factory method?
- By doing this, you prevent a close relationship between both the Creator and the tangible output.
- Single-responsibility rule To make the program’s code easier to support, you can consolidate the code for product creation in one location.
- The Open/Closed Principle Without affecting the current client code, you can add new product categories to the program.
Code Example
# The Creator class declares the factory method that is supposed to return an
# object of a Product class. The Creator's subclasses usually provide the
# implementation of this method.
class Creator
# Note that the Creator may also provide some default implementation of the
# factory method.
def factory_method
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# Also note that, despite its name, the Creator's primary responsibility is
# not creating products. Usually, it contains some core business logic that
# relies on Product objects, returned by the factory method. Subclasses can
# indirectly change that business logic by overriding the factory method and
# returning a different type of product from it.
def some_operation
# Call the factory method to create a Product object.
product = factory_method
# Now, use the product.
result = "Creator: The same creator's code has just worked with #{product.operation}"
result
end
end
# Concrete Creators override the factory method in order to change the resulting
# product's type.
class ConcreteCreator1 < Creator
# Note that the signature of the method still uses the abstract product type,
# even though the concrete product is actually returned from the method. This
# way the Creator can stay independent of concrete product classes.
def factory_method
ConcreteProduct1.new
end
end
class ConcreteCreator2 < Creator
# @return [ConcreteProduct2]
def factory_method
ConcreteProduct2.new
end
end
# The Product interface declares the operations that all concrete products must
# implement.
class Product
# return [String]
def operation
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Products provide various implementations of the Product interface.
class ConcreteProduct1 < Product
# @return [String]
def operation
'{Result of the ConcreteProduct1}'
end
end
class ConcreteProduct2 < Product
# @return [String]
def operation
'{Result of the ConcreteProduct2}'
end
end
# The client code works with an instance of a concrete creator, albeit through
# its base interface. As long as the client keeps working with the creator via
# the base interface, you can pass it any creator's subclass.
def client_code(creator)
print "Client: I'm not aware of the creator's class, but it still works.\n"\
"#{creator.some_operation}"
end
puts 'App: Launched with the ConcreteCreator1.'
client_code(ConcreteCreator1.new)
puts "\n\n"
puts 'App: Launched with the ConcreteCreator2.'
client_code(ConcreteCreator2.new)
Creational Design Pattern #4: Prototype
The Prototype, commonly called the Clone design pattern, is a creational pattern that enables you to duplicate existing objects without having your code depend on their classes. The actual items being duplicated are given control over the cloning process.
For all objects that support cloning, the pattern declares a common interface. This interface can copy an object without linking your program to the object’s class. Typically, such an interface has one clone method.
In all classes, the clone method is implemented relatively similarly. The method produces a new object of the current class while importing all field values from the old object. Since private fields of other objects that are members of the same class can typically be accessed by objects, you can even clone private fields in most programming languages.
Prototypes are items that allow for cloning. Clone your objects instead of subclassing them if they have dozens of fields and hundreds of possible configurations.
Why use Prototype?
- Objects can be copied apart from the concrete classes that make them up.
- Cloning pre-built prototypes allow you to do away with repetitive initialization code.
- You have more ease in producing complex objects.
- When working with configuration settings for complicated objects, inheritance is a substitute you can use.
Code Example
# The example class that has cloning ability. We'll see how the values of field
# with different types will be cloned.
class Prototype
attr_accessor :primitive, :component, :circular_reference
def initialize
@primitive = nil
@component = nil
@circular_reference = nil
end
# @return [Prototype]
def clone
@component = deep_copy(@component)
# Cloning an object that has a nested object with backreference requires
# special treatment. After the cloning is completed, the nested object
# should point to the cloned object, instead of the original object.
@circular_reference = deep_copy(@circular_reference)
@circular_reference.prototype = self
deep_copy(self)
end
# deep_copy is the usual Marshalling hack to make a deep copy. But it's rather
# slow and inefficient, therefore, in real applications, use a special gem
private def deep_copy(object)
Marshal.load(Marshal.dump(object))
end
end
class ComponentWithBackReference
attr_accessor :prototype
# @param [Prototype] prototype
def initialize(prototype)
@prototype = prototype
end
end
# The client code.
p1 = Prototype.new
p1.primitive = 245
p1.component = Time.now
p1.circular_reference = ComponentWithBackReference.new(p1)
p2 = p1.clone
if p1.primitive == p2.primitive
puts 'Primitive field values have been carried over to a clone. Yay!'
else
puts 'Primitive field values have not been copied. Booo!'
end
if p1.component.equal?(p2.component)
puts 'Simple component has not been cloned. Booo!'
else
puts 'Simple component has been cloned. Yay!'
end
if p1.circular_reference.equal?(p2.circular_reference)
puts 'Component with back reference has not been cloned. Booo!'
else
puts 'Component with back reference has been cloned. Yay!'
end
if p1.circular_reference.prototype.equal?(p2.circular_reference.prototype)
print 'Component with back reference is linked to original object. Booo!'
else
print 'Component with back reference is linked to the clone. Yay!'
end
Creational Design Pattern #5: Singleton
Singleton is a creational design pattern that ensures that only one object of its kind exists & provides a single point of access to it for any other code. Singleton has almost the same pros n cons are global variables. However, they break the modularity of your code.
Why use Singleton?
- You can be certain that there is just one instance of a class.
- In that instance, you obtain a global access point.
- Only when it is first requested is the singleton object initialized.
It is divided into Naïve Singleton & Thread-safe Singleton
Naïve Singleton
It’s pretty easy to implement a sloppy Singleton. You need to hide the constructor and implement a static creation method. The same class behaves incorrectly in a multithreaded environment. Multiple threads can call the creation method simultaneously and get several instances of the Singleton class. You can understand the concept better with the example below:
# The Singleton class defines the `instance` method that lets clients access the
# unique singleton instance.
class Singleton
@instance = new
private_class_method :new
# The static method that controls the access to the singleton instance.
#
# This implementation let you subclass the Singleton class while keeping just
# one instance of each subclass around.
def self.instance
@instance
end
# Finally, any singleton should define some business logic, which can be
# executed on its instance.
def some_business_logic
# ...
end
end
# The client code.
s1 = Singleton.instance
s2 = Singleton.instance
if s1.equal?(s2)
print 'Singleton works, both variables contain the same instance.'
else
print 'Singleton failed, variables contain different instances.'
end
Thread-safe Singleton
To fix the problem, you have to synchronize threads during the first creation of the Singleton object. You can understand this concept much better with the following example:
# The Singleton class defines the `intance` method that lets clients access the
# unique singleton instance.
class Singleton
attr_reader :value
@instance_mutex = Mutex.new
private_class_method :new
def initialize(value)
@value = value
end
# The static method that controls the access to the singleton instance.
#
# This implementation let you subclass the Singleton class while keeping just
# one instance of each subclass around.
def self.instance(value)
return @instance if @instance
@instance_mutex.synchronize do
@instance ||= new(value)
end
@instance
end
# Finally, any singleton should define some business logic, which can be
# executed on its instance.
def some_business_logic
# ...
end
end
# @param [String] value
def test_singleton(value)
singleton = Singleton.instance(value)
puts singleton.value
end
# The client code.
puts "If you see the same value, then singleton was reused (yay!)\n"\
"If you see different values, then 2 singletons were created (booo!!)\n\n"\
"RESULT:\n\n"
process1 = Thread.new { test_singleton('FOO') }
process2 = Thread.new { test_singleton('BAR') }
process1.join
process2.join
Behavioral Patterns
Next on the list of design patterns in ruby on rails is Behavioural patterns. These Patterns handle good communication and responsibility assignment among objects.
How to identify Ruby Design Patterns?
There are 11 patterns in the category of behavioural design patterns. There are methods for recognising this Ruby on Rails Design Pattern, including the ones listed below:
Pattern Name | Rating | Identification |
Chain of Responsibility | Complexity: Popularity: | The pattern is recognized by behavioral methods of one set of entities that indirectly call the same techniques in other objects, although all the objects adhere to the same interface. |
Command | Complexity: Popularity: | The command pattern is recognized by behavioral methods in a sender that invoke a method in a distinct receiver implementation wrapped by the commanding Component during its development. Command classes are often restricted to specific actions. |
Mediator | Complexity: Popularity: | A behavioral design technique called mediator allows you to lessen unpredictable dependencies between things. The patterns prevent the objects from collaborating directly with one another and force them to do so only through a mediator object. |
Observer | Complexity: Popularity: | The pattern can be identified by subscription methods, which keep items in a list, and calls made to the update method on objects contained in that list. |
Iterator | Complexity: Popularity: | The methods of navigation make iterators simple to identify. It is possible that the collection being browsed is not directly accessible to client code that uses iterators. |
Template method | Complexity: Popularity: | If a base class method calls many other conceptual or empty procedures, that method is likely a template method. |
Visitor | Complexity: Popularity: | With the help of this behavioral design pattern, you can keep algorithms separate from the objects they interact with in Ruby. |
Strategy | Complexity: Popularity: | A method that delegated the actual work to a nested object and a setter that allowed for substituting that object with another one is an indicator of the strategy pattern. |
State | Complexity: Popularity: | By using methods that alter their behavior in response to an object’s externally controlled state, state patterns can be identified. |
Behavioral Patterns #1:Chain of Responsibility
A behavioral design pattern called Chain of Responsibility, often called Chain of Command or CoR enables you to transfer requests along a chain of handlers. Each Handler chooses whether to execute a request or pass it on to the Handler behind it in the chain. The approach enables several objects to respond to requests without the sender class becoming dependent on the receivers’ concrete classes. Any handler that adheres to a common handler protocol can dynamically assemble the chain at runtime. In Ruby, the concept of a chain of responsibility is rather frequent; it mainly applies when your code uses object chains, such as event chains, filters, and others.
It depends on encoding the specific behavior in so-called handlers, which are stand-alone objects. According to this pattern, you should link these handlers together in a chain, with a field on each linked Handler that serves as a reference to the Handler after it. The handlers also send the request to subsequent links in the chain. Until each Handler has had an opportunity to respond to the request, it continues to move up the chain.
Why use chain responsibility?
- The processing of requests can happen in whatever sequence you choose.
- Single-responsibility rule Classes that execute operations can be separated from those that invoke them.
- The Open/Closed Principle Without affecting the current client code, you can add additional handlers to the app.
Code Example
# The Handler interface declares a method for building the chain of handlers. It
# also declares a method for executing a request.
class Handler
# @abstract
#
# @param [Handler] handler
def next_handler=(handler)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
#
# @param [String] request
#
# @return [String, nil]
def handle(request)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# The default chaining behavior can be implemented inside a base handler class.
class AbstractHandler < Handler
# @return [Handler]
attr_writer :next_handler
# @param [Handler] handler
#
# @return [Handler]
def next_handler(handler)
@next_handler = handler
# Returning a handler from here will let us link handlers in a convenient
# way like this:
# monkey.next_handler(squirrel).next_handler(dog)
handler
end
# @abstract
#
# @param [String] request
#
# @return [String, nil]
def handle(request)
return @next_handler.handle(request) if @next_handler
nil
end
end
# All Concrete Handlers either handle a request or pass it to the next handler
# in the chain.
class MonkeyHandler < AbstractHandler
# @param [String] request
#
# @return [String, nil]
def handle(request)
if request == 'Banana'
"Monkey: I'll eat the #{request}"
else
super(request)
end
end
end
class SquirrelHandler < AbstractHandler
# @param [String] request
#
# @return [String, nil]
def handle(request)
if request == 'Nut'
"Squirrel: I'll eat the #{request}"
else
super(request)
end
end
end
class DogHandler < AbstractHandler # @param [String] request # # @return [String, nil] def handle(request) if request == 'MeatBall' "Dog: I'll eat the #{request}" else super(request) end end end # The client code is usually suited to work with a single handler. In most # cases, it is not even aware that the handler is part of a chain. def client_code(handler) ['Nut', 'Banana', 'Cup of coffee'].each do |food| puts "\nClient: Who wants a #{food}?" result = handler.handle(food) if result print " #{result}" else print " #{food} was left untouched." end end end monkey = MonkeyHandler.new squirrel = SquirrelHandler.new dog = DogHandler.new monkey.next_handler(squirrel).next_handler(dog) # The client should be able to send a request to any handler, not just the first # one in the chain. puts 'Chain: Monkey > Squirrel > Dog'
client_code(monkey)
puts "\n\n"
puts 'Subchain: Squirrel > Dog'
client_code(squirrel)
Behavioral Patterns #2: Iterator
Iterator is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation, including stack, tree, and more. It is a behavioral pattern that allows sequential traversal through a complex data structure without exposing its internal details.
Thanks to the Iterator, clients can go over elements of different collections similarly using a single iterator interface. The main idea of the iterator pattern is to extract the traversal behavior of the collection into a separate object called an iterator. In addition to implementing the algorithm, an iterator object encapsulates all of the traversal details, such as the current position and how many elements are left behind.
Why use iterators?
- The concept of a single responsibility. Bulky traversal algorithms might be separated into different classes to simplify the client code and collections.
- Closed/Open Principle. New collections and iterators can be added and passed to existing code without causing any issues.
- Since each iterator object has its iteration state, you can do parallel iterations over the same collection.
- You can postpone an iteration and resume it later for the same reason.
Code Example
class AlphabeticalOrderIterator
# In Ruby, the Enumerable mixin provides classes with several traversal and
# searching methods, and with the ability to sort. The class must provide a
# method each, which yields successive members of the collection.
include Enumerable
# This attribute indicates the traversal direction.
attr_accessor :reverse
private :reverse
# @return [Array]
attr_accessor :collection
private :collection
# @param [Array] collection
# @param [Boolean] reverse
def initialize(collection, reverse = false)
@collection = collection
@reverse = reverse
end
def each(&block)
return @collection.reverse.each(&block) if reverse
@collection.each(&block)
end
end
class WordsCollection
# @return [Array]
attr_accessor :collection
private :collection
def initialize(collection = [])
@collection = collection
end
# The `iterator` method returns the iterator object itself, by default we
# return the iterator in ascending order.
def iterator
AlphabeticalOrderIterator.new(@collection)
end
# @return [AlphabeticalOrderIterator]
def reverse_iterator
AlphabeticalOrderIterator.new(@collection, true)
end
# @param [String] item
def add_item(item)
@collection << item
end
end
# The client code may or may not know about the Concrete Iterator or Collection
# classes, depending on the level of indirection you want to keep in your
# program.
collection = WordsCollection.new
collection.add_item('First')
collection.add_item('Second')
collection.add_item('Third')
puts 'Straight traversal:'
collection.iterator.each { |item| puts item }
puts "\n"
puts 'Reverse traversal:'
collection.reverse_iterator.each { |item| puts item }
Behavioral Patterns #3: Observer
The observer behavioral pattern, also known as an event-subscriber or listener, enables you to build a subscription mechanism to alert numerous objects to any occurrences that affect the object you’re watching. Thanks to the observer pattern, any object that implements a subscriber interface has a mechanism to subscribe and unsubscribe to and from these events. The interesting item is frequently referred to as the Subject, but because it informs other objects of changes to its state, it is also known as the publisher. The term “subscriber” refers to any objects that desire to follow changes to the publisher’s state.
Why use Observer?
- The Open/Closed Principle If there is a publisher interface, adding additional subscriber classes can be done without altering the code for the publisher.
- Relationships between objects can be established during runtime.
Code Example
# The Subject interface declares a set of methods for managing subscribers.
class Subject
# Attach an observer to the subject.
def attach(observer)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# Detach an observer from the subject.
def detach(observer)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# Notify all observers about an event.
def notify
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# The Subject owns some important state and notifies observers when the state
# changes.
class ConcreteSubject < Subject
# For the sake of simplicity, the Subject's state, essential to all
# subscribers, is stored in this variable.
attr_accessor :state
# @!attribute observers
# @return [Array] attr_accessor :observers private :observers
def initialize
@observers = []
end
# List of subscribers. In real life, the list of subscribers can be stored
# more comprehensively (categorized by event type, etc.).
# @param [Obserser] observer
def attach(observer)
puts 'Subject: Attached an observer.'
@observers << observer
end
# @param [Obserser] observer
def detach(observer)
@observers.delete(observer)
end
# The subscription management methods.
# Trigger an update in each subscriber.
def notify
puts 'Subject: Notifying observers...'
@observers.each { |observer| observer.update(self) }
end
# Usually, the subscription logic is only a fraction of what a Subject can
# really do. Subjects commonly hold some important business logic, that
# triggers a notification method whenever something important is about to
# happen (or after it).
def some_business_logic
puts "\nSubject: I'm doing something important."
@state = rand(0..10)
puts "Subject: My state has just changed to: #{@state}"
notify
end
end
# The Observer interface declares the update method, used by subjects.
class Observer
# Receive update from subject.
def update(_subject)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Observers react to the updates issued by the Subject they had been
# attached to.
class ConcreteObserverA < Observer
# @param [Subject] subject
def update(subject)
puts 'ConcreteObserverA: Reacted to the event' if subject.state < 3
end
end
class ConcreteObserverB < Observer # @param [Subject] subject def update(subject) return unless subject.state.zero? || subject.state >= 2
puts 'ConcreteObserverB: Reacted to the event'
end
end
# The client code.
subject = ConcreteSubject.new
observer_a = ConcreteObserverA.new
subject.attach(observer_a)
observer_b = ConcreteObserverB.new
subject.attach(observer_b)
subject.some_business_logic
subject.some_business_logic
subject.detach(observer_a)
subject.some_business_logic
Behavioral Patterns #4: Strategy
Strategy is a behavioral design pattern that lets you define a family of algorithms, put each into a separate class, and make their objects interchangeable. This behavioral pattern turns a set of behaviors into objects and makes them interchangeable inside the original context object. The original object, called context, references a strategy object. The context delegates executing the behavior to the linked strategy object. To change how the context performs its work, other objects may replace the currently linked strategy object with another one.
Why use strategy?
- Algorithms inside of an object can be changed during runtime.
- It is possible to separate an algorithm’s implementation specifics from the code that employs them.
- It is possible to use composition in place of inheritance.
- The Open/Closed Principle There is no need to alter the setting to introduce new tactics.
Code Example
# The Context defines the interface of interest to clients.
class Context
# The Context maintains a reference to one of the Strategy objects. The
# Context does not know the concrete class of a strategy. It should work with
# all strategies via the Strategy interface.
attr_writer :strategy
# Usually, the Context accepts a strategy through the constructor, but also
# provides a setter to change it at runtime.
def initialize(strategy)
@strategy = strategy
end
# Usually, the Context allows replacing a Strategy object at runtime.
def strategy=(strategy)
@strategy = strategy
end
# The Context delegates some work to the Strategy object instead of
# implementing multiple versions of the algorithm on its own.
def do_some_business_logic
# ...
puts 'Context: Sorting data using the strategy (not sure how it\'ll do it)'
result = @strategy.do_algorithm(%w[a b c d e])
print result.join(',')
# ...
end
end
# The Strategy interface declares operations common to all supported versions of
# some algorithm.
#
# The Context uses this interface to call the algorithm defined by Concrete
# Strategies.
class Strategy
# @abstract
#
# @param [Array] data
def do_algorithm(_data)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Strategies implement the algorithm while following the base Strategy
# interface. The interface makes them interchangeable in the Context.
class ConcreteStrategyA < Strategy
# @param [Array] data
#
# @return [Array]
def do_algorithm(data)
data.sort
end
end
class ConcreteStrategyB < Strategy
# @param [Array] data
#
# @return [Array]
def do_algorithm(data)
data.sort.reverse
end
end
# The client code picks a concrete strategy and passes it to the context. The
# client should be aware of the differences between strategies in order to make
# the right choice.
context = Context.new(ConcreteStrategyA.new)
puts 'Client: Strategy is set to normal sorting.'
context.do_some_business_logic
puts "\n\n"
puts 'Client: Strategy is set to reverse sorting.'
context.strategy = ConcreteStrategyB.new
context.do_some_business_logic
This is just a scratch on the surface of behavioral patterns. There are other ruby design patterns available. Some of them are more useful; some are less. Any design pattern should be used with caution. When using them not correctly, we can harm our architecture and over complicate the code, which leads to longer development time and higher technical debt.
Structural Pattern
Last on the list of Ruby Design pattern is Structural Design patterns. These design pattern helps in maintaining these structures adaptable and effective, it also describe how to combine items and classes into a bigger structure.
How to identify Ruby Design Patterns?
The structural design patterns category includes 7 patterns. Additionally, there are methods you may employ to spot this Ruby on Rails Design Pattern, including the ones listed below:
Pattern Name | Rating | Identification |
Adapter | Complexity: Popularity: | An instance of a separate abstract or interface type is required by the Adapter’s constructor, which identifies it. When an adapter receives a call to one of its methods, it converts the argument values to the proper format before routing the call to one or more methods of the wrapped objects. |
Bridge | Complexity: Popularity: | A marked separation between the governing entity and the various platforms it depends on identifies the bridge. |
Composite | Complexity: Popularity: | This is certainly a composite if there is an object tree and each object belongs to the same class hierarchy. This is a composite of these classes’ methods that pass work down the hierarchy’s base class or interface to the tree’s descendant objects. |
Façade | Complexity: Popularity: | A class that delegates most of its work to other classes while maintaining a straightforward interface is known as a facade. Most of the time, the façade controls the entire life cycle of the things they utilize. |
Decorator | Complexity: Popularity: | By accepting objects of the same class or interface as the current class, decorators can be identified by their creation methods or constructors. |
Proxy | Complexity: Popularity: | If the proxy is not a service subclass, all real work is delegated to another project function that ultimately should relate to a service object. |
Flyweight | Complexity: Popularity: | When a creation method returns already cached objects rather than producing new ones, it is known to be a flyweight. |
In Search to Hire Ruby on Rails developeR?
Hire Ruby on Rails developers from Aglowid to build quality-rich RoR solutions
Structural Patterns #1: Adapter
The adaptor sometimes referred to as Wrapper, is a structural design pattern that enables the cooperation of incompatible objects. You can design an adaptor, a unique object that changes one object’s interface so that another can comprehend it.
An adapter covers one of the objects to conceal the intricate conversion process behind the scenes. The wrapper object does not know the adaptor at all. An adaptor that translates all the data to imperial units like feet and miles can encase an object that functions in meters and kilometers, for instance.
The adaptor can assist items with distinct interfaces in cooperating and converting data into various formats. Here’s how it functions.
- The Adapter gets an interface compatible with one of the existing objects.
- Using this interface, the existing object can safely call the Adapter’s method.
- Upon receiving a call, the Adapter passes the request to the second object but in a format & other than what the second object expects.
Why use an Adapter?
- Single-responsibility rule The program’s main business logic can be separated from the code for the interface or data conversion.
- The Open/Closed Principle If you use the client interface to interact with the adapters, you can add new types of adapters to the application without affecting the code that already runs the clients.
Also Read : React With Ruby on Rails
Code Example
# The Target defines the domain-specific interface used by the client code.
class Target
# @return [String]
def request
'Target: The default target\'s behavior.'
end
end
# The Adaptee contains some useful behavior, but its interface is incompatible
# with the existing client code. The Adaptee needs some adaptation before the
# client code can use it.
class Adaptee
# @return [String]
def specific_request
'.eetpadA eht fo roivaheb laicepS'
end
end
# The Adapter makes the Adaptee's interface compatible with the Target's
# interface.
class Adapter < Target
# @param [Adaptee] adaptee
def initialize(adaptee)
@adaptee = adaptee
end
def request
"Adapter: (TRANSLATED) #{@adaptee.specific_request.reverse!}"
end
end
# The client code supports all classes that follow the Target interface.
def client_code(target)
print target.request
end
puts 'Client: I can work just fine with the Target objects:'
target = Target.new
client_code(target)
puts "\n\n"
adaptee = Adaptee.new
puts 'Client: The Adaptee class has a weird interface. See, I don\'t understand it:'
puts "Adaptee: #{adaptee.specific_request}"
puts "\n"
puts 'Client: But I can work with it via the Adapter:'
adapter = Adapter.new(adaptee)
client_code(adapter)
Structural Patterns #2: Bridge
A structural design pattern called the bridge splits a large class or body of business logic into discrete class hierarchies that can be created separately. It enables the separation of a large class or group of related classes into two distinct hierarchies, Abstraction and Implementation, which can be developed separately from one another.
Abstraction is a high-level control layer for something and is also referred to as an interface. This layer does not intend to perform any actual work by itself. The implementation layer should be assigned the task (also called the platform). You should know that we are not discussing interfaces or abstract classes from your programming language. The two are not equivalent.
Why use Bridge Pattern?
- You can develop classes and applications that are platform-independent.
- The high-level abstractions used by the client programs are. The platform specifics aren’t revealed to it.
- The Open/Closed Principle New implementations and abstractions may be introduced without relation to one another.
- Single-responsibility rule When implementing, you can concentrate on platform specifics, while Abstraction focuses on high-level logic.
Code Example
# The Abstraction defines the interface for the "control" part of the two class
# hierarchies. It maintains a reference to an object of the Implementation
# hierarchy and delegates all of the real work to this object.
class Abstraction
# @param [Implementation] implementation
def initialize(implementation)
@implementation = implementation
end
# @return [String]
def operation
"Abstraction: Base operation with:\n"\
"#{@implementation.operation_implementation}"
end
end
# You can extend the Abstraction without changing the Implementation classes.
class ExtendedAbstraction < Abstraction
# @return [String]
def operation
"ExtendedAbstraction: Extended operation with:\n"\
"#{@implementation.operation_implementation}"
end
end
# The Implementation defines the interface for all implementation classes. It
# doesn't have to match the Abstraction's interface. In fact, the two interfaces
# can be entirely different. Typically the Implementation interface provides
# only primitive operations, while the Abstraction defines higher-level
# operations based on those primitives.
class Implementation
# @abstract
#
# @return [String]
def operation_implementation
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Each Concrete Implementation corresponds to a specific platform and implements
# the Implementation interface using that platform's API.
class ConcreteImplementationA < Implementation
# @return [String]
def operation_implementation
'ConcreteImplementationA: Here\'s the result on the platform A.'
end
end
class ConcreteImplementationB < Implementation
# @return [String]
def operation_implementation
'ConcreteImplementationB: Here\'s the result on the platform B.'
end
end
# Except for the initialization phase, where an Abstraction object gets linked
# with a specific Implementation object, the client code should only depend on
# the Abstraction class. This way the client code can support any abstraction-
# implementation combination.
def client_code(abstraction)
# ...
print abstraction.operation
# ...
end
# The client code should be able to work with any pre-configured abstraction-
# implementation combination.
implementation = ConcreteImplementationA.new
abstraction = Abstraction.new(implementation)
client_code(abstraction)
puts "\n\n"
implementation = ConcreteImplementationB.new
abstraction = ExtendedAbstraction.new(implementation)
client_code(abstraction)
Structural Patterns #3: Composite
A structural design pattern called Composite, called Object Tree enables you to assemble items into tree structures and then treat those hierarchies as when they were individual objects.
Using the composite structural design pattern, things can be combined into a tree-like structure and used as if they were a single entity. It became a fairly well-liked mechanism for running procedures.
Why use Composite?
- You may work with complex tree structures more conveniently by utilizing polymorphism and recursion to your advantage.
- The application of Closed & Open principles will allow for introducing a new element type while maintaining the functionality of the existing code already present in the object tree.
Code Example
# The base Component class declares common operations for both simple and
# complex objects of a composition.
class Component
# @return [Component]
def parent
@parent
end
# Optionally, the base Component can declare an interface for setting and
# accessing a parent of the component in a tree structure. It can also provide
# some default implementation for these methods.
def parent=(parent)
@parent = parent
end
# In some cases, it would be beneficial to define the child-management
# operations right in the base Component class. This way, you won't need to
# expose any concrete component classes to the client code, even during the
# object tree assembly. The downside is that these methods will be empty for
# the leaf-level components.
def add(component)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# @abstract
#
# @param [Component] component
def remove(component)
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
# You can provide a method that lets the client code figure out whether a
# component can bear children.
def composite?
false
end
# The base Component may implement some default behavior or leave it to
# concrete classes (by declaring the method containing the behavior as
# "abstract").
def operation
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# The Leaf class represents the end objects of a composition. A leaf can't have
# any children.
#
# Usually, it's the Leaf objects that do the actual work, whereas Composite
# objects only delegate to their sub-components.
class Leaf < Component
# return [String]
def operation
'Leaf'
end
end
# The Composite class represents the complex components that may have children.
# Usually, the Composite objects delegate the actual work to their children and
# then "sum-up" the result.
class Composite < Component
def initialize
@children = []
end
# A composite object can add or remove other components (both simple or
# complex) to or from its child list.
# @param [Component] component
def add(component)
@children.append(component)
component.parent = self
end
# @param [Component] component
def remove(component)
@children.remove(component)
component.parent = nil
end
# @return [Boolean]
def composite?
true
end
# The Composite executes its primary logic in a particular way. It traverses
# recursively through all its children, collecting and summing their results.
# Since the composite's children pass these calls to their children and so
# forth, the whole object tree is traversed as a result.
def operation
results = []
@children.each { |child| results.append(child.operation) }
"Branch(#{results.join('+')})"
end
end
# The client code works with all of the components via the base interface.
def client_code(component)
puts "RESULT: #{component.operation}"
end
# Thanks to the fact that the child-management operations are declared in the
# base Component class, the client code can work with any component, simple or
# complex, without depending on their concrete classes.
def client_code2(component1, component2)
component1.add(component2) if component1.composite?
print "RESULT: #{component1.operation}"
end
# This way the client code can support the simple leaf components...
simple = Leaf.new
puts 'Client: I\'ve got a simple component:'
client_code(simple)
puts "\n"
# ...as well as the complex composites.
tree = Composite.new
branch1 = Composite.new
branch1.add(Leaf.new)
branch1.add(Leaf.new)
branch2 = Composite.new
branch2.add(Leaf.new)
tree.add(branch1)
tree.add(branch2)
puts 'Client: Now I\'ve got a composite tree:'
client_code(tree)
puts "\n"
puts 'Client: I don\'t need to check the components classes even when managing the tree:'
client_code2(tree, simple)
Structural Patterns #4: Decorator
Using a structural design pattern called Decorator, you may give objects different behaviors by enclosing them in special wrapper objects that also contain the behaviors. Since both target objects and decorators have the same interface, you can wrap things multiple times using decorators. The final product will exhibit all of wapper’s stacking behavior. It is rather common in ruby code, particularly in work that deals with streams.
The alternate term for the decorator pattern, Wrapper, perfectly captures the pattern’s central concept. A wrapper is a linkable object that can link to a target object. It delegates to it all requests it receives and has the same set of methods as the target. While doing something either before or after passing the request to the destination, the Wrapper can change the outcome.
Why use a Decorator?
- Without creating a new subclass, you can change how an object behaves.
- At runtime, you can change an object’s responsibility.
- You can combine several behaviors by encasing a given object with a number of decorators.
- Using the single responsibility concept, you may break up a monolithic class that implements a variety of possible behavioral variants into smaller classes.
Code Example
# decorators.
class Component
# @return [String]
def operation
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# Concrete Components provide default implementations of the operations. There
# might be several variations of these classes.
class ConcreteComponent < Component
# @return [String]
def operation
'ConcreteComponent'
end
end
# The base Decorator class follows the same interface as the other components.
# The primary purpose of this class is to define the wrapping interface for all
# concrete decorators. The default implementation of the wrapping code might
# include a field for storing a wrapped component and the means to initialize
# it.
class Decorator < Component
attr_accessor :component
# @param [Component] component
def initialize(component)
@component = component
end
# The Decorator delegates all work to the wrapped component.
def operation
@component.operation
end
end
# Concrete Decorators call the wrapped object and alter its result in some way.
class ConcreteDecoratorA < Decorator
# Decorators may call parent implementation of the operation, instead of
# calling the wrapped object directly. This approach simplifies extension of
# decorator classes.
def operation
"ConcreteDecoratorA(#{@component.operation})"
end
end
# Decorators can execute their behavior either before or after the call to a
# wrapped object.
class ConcreteDecoratorB < Decorator
# @return [String]
def operation
"ConcreteDecoratorB(#{@component.operation})"
end
end
# The client code works with all objects using the Component interface. This way
# it can stay independent of the concrete classes of components it works with.
def client_code(component)
# ...
print "RESULT: #{component.operation}"
# ...
end
# This way the client code can support both simple components...
simple = ConcreteComponent.new
puts 'Client: I\'ve got a simple component:'
client_code(simple)
puts "\n\n"
# ...as well as decorated ones.
#
# Note how decorators can wrap not only simple components but the other
# decorators as well.
decorator1 = ConcreteDecoratorA.new(simple)
decorator2 = ConcreteDecoratorB.new(decorator1)
puts 'Client: Now I\'ve got a decorated component:'
client_code(decorator2)
Structural Patterns #5: Façade
A façade is a structural design pattern that provides a limited but simplified interface to a complex system of classes, libraries, or frameworks. A facade is a class that provides a simple interface to a complex subsystem that contains lots of moving parts. A facade might provide limited functionality compared to directly working with the subsystem. However, it includes only those features that clients care about. Having a facade is handy when you need to integrate your app with a sophisticated library with dozens of features, but you need a tiny bit of its functionality.
Why use a façade?
- You can keep your code separate from a subsystem’s complexity.
Code Example
# The Facade class provides a simple interface to the complex logic of one or
# several subsystems. The Facade delegates the client requests to the
# appropriate objects within the subsystem. The Facade is also responsible for
# managing their lifecycle. All of this shields the client from the undesired
# complexity of the subsystem.
class Facade
# Depending on your application's needs, you can provide the Facade with
# existing subsystem objects or force the Facade to create them on its own.
def initialize(subsystem1, subsystem2)
@subsystem1 = subsystem1 || Subsystem1.new
@subsystem2 = subsystem2 || Subsystem2.new
end
# The Facade's methods are convenient shortcuts to the sophisticated
# functionality of the subsystems. However, clients get only to a fraction of
# a subsystem's capabilities.
def operation
results = []
results.append('Facade initializes subsystems:')
results.append(@subsystem1.operation1)
results.append(@subsystem2.operation1)
results.append('Facade orders subsystems to perform the action:')
results.append(@subsystem1.operation_n)
results.append(@subsystem2.operation_z)
results.join("\n")
end
end
# The Subsystem can accept requests either from the facade or client directly.
# In any case, to the Subsystem, the Facade is yet another client, and it's not
# a part of the Subsystem.
class Subsystem1
# @return [String]
def operation1
'Subsystem1: Ready!'
end
# ...
# @return [String]
def operation_n
'Subsystem1: Go!'
end
end
# Some facades can work with multiple subsystems at the same time.
class Subsystem2
# @return [String]
def operation1
'Subsystem2: Get ready!'
end
# ...
# @return [String]
def operation_z
'Subsystem2: Fire!'
end
end
# The client code works with complex subsystems through a simple interface
# provided by the Facade. When a facade manages the lifecycle of the subsystem,
# the client might not even know about the existence of the subsystem. This
# approach lets you keep the complexity under control.
def client_code(facade)
print facade.operation
end
# The client code may have some of the subsystem's objects already created. In
# this case, it might be worthwhile to initialize the Facade with these objects
# instead of letting the Facade create new instances.
subsystem1 = Subsystem1.new
subsystem2 = Subsystem2.new
facade = Facade.new(subsystem1, subsystem2)
client_code(facade)
Looking to Hire RoR Development Company?
Aglowid’s Ruby on Rails development services enables you to deliver a high-quality user experience to your customers
Structural Patterns #6: Flyweight
Using the structural design pattern known as “flyweight,” you can fit more objects into the available RAM by sharing a common state between different objects rather than storing all the data separately for each one. Programs may support enormous numbers of objects because of this structural design pattern, which keeps memory usage to a minimum. The pattern can accomplish this by dividing the object state among several objects. In other words, the Flyweight reduces RAM usage by caching data that numerous objects need. Minimizing memory intake is the sole objective of the flyweight pattern. You might disregard this pattern if your program doesn’t struggle with RAM limitations.
Why use Flyweight
Reduced memory use is the sole goal of this Ruby on Rails design approach. As a result, if your program uses little RAM, it won’t interrupt you for a time.
Code Example
require 'json'
# The Flyweight stores a common portion of the state (also called intrinsic
# state) that belongs to multiple real business entities. The Flyweight accepts
# the rest of the state (extrinsic state, unique for each entity) via its method
# parameters.
class Flyweight
# @param [String] shared_state
def initialize(shared_state)
@shared_state = shared_state
end
# @param [String] unique_state
def operation(unique_state)
s = @shared_state.to_json
u = unique_state.to_json
print "Flyweight: Displaying shared (#{s}) and unique (#{u}) state."
end
end
# The Flyweight Factory creates and manages the Flyweight objects. It ensures
# that flyweights are shared correctly. When the client requests a flyweight,
# the factory either returns an existing instance or creates a new one, if it
# doesn't exist yet.
class FlyweightFactory
# @param [Hash] initial_flyweights
def initialize(initial_flyweights)
@flyweights = {}
initial_flyweights.each do |state|
@flyweights[get_key(state)] = Flyweight.new(state)
end
end
# Returns a Flyweight's string hash for a given state.
def get_key(state)
state.sort.join('_')
end
# Returns an existing Flyweight with a given state or creates a new one.
def get_flyweight(shared_state)
key = get_key(shared_state)
if !@flyweights.key?(key)
puts "FlyweightFactory: Can't find a flyweight, creating new one."
@flyweights[key] = Flyweight.new(shared_state)
else
puts 'FlyweightFactory: Reusing existing flyweight.'
end
@flyweights[key]
end
def list_flyweights
puts "FlyweightFactory: I have #{@flyweights.size} flyweights:"
print @flyweights.keys.join("\n")
end
end
# @param [FlyweightFactory] factory
# @param [String] plates
# @param [String] owner
# @param [String] brand
# @param [String] model
# @param [String] color
def add_car_to_police_database(factory, plates, owner, brand, model, color)
puts "\n\nClient: Adding a car to database."
flyweight = factory.get_flyweight([brand, model, color])
# The client code either stores or calculates extrinsic state and passes it to
# the flyweight's methods.
flyweight.operation([plates, owner])
end
# The client code usually creates a bunch of pre-populated flyweights in the
# initialization stage of the application.
factory = FlyweightFactory.new([
%w[Chevrolet Camaro2018 pink],
['Mercedes Benz', 'C300', 'black'],
['Mercedes Benz', 'C500', 'red'],
%w[BMW M5 red],
%w[BMW X6 white]
])
factory.list_flyweights
add_car_to_police_database(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red')
add_car_to_police_database(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red')
puts "\n\n"
factory.list_flyweights
Structural Patterns #7: Proxy
Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something before or after the request gets through to the original object. This structural design pattern provides an object that is a substitute for a real service object used by a client. A proxy receives client requests, does some work (access control, caching, etc.), and then passes the request to a service object. The proxy object has the same interface as a service, which makes it interchangeable with a real object when passed to a client.
Why use the proxy?
- Without the Client’s knowledge, you can control the service object.
- When clients aren’t concerned, you can control how long a service object exists.
- The proxy still functions if the service object isn’t ready or isn’t there.
- The Open/Closed Principle With no need to modify the service or clients, new proxies can be added.
Code Example
# The Subject interface declares common operations for both RealSubject and the
# Proxy. As long as the client works with RealSubject using this interface,
# you'll be able to pass it a proxy instead of a real subject.
class Subject
# @abstract
def request
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end
# The RealSubject contains some core business logic. Usually, RealSubjects are
# capable of doing some useful work which may also be very slow or sensitive -
# e.g. correcting input data. A Proxy can solve these issues without any changes
# to the RealSubject's code.
class RealSubject < Subject
def request
puts 'RealSubject: Handling request.'
end
end
# The Proxy has an interface identical to the RealSubject.
class Proxy < Subject
# @param [RealSubject] real_subject
def initialize(real_subject)
@real_subject = real_subject
end
# The most common applications of the Proxy pattern are lazy loading, caching,
# controlling the access, logging, etc. A Proxy can perform one of these
# things and then, depending on the result, pass the execution to the same
# method in a linked RealSubject object.
def request
return unless check_access
@real_subject.request
log_access
end
# @return [Boolean]
def check_access
puts 'Proxy: Checking access prior to firing a real request.'
true
end
def log_access
print 'Proxy: Logging the time of request.'
end
end
# The client code is supposed to work with all objects (both subjects and
# proxies) via the Subject interface in order to support both real subjects and
# proxies. In real life, however, clients mostly work with their real subjects
# directly. In this case, to implement the pattern more easily, you can extend
# your proxy from the real subject's class.
def client_code(subject)
# ...
subject.request
# ...
end
puts 'Client: Executing the client code with a real subject:'
real_subject = RealSubject.new
client_code(real_subject)
puts "\n"
puts 'Client: Executing the same client code with a proxy:'
proxy = Proxy.new(real_subject)
client_code(proxy)
output.txt: Execution result
Client: Executing the client code with a real subject:
RealSubject: Handling request.
Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.
Wrapping Up!
Design Patterns in Ruby are a great way to improve code readability, maintainability, and scalability. They can be used to create expressive, elegant code and make it easier to understand and maintain. With the help of Ruby Design Patterns, developers can create more reliable, efficient, and maintainable code. Understanding the different design patterns and how they can be used to your advantage is important. You can make your code more efficient and easier to maintain and extend with the right design pattern.
have a unique app Idea?
Hire Certified Developers To Build Robust Feature, Rich App And Websites
Also Check:
This post was last modified on August 2, 2024 6:52 pm