Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
93: The Null Object Pattern in Ruby
You've probably spent a fair amount of time using the safe navigation operator &. or || to stop your app from blowing up with the dreaded NoMethodError: undefined method '...' for nil:NilClass. When you first encounter this, it feels like a victory. You've silenced the error, and your code is "safe."
The Safe Navigation Operator is a Complete Solution
The common misconception is that &. is the "correct" way to handle missing data. It's not. While it prevents a crash, it forces the caller of the method to care about whether an object exists or not. This is what I call "defensive programming," and it quickly turns your business logic into a minefield of conditionals.
Look at this snippet for a hypothetical e-commerce system where a User might or might not have a DiscountProfile:
class Order
def total(user)
discount = user.discount_profile&.amount || 0
100 - discount
end
end
This looks clean, right? But what happens when you need to check discount_profile.eligible?, discount_profile.expiry_date, and discount_profile.code in five different places across your app? You end up peppering your entire codebase with &. and ||. You aren't describing how your system works; you're spending all your energy managing the fact that something might be missing.
Turning "Nothing" into a First-Class Object
The Null Object Pattern flips the script. Instead of returning nil and making the rest of the app handle the void, we return a specific object that represents "nothing" but still speaks the same language as the "something" object. It's a way of saying, "Yes, this is empty, but here is how an empty version of this object behaves."
Let's refactor that discount example. We'll create a NullDiscountProfile that implements the same methods as a real profile, but returns default, neutral values.
class DiscountProfile
attr_reader :amount
def initialize(amount)
@amount = amount
end
def eligible?
true
end
end
class NullDiscountProfile
def amount
0
end
def eligible?
false
end
end
class User
attr_reader :discount_profile
def initialize(discount_profile = nil)
# We ensure that we NEVER return nil here
@discount_profile = discount_profile || NullDiscountProfile.new
end
end
Now, look at how the Order class changes:
class Order
def total(user)
# No safe navigation, no || 0. Just call the method.
100 - user.discount_profile.amount
end
end
I love this approach because the Order class no longer needs to know that a user might lack a discount profile. It just asks for the amount and gets a number. The logic for "what happens when there is no discount" is encapsulated inside the NullDiscountProfile class, where it belongs.
A few things to keep in mind as you implement this:
- Interface Parity: Your Null Object must respond to the same methods as the real object. If the real object has a
savemethod, the Null Object should probably have one too (though it might do nothing). - Avoid "Leaking" Nil: The goal is to stop
nilfrom propagating. The best place to swapnilfor a Null Object is usually in the getter method or the factory that instantiates the object. - Truthiness: Remember that in Ruby, every object except
falseandnilis truthy. If you have code that doesif user.discount_profile, it will now always be true. If you actually need to check for the absence of a real object, you can add a method likenull?to both classes.
📋 Practical Task
Refactoring the Guest Customer System
You are working on a checkout system where some customers are registered members and some are guests. Currently, the code is riddled with nil checks because guests don't have a CustomerProfile object.
Your Task: Implement a NullCustomerProfile class to remove the defensive programming from the Invoice class.
class CustomerProfile
attr_reader :name, :email
def initialize(name, email)
@name = name
@email = email
end
def loyalty_points
100 # Simplified for this example
end
end
class Invoice
def summary(profile)
# TODO: Refactor this method to remove the conditional logic
if profile
"Customer: #{profile.name} (#{profile.email}) - Points: #{profile.loyalty_points}"
else
"Customer: Guest (N/A) - Points: 0"
end
end
end
# Current problematic usage:
invoice = Invoice.new
puts invoice.summary(CustomerProfile.new("Alice", "alice@example.com")) # Works
puts invoice.summary(nil) # Requires the 'if' check in Invoice
Requirements:
- Create a
NullCustomerProfileclass that provides default values forname,email, andloyalty_points. - Modify
Invoice#summaryso it can simply call the methods on theprofileobject without using anif/elseor&.. - Ensure that
invoice.summary(NullCustomerProfile.new)produces the same output as the originalinvoice.summary(nil).
There are no comments for now.