Encapsulation in Ruby on Rails

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)

and

is a concept that binds together the data and functions that manipulate the data, and keep both safe from outside interference and misuse. (2)

Long version

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 Bill#pay and Bill#paid?.

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:

  • Account class has a field balance that stores the current account balance
  • The existence of balance= method

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:

  1. Does class A exist alone?
  2. Does class B depends on A?
  3. Can class B exist without A?

If 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 A.
For example, a blog post with comments: always create comments with post.comments.create(...) and never Comment.create(post: post...)


(1): https://stackify.com/oop-concept-for-beginners-what-is-encapsulation/

(2): http://rubyblog.pro/2017/01/object-oriented-programming-encapsulation-inheritance

Icons created by Freepik - Flaticon