Encapsulation in Ruby on Rails
In Object-Oriented Programming, encapsulation is one of the fundamentals concept. Understand encapsulation will help you write concise and easy to maintain code. But what exactly is encapsulation? If you search on the Internet you might find something like this:
encapsulation is hide the internal representation or state of an object. (1)
is a concept that binds together the data and functions that manipulate the data, and keep both safe from outside interference and misuse. (2)
Encapsulation is the ability to hide the internals of the class, exposing a simple interface while doing complex operations to mutate the object's state.
For example, for a
Bill class, the caller does not need to know how to mark a bill as paid: is it a boolean field set to true? a
paid_at date set to today's date?. Doesn't matter. The caller only need to invoke
The problem with the definitions of encapsulation on the Internet
It is common to see people showing encapsulation examples with something like this
class Account < ApplicationRecord def initialize(iban, balance) @iban = iban @balance = balance end attr_accessor :iban, :balance end account = Account.find_by(iban: 'DE123') # user transfer money account.balance += 20 account.save!
and then say this is encapsulation because there is setters and getters created by ActiveRecord. This is not.
To update the balance clients must know two things:
Accountclass has a field
balancethat stores the current account balance
- The existence of
We broke the encapsulation because clients knows too much about the
Account. This small problem can become a snowball: the more clients you have whose actions change account's balance the more you are exposing user's account. Think about a client called
WithdrawService that will handle money withdraw from an account. This client will have unlimited access to account's balance and can change this value as it wants. Since
balance is not encapsulated, bad intentioned clients can update their balance, allowing to spend more than they have.
How to proper encapsulate on Ruby on Rails
Domain-driven design has an interesting pattern called Aggregate. An aggregate is the entrypoint for the operations that mutates an object's state.
Back to our bank account example we know that an account has many transactions and a transaction is always attached to a bank account. It is safe to say that a transaction does not exist without a bank account. That means
Account is our aggregate and all transactions and balance manipulation should happen inside it.
A better version of our bank account would be something like this:
class Account < ApplicationRecord has_many :transactions def execute_transfer(to, amount) transaction do transactions.create!(amount: -amount) to.transactions.create!(amount: amount) end end end class Transaction < ApplicationRecord belongs_to :account after_commit :update_balance_account private def update_balance_account transaction do account.balance += amount account.save! end end end
ps: this is just an example, don't use callbacks for something like this
Whoever is dealing with transactions now doesn't need to manipulate
balance. We encapsulated
Account so the API remains simple while we can evolve the method. With this business case isolated, we can add balance validation for example, without having to update the clients.
Rules for find classes lacking encapsulation
To find these classes that are leaking information and methods to clients, ask yourself this:
- Does class
- Does class
- Can class
A can exist alone and
B does not make sense to exist unless linked to
A (a simple
A has_many B) you should encapsulate your code so all
B transactions happen inside
For example, a blog post with comments: always create comments with
post.comments.create(...) and never