Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions content/blog/refinements-in-ruby.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
+++
title = "Refinements in Ruby: A Flexible Approach to Modifying Core Classes"
slug = "refinements-in-ruby"
date = 2024-09-13T11:48:18+05:30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update the date to today, it will show up chronologically on the list

Suggested change
date = 2024-09-13T11:48:18+05:30
date = 2024-12-12T11:48:18+05:30

image = "/images/2024/refinements-in-ruby/header.jpeg"
draft = false
authors = ["Syed Mohd Mehndi"]
description = "Refinements in Ruby: A Flexible Approach to Modifying Core Classes"
tags = ["Ruby", "Clean Code"]
categories = ["Ruby", "Clean Code"]
type = ""
+++

Ruby is a highly flexible programming language known for its dynamic capabilities and metaprogramming power. One such powerful feature in Ruby is refinements. Introduced in Ruby 2.0, refinements allow you to modify existing classes (even core classes like String, Array, etc.) in a more controlled and localized way. This is particularly useful when you want to change class behavior in specific contexts without affecting the global environment, which is a common issue with traditional monkey-patching.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Ruby is a highly flexible programming language known for its dynamic capabilities and metaprogramming power. One such powerful feature in Ruby is refinements. Introduced in Ruby 2.0, refinements allow you to modify existing classes (even core classes like String, Array, etc.) in a more controlled and localized way. This is particularly useful when you want to change class behavior in specific contexts without affecting the global environment, which is a common issue with traditional monkey-patching.
Ruby is a highly flexible programming language known for its dynamic capabilities and metaprogramming power. One such powerful feature in Ruby is **refinements**. Introduced in Ruby 2.0, refinements allow you to modify existing classes (even core classes like `String`, `Array`, etc.) in a more controlled and localised way. This is particularly useful when you want to change class behaviour in specific contexts without affecting the global environment, which is a common issue with traditional monkey-patching.

Shouldn't we use Indian English instead of US English?


In this blog, we’ll dive into the basics of using refinements, explore common use cases, and explain why they can be preferable to monkey-patching.

## What Are Refinements?

Refinements offer a way to extend or override methods of existing classes, but only within a specific scope (like a module or class). This helps prevent unintended side effects that can occur when modifying core classes or third-party libraries globally. In other words, refinements give you a safer alternative to monkey-patching by limiting the scope of changes.

### Key Characteristics of Refinements:

- **Scoped Modifications**: Refinements apply only in specific contexts (like within a module or class).
- **Opt-in Behavior**: You have to explicitly “activate” a refinement within the scope where you want it to apply.
- **No Global Impact**: Unlike monkey-patching, which modifies behavior globally, refinements are local to the scope they are activated in.

## Refinements in Action

Let’s walk through an example to demonstrate how refinements work in Ruby.

### Step 1: Defining a Refinement

Here’s how you define a refinement in Ruby. We will modify the String class to add a new method, #reverse_words, which reverses the words in a string, not the characters.

```ruby
module StringExtensions
refine String do
def reverse_words
self.split(' ').reverse.join(' ')
end
end
end
Comment on lines +37 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
module StringExtensions
refine String do
def reverse_words
self.split(' ').reverse.join(' ')
end
end
end
module StringExtensions
refine String do
def reverse_words
self.split(' ').reverse.join(' ')
end
end
end

Personally, I use 2 spaces as it reads nicely without having too much whitespace. But to each their own I guess.

```

In this example, we created a module `StringExtensions` that contains a refinement of the String class. This refinement adds a method reverse_words to reverse the order of words in a string.

### Step 2: Activating the Refinement

To use the refinement, we need to activate it in a specific scope with the `using` keyword:

```ruby
class SentenceManipulator
using StringExtensions

def initialize(sentence)
@sentence = sentence
end

def reverse
@sentence.reverse_words
end
end

sentence = SentenceManipulator.new("Hello World from Ruby")
puts sentence.reverse
# Output: "Ruby from World Hello"
```

Here, the refinement applies only inside the `SentenceManipulator` class. If you try to use reverse_words outside of this class, it will not be available.

### Step 3: Trying to Use the Refinement Outside the Scope

```ruby
puts "Hello World from Ruby".reverse_words
# Output: NoMethodError: undefined method `reverse_words' for "Hello World from Ruby":String
```

This code will raise an error because the refinement was not activated globally, and it’s only available within the SentenceManipulator class.

### What Happens When You Use Refinements Inside Their Defining Module?

Refinements in Ruby are designed to only take effect when explicitly activated with the `using` keyword. This means that even if you try to use a refinement inside the module where it is defined, it won’t work unless the refinement is explicitly activated in the scope where it is used.

Here’s an example to demonstrate this behavior:

```ruby
module StringExtensions
refine String do
def reverse_words
self.split(' ').reverse.join(' ')
end
end

class Foo
def bar
'Hello World'.reverse_words
end
end
end

StringExtensions::Foo.new.bar
# Output: NoMethodError: undefined method `reverse_words' for "Hello World":String
```

#### Why Does This Happen?

In Ruby, refinements are not automatically activated within the module where they are defined. Instead, you must explicitly activate them with the `using` keyword in the scope where they are intended to be used.

#### Fixing the Issue

To fix this, you can activate the refinement within the `Foo` class with the `using` keyword:

```ruby
module StringExtensions
refine String do
def reverse_words
self.split(' ').reverse.join(' ')
end
end

class Foo
using StringExtensions

def bar
'Hello World'.reverse_words
end
end
end

puts StringExtensions::Foo.new.bar
# Output: "World Hello"
```

By adding using `StringExtensions` inside the `Foo` class, we explicitly enable the refinement for that scope, and the reverse_words method becomes available.

#### Key Takeaway

Even though a refinement is defined within a module, it is not automatically available within the module’s inner classes or methods. This is intentional to ensure that refinements remain opt-in and their effects are explicitly scoped. Always use the using keyword in the desired scope to activate the refinement.

## Why Use Refinements Instead of Monkey-Patching?

1. **Limited Scope**: Unlike monkey-patching, which alters a class’s behavior globally, refinements allow changes to be confined to specific contexts. This reduces the risk of breaking code in other parts of your application.

1. **Better Code Isolation**: Refinements ensure that method modifications do not leak out and affect other classes or libraries unintentionally. With refinements, you can safely modify the behavior of third-party libraries without worrying about conflicts.

1. **Safer Library Usage**: Libraries can define refinements to tweak core class behaviors for internal use without impacting their users’ code. This leads to more reliable and maintainable libraries.
Comment on lines +143 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have them in a single list?


## Common Use Cases for Refinements

1. **Overriding Third-Party Library Behavior**: If you need to modify the behavior of a third-party library without affecting the rest of your application, refinements allow you to do so safely.

1. **Enhancing Core Classes for Specific Purposes**: When working with Ruby's core classes like String or Array, you may want to add methods that are useful for your domain without affecting how those classes work globally.

1. **Testing and Prototyping**: Refinements are great for temporarily modifying behavior during testing or prototyping, as the changes can be localized to the test suite or experimental code.

## Caveats of Using Refinements

While refinements offer great flexibility, they come with certain caveats:

1. **Performance Overhead**: Refinements introduce a small overhead, especially when resolving method calls. While this is generally minimal, it may impact performance in hot paths.

1. **Limited Usage in Some Libraries**: Some libraries and frameworks (such as Rails) may not fully support refinements or may have conflicts with them due to their reliance on method lookups.

1. **Limited Method Visibility**: Refinements only affect methods directly called on objects. They do not apply to methods called indirectly via send, method, or define_method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. **Limited Method Visibility**: Refinements only affect methods directly called on objects. They do not apply to methods called indirectly via send, method, or define_method.
1. **Limited Method Visibility**: Refinements only affect methods directly called on objects. They do not apply to methods called indirectly via send, method, or `define_method`.


## Conclusion

Refinements in Ruby provide a powerful and flexible alternative to monkey-patching by allowing developers to extend or modify classes in a controlled and localized manner. They help maintain code modularity, prevent unintended side effects, and promote better practices when working with core classes and third-party libraries.

However, keep in mind the caveats and use refinements judiciously. If your goal is to alter class behavior without the risk of affecting the global state of your application, refinements are an excellent tool to have in your Ruby toolkit.

## Further Reading:

Ruby Documentation on Refinements

- [Ruby Documentation on Refinements](https://docs.ruby-lang.org/en/2.4.0/syntax/refinements_rdoc.html 'Ruby Documentation on Refinements')
- [Understanding Monkey-Patching in Ruby](https://blog.incubyte.co/blog/monkey-patch 'Understanding Monkey-Patching in Ruby')

Happy coding!
Copy link
Collaborator

@2KAbhishek 2KAbhishek Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can expand the width of the image, since its plain white

image

this is what it looks like now

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading