Event-driven Development di Rails
Karena kebetulan di kantor kemarin ada ngerjain projek yang di dalam codebasenya pakai pattern ini, jadi sambil belajar, coba nulis disini juga :)
Event-driven development mungkin lebih terkenal di dunia frontend dibanding di dunia backend. Jika anda pernah menggunakan modern javascript saat ini seperti react dan vue.js mungkin anda akan familiar dengan konsep ini. Event-driven development adalah sebuah pattern yang menggunakan event sebagai flow sistemnya.
Pada tulisan ini saya mencoba menjelaskan konsep dari event-driven development menggunakan studi kasus. Kita akan membuat sebuah projek sederhana. Pertama kita akan menggunakan cara tradisional, lalu melakukan refactoring dengan mengimplementasikan event-driven. Untuk pattern ini saya akan menggunakan paket rails_event_store.
Desain Aplikasi
Seperti yang sudah anda tau, kita akan membuat aplikasi api pembuat user, kira-kira desainnya seperti ini:
-
Membuat user baru:
POST /api/users { "email": "pquest@gmail.com", }
-
Melihat informasi data dari user:
GET /api/users/:id { "email: "pquest@gmail.com", "status": "inactive" }
-
Membuat user menjadi active:
POST /api/users/activation { "id": 1 }
Bisa dilihat dari desain aplikasi diatas, maka kita akan membuat setidaknya tiga fitur, yaitu membuat user baru dengan status yang inactive, lalu membuat sebuah enpoint yang dapat membuat spesifik user bisa menjadi aktif.
Fitur terakhir kita dapat melihat informasi dari user yang bersangkutan. Pada fitur pertama dan ketiga kita akan membuatkan lognya.
Sekarang, mari kita membuat fiturnya satu-per-satu dari fitur pertama membuat user baru, kode specnya:
require 'rails_helper'
RSpec.describe "Create new user", type: :request do
it 'creates expected user' do
post '/api/users', params: { user: { email: 'pquest@gmail.com' } }
expect(json_response['email']).to eq 'pquest@gmail.com'
user = User.find_by(email: 'pquest@gmail.com')
expect(user).to be_inactive
log = Log.last
expect(log.text).to eq 'User with email pquest@gmail.com has been created'
end
private
def json_response
JSON.parse(response.body)
end
end
Jalankan dan lihat kode testnya menghasilkan failed test case. Lalu kita buat kode testnya menjadi passed, dengan menulis kode ini:
# app/controllers/api/users_controller.rb
module Api
class UsersController < ApplicationController
def create
user = User.new(user_params)
user.save
Log.create(text: "User with email #{user.email} has been created")
render json: { email: user.email }
end
private
def user_params
params.require(:user).permit(:email)
end
end
end
# app/models/user.rb
class User < ApplicationRecord
enum status: [:inactive]
end
Fitur pertama telah berhasil kita buat, sekarang fitur kedua yaitu melihat informasi dari spesific user:
require 'rails_helper'
RSpec.describe 'Show user information', type: :request do
it 'returns expected user' do
user = create(:user, email: 'pquest@gmail.com')
get "/api/users/#{user.id}"
expect(json_response['email']).to eq 'pquest@gmail.com'
expect(json_response['status']).to eq 'inactive'
end
private
def json_response
JSON.parse(response.body)
end
end
Sekarang buat kodenya menjadi passed dengan kode ini:
# app/controllers/api/users_controller.rb
module Api
class UsersController < ApplicationController
# ...
def show
user = User.find_by(id: params[:id])
render json: { email: user.email, status: user.status }
end
# ...
end
end
Sekarang mari kita buat fitur terakhir yaitu membuat user statusnya menjadi active dari sebelumnya yang inactive.
require 'rails_helper'
RSpec.describe "User activation", type: :request do
it 'make user actives' do
user = create(:user, status: 0)
expect(user).to be_inactive
post '/api/users/activations', params: { id: user.id }
expect(json_response['email']).to eq user.email
expect(json_response['status']).to eq 'active'
user.reload
expect(user).to be_active
log = Log.last
expect(log.text).to eq "User with email #{user.email} has been activated"
end
private
def json_response
JSON.parse(response.body)
end
end
Untuk kode produksinya:
# /api/users/activations_controller.rb
module Api
module Users
class ActivationsController < ApplicationController
def create
user = User.find_by(id: params[:id])
user.active!
Log.create(text: "User with email #{user.email} has been activated")
render json: { email: user.email, status: user.status }
end
end
end
end
# app/models/user.rb
class User < ApplicationRecord
enum status: [:inactive, :active]
end
Maka, ketiga fitur sudah berjalan seperti yang di ekspektasi sekarang waktunya untuk melakukan refactoring dengan menggunakan event-driven development.
Kita akan menggunakan rails-event-store, maka sebelumnya silahkan tambahkan gem tersebut di Gemfile:
gem "rails_event_store"
Lalu jalankan $> bundle install
.
Sekarang kita lakukan setup lain yaitu database:
$> string stop
$> rails generate rails_event_store_active_record:migration
$> rails db:migrate
Setelah database telah berhasil di-isi sekarang kita buat global statenya:
Rails.configuration.to_prepare do
Rails.configuration.event_store = $event_store = RailsEventStore::Client.new
$event_store.subscribe(UserCreatedHandler.new, to: [UserCreated])
end
Kita akan merefactor fitur yang pertama, yaitu fitur membuat user baru:
# app/controllers/users_controller.rb
module Api
class UsersController < ApplicationController
def create
user = User.new(user_params)
user.save
event = UserCreated.new(data: { user: user })
$event_store.publish(event)
render json: { email: user.email }
end
# ...
end
end
## app/handlers/user_created_handler.rb
class UserCreatedHandler
def call(event)
user = event.data[:user]
user_email = user.email
Log.create(text: "User with email #{user_email} has been created")
end
end
# app/events/user_created.rb
class UserCreated < RailsEventStore::Event; end
Sekarang jalankan kode testnya kembali, dan kita masih mendapat pesan sukses, artinya refactoring untuk fitur pertama kita telah berhasil.
Sekarang lanjut ke fitur ketiga (fitur kedua kita lewati karena memang tidak ada handlernya/log):
# app/controllers/users/activations_controller.rb
module Api
module Users
class ActivationsController < ApplicationController
def create
user = User.find_by(id: params[:id])
user.active!
event = UserActivated.new(data: { user: user })
$event_store.publish(event)
render json: { email: user.email, status: user.status }
end
end
end
end
# app/handlers/user_activated_handler.rb
class UserActivatedHandler
def call(event)
user = event.data[:user]
user_email = user.email
Log.create(text: "User with email #{user_email} has been activated")
end
end
# app/events/user_activated.rb
class UserActivated < RailsEventStore::Event; end
Sekarang jalankan kembali testnya dan hasilnya akan kembali sukses. Artinya kita berhasil merefactoring fitur terakhir ini.
Konklusi
Karna saya baru menggunakan atau belajar pola ini, jadi saya masih belum bisa menyarankan untuk menggunakan pola ini untuk next projek anda atau menggunakan pola ini untuk membersihkan atau refactor projek anda.
Tapi kelebihan yang mungkin saya rasakan mungkin dengan pola ini kode kita menjadi lebih independent, peran handlernya bisa sangat jelas dibandingkan dengan pola service objek yang mungkin harus lebih hati-hati dalam membuatnya.
Tapi kekuarangnya mungkin kita akan sulit melakukan track flow dari kode kita karena mungkin akan sulit menemukan sumbernya. Jadi mungkin debuggingnya caranya bisa beda dengan tradisional pada umumnya.
Sekali lagi saya tidak bisa menentukan mana pola yang lebih baik, karena masih baru belajar juga. Jika anda tertarik untuk kode sumbernya bisa di lihat disini.
Terima kasih telah membaca, semoga tulisan ini dapat bermamfaat bagi pembaca skalian, thank you.
Happy hacking ~