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
94: The Factory Pattern in Ruby
A few years ago, I was reviewing a PR for a junior dev who was building a payment integration for an e-commerce site. At first, it looked fine—they had a StripeProcessor class that handled everything. But then the business decided to add PayPal and Square. I remember opening the CheckoutService and seeing a massive if/elsif/else block that spanned thirty lines just to decide which class to instantiate based on a string from the database. It was brittle, ugly, and every time we added a new payment method, we had to touch the core checkout logic, which is a recipe for introducing bugs in the most sensitive part of the app.
This is exactly where the Factory Pattern saves your sanity. In essence, a Factory is an object whose sole responsibility is to create other objects. Instead of your business logic worrying about how to instantiate a specific class or which specific class is needed, it just tells the Factory: "I need a processor for 'stripe'," and the Factory handles the messy details of returning the right instance.
Stop Scattering Your New Keywords
When you call .new on a class, you're hard-coding a dependency. If you have StripeProcessor.new sprinkled across five different services, and you later decide that StripeProcessor now requires an API key in its initializer, you have to hunt down every single one of those calls and update them. That's a tedious way to spend a Tuesday.
By wrapping this in a Factory, you centralize the instantiation. Your service layer doesn't know StripeProcessor exists; it only knows the PaymentProcessorFactory. This decoupling means you can change your class names, add initialization logic, or even switch to a different implementation entirely without touching your business logic.
class StripeProcessor
def process(amount)
puts "Processing $#{amount} via Stripe..."
end
end
class PaypalProcessor
def process(amount)
puts "Processing $#{amount} via PayPal..."
end
end
class PaymentProcessorFactory
def self.for(type)
case type
when 'stripe' then StripeProcessor.new
when 'paypal' then PaypalProcessor.new
else
raise "Unsupported payment method: #{type}"
end
end
end
Keeping it Ruby-ish with Hash Lookups
While a case statement works, it can get bloated. Since we're working in Ruby, we can leverage the fact that classes are first-class objects. We can store the classes themselves in a hash and call .new on the result. I personally prefer this approach because it's cleaner and makes it incredibly easy to register new types dynamically if you ever need to.
Check out how much leaner this becomes. We map the identifier to the class, fetch the class, and then instantiate it. It's a tiny change that makes the code feel much more professional and extensible.
class PaymentProcessorFactory
PROCESSORS = {
'stripe' => StripeProcessor,
'paypal' => PaypalProcessor,
'square' => SquareProcessor
}.freeze
def self.for(type)
processor_class = PROCESSORS[type]
raise "Unsupported payment method: #{type}" unless processor_class
processor_class.new
end
end
# Now the client code is blissfully ignorant:
processor = PaymentProcessorFactory.for('stripe')
processor.process(100)
The beauty here is that the CheckoutService now only cares that the object it receives responds to .process. It doesn't care if it's Stripe, PayPal, or some obscure local bank API. You've effectively isolated the "creation" logic from the "execution" logic.
📋 Practical Task
The Multi-Channel Notification Dispatcher
You are building a notification system for a social media app. Depending on the user's preferences, a notification needs to be sent via Email, SMS, or Push Notification.
Your task:
- Create three classes:
EmailNotification,SmsNotification, andPushNotification. Each should have a method calledsend_message(message)that prints "Sending [Message] via [Channel]". - Implement a
NotificationFactorythat uses a hash lookup (not a case statement) to return the correct notification object based on a symbol (e.g.,:email,:sms,:push). - Ensure the factory raises a custom error or a descriptive
RuntimeErrorif an unsupported notification type is requested. - Write a small script that iterates through an array of preferences
[:email, :push, :carrier_pigeon, :sms]and attempts to send the message "Your post was liked!" using the factory. Your code should handle the error gracefully when it hits:carrier_pigeon.
There are no comments for now.