Sebenarnya saya sudah cukup lama ingin menulis tentang hal ini tapi entah kenapa selalu kena pending.

Tapi, karena baru saja kemarin di projek saya, saya menemukan bug dan saya pun kesulitan untuk debuggingnya karena ada kesalahan dalam menulis private method ini maka akhirnya saya kesampaian juga untuk menulisnya disini.

Sebenernya saya juga bingung untuk memberikan judulnya karena mungkin kita tidak hanya berbicara tentang private method, namun juga akan berbicara tentang penggunaan instance variable dan bahkan public method.

Tapi karena private method lebih banyak berperan di tulisan ini, maka saya ambil itu sebagai judul, saja.

Sebelum mulai berbicara tentang private method saya ingin berbicara tentang dependency (kebergantungan).

Saya rasa anda pasti sudah pernah dengan bahkan mengetahui apa itu dependency. Singkatnya dependency sendiri ada jenis-jenis yang berbeda, antara lain:

  • Dependency level paket. Contohnya jika kita menggunakan Rails, Rails akan menggunakan paket activerecord. Ketika kita menggunakan activerecord, paket tersebut menggunakan paket activesupport. Maka, ketika kode di activerecord ada yang berubah maka implementasi dari kode Rails kita juga akan berubah.

    Paket dependency

  • Dependency level kelas. Contohnya pada kelas controller kita sering memanggil kelas model artinya kelas controller kita dependency terhadap model. Maka ketika kita membuat perubahan di model, maka akan berpengaruh pada kelas controller kita.

    Class dependent

  • Depedency level method. Contohnya ketika kita memanggil sebuah method dari method lain. Contohnya:

    def show
      @user = find_user(params[:id])
    end
    
    private
    
    def find_user(id)
      User.find_by(id: id)
    end
    

    Pada kode diatas, method show dependent terhadap method find_user(id), artinya jika mengubah kode di method find_user(id) maka akan berpengaruh pada method show.

    Function dependent

Kode yang memiliki banyak dependency adalah kode yang buruk. Misalnya anda memiliki satu kelas yang didalamnya memiliki dependent secara bertingkat hingga empat tingkat. Misalnya kelas A memanggil kelas B dan kelas B memanggil kelas C. Lalu kelas C memanggil kelas D.

Bad class depedency

Maka perubahan yang dilakukan di kelas D akan mengakibatkan perubahan di ketiga kelas diatasnya (A/B/C). Jika ada bug di fitur tersebut mungkin anda sudah bisa membayangkan bagaimana sulitnya kita mencari bug tersebut.

Jika kode yang memiliki depedency seperti itu, apakah anda yakin jika mengubah kode di dalam kelas D tidak akan menghancurkan ketiga kelas yang depedent terhadapnya?

Uncle Bob menyebut kode ini dengan rigidity. Kode menjadi sulit untuk dimodifikasi karena pergantian kecil saja dapat menghancurkan kelas-kelas yang lain.

Kode yang baik adalah kode yang mudah untuk dimodifikasi, maka semakin kecil depedency dari sebuah kode, kode tersebut semakin baik.

Menghindari penggunaan depedency dalam pembuatan software sangatlah sulit dan sebenernya penggunaan dependency dengan benar dapat membuat kode menjadi bersih. Khususnya untuk level kelas, ada yang namanya konsep dependency injection.

Namun, pada tulisan ini kita tidak sedang membahas dependency level kelas, melainkan level method.

Seperti yang sudah saya sebutkan sebelumnya, dependency juga bisa terjadi pada level method. Kita akan membahas buruknya hal tersebut, dan bagaimana cara saya untuk mengatasinya.

Kode yang buruk:

# frozen_string_literal: true

module AuthService
  # Class that handling creating token when employee sign in
  class CreateToken
    attr_reader :jwt_token, :login_url, :default_password

    def initialize(auth_params)
      @auth_params = auth_params

      raise 'Invalid auth params' if auth_params_empty?

      @username_or_email = auth_params[:username_or_email]
      @password = auth_params[:password]
    end

    def run
      checking_credentials
      load_default_password
      create_jwt_token
      create_moodle_token
    end

    private

    def auth_params_empty?
      return true if @auth_params[:username_or_email].nil? || @auth_params[:password].nil?
    end

    def create_jwt_token
      @jwt_token = TokenService.new(payload: { employee_id: @employee.id }).encoded
    end

    def checking_credentials
      load_employee

      return true if @employee&.authenticate(@password)

      raise 'Credentials is invalid'
    end

    def load_employee
      load_moodle_user

      @employee = @user.employee
    end

    def load_default_password
      @default_password = @employee.default_password
    end

    def create_moodle_token
      moodle = MoodleService::SignIn.new(
        username: @user.username,
        email: @user.email
      )

      # => Requesting token to moodle website
      moodle.run

      @login_url = moodle.login_url # => grap the moodle token
    end

    def load_moodle_user
      @user = Moodle::User.find_by(
        'username = :username_or_email OR email = :username_or_email',
        username_or_email: @username_or_email
      )
      raise 'Moodle user not found' if @user.nil?
    end
  end
end

Kelas diatas sebenarnya memiliki tanggung jawab yang sangatlah simple, yaitu mengembalikan token ke controller jika username dan password benar.

Untuk garis dependency-nya :

Bad Dependency Arrow

But wait….

Bagaimana jika saya menghapus saya mengubah nilai dari instance variable @auth_params[:username_or_email] dari method auth_params_empty? menjadi:

def auth_params_empty?
  return true if @auth_params[:username_or_email].nil? || @auth_params[:password].nil?
  @auth_params[:username_or_email] = "Changed!"
end

Maka method yang menggunakan instance variable tersebut akan juga ikut berubah dalam hal ini adalah method load_moodle_user. Jika method tersebut berubah keempat method yang dependent pada dirinya juga ikut berubah.

Oh…

Pada method create_moodle_token dan create_jwt_token juga menggunakan instance variable yang dibuat di kedua method load_employee load_moodle_user, maka artinya create_moodle_token dan create_jwt_token juga ikut berubah.

Dan dapat dibilang karena perubahan kecil di method tersebut, hampir semua method di kelas ini juga ikut berubah.

How a bad code :(

Maka dalam menulis kode di private method saya memiliki aturan untuk:

  1. private method tidak boleh menggunakan instance variable. Gunakan local variable yang dilempar melalui parameter.
  2. private method tidak boleh dependent atau memanggil method yang lain. private method harus terisolasi atau berdiri sendiri.
  3. public method menjadi main method yang bertanggung jawab terhadap flow algoritma dan perpindahan data antara satu private method dan private method lain. Jangan pisahkan proses flow-nya seperti yang kita lakukan sebelumnya

Maka, berdasarkan aturan tersebut, saya menulis ulang kelas tersebut menjadi:

# frozen_string_literal: true

module AuthService
  # Class that handling creating token when employee sign in
  class CreateToken
    attr_reader :jwt_token, :login_url, :default_password

    def initialize(username_or_email, password)
      raise 'Invalid auth params' if username_or_email.blank? && password.blank?

      @username_or_email = username_or_email
      @password = password
    end

    def run
      moodle_user = find_moodle_user(@username_or_email)
      raise 'Moodle user not found' if moodle_user.blank?

      employee = moodle_user.employee
      raise 'Credentials is invalid' unless employee&.authenticate(@password)

      @jwt_token = create_jwt_token(employee)
      @login_url = create_moodle_token(moodle_user)
      @default_password = employee.default_password
    end

    private

    def find_moodle_user(username_or_email)
      Moodle::User.find_by(
        'username = :username_or_email OR email = :username_or_email',
        username_or_email: username_or_email
      )
    end

    def create_jwt_token(employee)
      TokenService.new(payload: { employee_id: employee.id }).encoded
    end

    def create_moodle_token(moodle_user)
      moodle = MoodleService::SignIn.new(
        username: moodle_user.username,
        email: moodle_user.email
      )
      moodle.run
      moodle.login_url
    end
  end
end

Apakah anda merasakan perbedaannya?

Untuk garis dependency-nya kira-kira menjadi seperti ini:

Good Arrow Dependency

Pada kode diatas private method kita tidak memanggil private method yang lain, namun memangil kelas yang lain, yang menurut saya masih cukup baik. Selain itu saya juga menghilangkan penggunaan passing parameter by hash namun menggantinya dengan passing parameter by values yang membuat kodenya semakin simple.

Karena bagi saya passing by hash hanya membuat kodenya menjadi semakin kompleks dan lebih sulit dibaca. Anda bisa coba bandingkan kode dibawah ini:

# Passing by options(hash)
CreateToken.new(
  username_or_email: auth_attributes[:username_or_email],
  password: auth_attributes[:password]
)

# Passing by values
CreateToken.new(
  auth_attributes[:username_or_email],
  auth_attributes[:password]
)

# Definition (options)
def initialize(auth_attributes)

# Definition (value)
def initialize(username_or_email, password)

Kode kita menjadi lebih simple dan lebih reliable.

Memang setau saya tidak ada aturan resmi mengenai ini. Namun jika anda lebih memilih parameter passing by hash, saya menyarankan untuk menggunakan passing by keywords, maka anda perlu mengubah kode definisinya menjadi:

# Before
def initialize(auth_attributes = {})

# After
def initialize(username_or_email:, password:)

Sekiranya segitu saja untuk tulisan kali ini, ikuti tips ini sebisa mungkin maka kode anda akan menjadi lebih bersih dan mudah untuk diubah.

Rule ini tidak bersifat mutlak, mungkin saja ada masalah-masalah yang memang tidak cocok dengan rule ini.

Terima kasih,

Happy Hacking!