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
16: Attr Accessors and Encapsulation
Think about your microwave. On the outside, you have a few buttons: a timer, a start button, and maybe a power level selector. You can see the clock on the display, and you can change the time. But you can't just reach your finger inside the machine and manually move the magnetron or tweak the high-voltage transformer to make it heat faster. If you could, you'd probably blow a fuse—or worse.
The buttons are the "interface." The actual circuitry inside is the "implementation." In Ruby, we call this concept encapsulation. We want to hide the messy, dangerous internal state of our objects and only expose specific ways to interact with them. This prevents other parts of your program from accidentally breaking the internal logic of your class.
Why your variables are hiding from you
In Ruby, instance variables (the ones starting with @) are strictly private. If you create a User class with a @username variable, you cannot access that variable from outside the object. I remember the first time I tried this; I kept getting nil and thought I was losing my mind.
class User
def initialize(name)
@name = name
end
end
me = User.new("Dave")
puts me.name # This will throw a NoMethodError!
Even though the data is right there inside the object, Ruby doesn't automatically give you a way to see it. To get that value, you'd have to write a "getter" method manually:
def name
@name
end
Writing those one-line methods for every single variable is tedious. That's where the attr_ family of methods comes in. They are essentially shortcuts that write those boring methods for you behind the scenes.
The shortcuts: Reader, Writer, and Accessor
Depending on how much access you want to give the outside world, you have three choices. I usually default to the most restrictive one first and only open things up as needed.
- attr_reader: This creates a "getter" method. It lets people read the value, but they can't change it. (Like the clock on the microwave).
- attr_writer: This creates a "setter" method. It lets people change the value, but they can't read it. This is rare, but useful for things like passwords where you want to set it once and never retrieve it.
- attr_accessor: This does both. It's a reader and a writer combined. (Like the timer button on the microwave).
Here is how that looks in a real class. Let's say we're building a simple LightBulb:
class LightBulb
attr_reader :brand # We can see the brand, but not change it
attr_accessor :brightness # We can see and change the brightness
def initialize(brand)
@brand = brand
@brightness = 0
end
end
lamp = LightBulb.new("Philips")
puts lamp.brand # Works! Output: Philips
lamp.brightness = 80 # Works! (via attr_accessor)
puts lamp.brightness # Works! Output: 80
lamp.brand = "GE" # Error! No writer method for brand
Protecting the guts of your object
You might be wondering, "Why not just use attr_accessor for everything?" Trust me, I've been there. It's tempting. But the goal of encapsulation is to maintain control. If you let any part of your code change any variable at any time, you'll eventually end up with a bug where a variable is set to a value that makes no sense (like a brightness of -500).
When you use a reader instead of an accessor, you're telling other developers: "You can look, but don't touch." If you need the value to change, but you want to make sure it stays within a certain range, you should skip the attr_writer and write your own custom setter method to validate the data before assigning it to the instance variable.
📋 Practical Task
Building a Secure Bank Account Wallet
You need to create a BankAccount class that prevents users from arbitrarily setting their own balance. If we used attr_accessor :balance, anyone could just type account.balance = 1000000 and rob the bank!
Your requirements:
- Create a
BankAccountclass that takes anowner_nameand aninitial_balanceduring initialization. - The
owner_nameshould be readable from the outside, but not changeable (use the appropriateattr_method). - The
balanceshould be readable from the outside, but not directly writable. - Create a method called
deposit(amount)that adds the amount to the balance. - Create a method called
withdraw(amount)that subtracts the amount from the balance, but only if the user has enough money. If they don't, print "Insufficient funds!".
Test your code by:
- Creating a new account for "Alice" with 100 dollars.
- Depositing 50 dollars.
- Attempting to withdraw 200 dollars (should fail).
- Withdrawing 30 dollars.
- Printing the final balance.
There are no comments for now.