Ruby on Rails
Useful and Practical Patterns
Dwi Wahyudi - Jurnal Software Engineer
Let’s keep it simple.
Form Object
For when we want to deal with
complex forms (cross-domain) (user
inputs).
Form Object
● By using ActiveModel module.
● Handle complex forms from multiple domains in one place.
○ One use case (data create/update) => One form class.
○ One form class => One API/HTML form.
○ Use database transaction to atomically-commit saving data.
● Correlated with create/update actions, or anything that create/update
data.
● https://guides.rubyonrails.org/v4.2/active_model_basics.html
● We’re going to focus on validations (ActiveModel::Validations).
● Validations error should return error 422.
Form Class (continued...)
● From create / update actions to form and back to actions again.
● One form class shouldn’t call another form class.
○ The business case is rarely happen.
○ Be clear on what we do.
● A form class does exactly one form, and it should do it well.
● If we feel that a form class can have complex/reusable validations, than
it’s time to create a custom validation class.
Example
class RegistrationForm
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :name, :email, :password,
:password_confirmation, :phone_num,
:address, :billing_address,
:tax_number, :dependents
validates :name, :email, :password,
:password_confirmation, :phone_num, presence: true
validates :password, length: { in: 8..20 }
validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
def initialize(user, address_data, tax_data, dependents_data)
@user = user
# ...
end
def save
# ... save user data, address data, tax data, dependents data, etc.
end
end
Save Method
def save
return false if invalid?
ActiveRecord::Base.transaction do
save_user_data
save_address_data
save_dependents_data
save_tax_data
# ... etc
end
true
end
In Controller Action
def create
form = RegistrationForm.new(form_params) # harus dipilah dulu, don’t pass web params
if form.save
render json: { type: 'success',
message: I18n.t('api.registration_form.success') },
status: :created
else
render_error(form.errors) # form.errors has complete data about errors validated by form
end
end
private
def form_params
params.permit(:name, :email, :password,
:password_confirmation, :phone_num,
:address, :billing_address,
:tax_number, :dependents)
end
Custom Validator Class
● If we feel that validations in a form class is long / complex / reusable, we
should consider extracting them to a custom validator class.
● https://guides.rubyonrails.org/v4.2/active_record_validations.html#perfor
ming-custom-validations
Custom Validator Class
class RegistrationTimeAndLocationAttributesValidator < ActiveModel::Validator
def validate(form)
obtain_attributes(form)
validate_attributes(form)
end
private
def obtain_attributes(form)
# .. obtain attributes from form instance variable
# form.user
# form.address_data
# etc
end
def validate_attributes(form)
# ...
end Then add this in form class:
end validates_with RegistrationTimeAndLocationAttributesValidator
Service Object
Small, reusable and testable classes.
Service Object
● Simple, reusable and isolated class.
● Simple Ruby class.
● Benefits:
○ Slim models, slim controllers.
○ Separate the web-related code (Rails controllers) with business logic.
● Can be used not only to serve web requests, but other tasks as well.
● May create feature envy code smell, but it is expected.
● Not recommended if only involving a single class/object usage (for simple
usage) (write code in that class instead).
Service Class (continued...)
● A service class does exactly one thing, and it should do it well.
● Usually for reading/querying data.
○ Caching of that data might be handled here.
○ Can also create/update data, but usually not from user inputs.
● Hiding the complexity for a business process.
● The smaller the better.
● A service might not represent a Use Case.
● But a Use Case might be represented by a service class.
Example
class CalculateOrderTotal class CustomerLatestActivitiesFetchService
def initialize(order) def initialize(customer, size = 800)
@order = order @customer = customer
end @size = size
end
def perform # or def call
# do calculation here. def perform # or def call
end # do fetching here here.
end
private
private
def other_methods
end def other_methods
end end
end
Service Can Call Other Services
class CalculateOrderTotal
def initialize(order)
@order = order
end
def perform
# Reuse a class, hide complexity, can be tested easily
# By mocking the value.
# Fetching and processing promotions data.
discount = CalculateOrderDiscount.new(order).perform
# Fetching and processing tax data.
tax = CalculateOrderTax.new(order).perform
# Feature envy code smell, expected.
(@order.items_total - discount) + tax
end
end
Dependency
Injection
Improve code reusability with
interface / duck-typing.
Dependency Injection
● Involving Interface or duck-typing.
○ Ruby code depends on duck-typing
● Interface requires all implementing classes to implement all of the
methods.
● One class may implements multiple interfaces/behaviours.
● Decouple software components.
○ Making changes and extensions easier.
○ Open for extensions closed for modifications.
● Composition over inheritance.
Pseudo-Interface in Ruby
class ProductDisplayable
ProductDisplayable is a behaviour.
def name
raise 'must implement this' This can be used to display any product
end we want to sell. Very common in e-commerce.
def price
raise 'must implement this'
But products can be anything. From car, food to
end insurance, it’s also a good idea to create separate
class to implement the interface (CarDisplay,
def discount
FoodDisplay, InsuranceDisplay). They
raise 'must implement this'
end might come from different table or even different
database / API.
def image_location
raise 'must implement this'
end
Adding new type of product (from different
database / API) wouldn’t be much of problem.
def link Just add another Display class, without changing
raise 'must implement this'
end
the existing codebase.
end
Implementing the Pseudo-Interface
# From Database # From API
class FoodDisplay < ProductDisplayable class CarDisplay < ProductDisplayable
def initialize(food) def initialize(car)
@food = food @car = car
end end
def name def name
@food.name @car[:data][:name]
end end
# ... other methods # ... other methods
end end
Duck-Typing
class PromotedProductsPresenter
def initialize(product_displayables)
@product_displayables = product_displayables
end
# Use duck-typing to deal with presenting data. Let say we use this
def index_present
presenter to show promoted
@product_displayables.map do |product|
products on a page.
{
name: product.name,
price: product.price,
discount: product.discount,
image_location: product.image_location,
link: product.link
}
end
end
end
Usage
# Assume that these objects has implemented all of interface methods.
products = [apel, car1, insurance1, ticket_promo_to_japan_from_partner,
ice_cream, kfc_voucher, mobile_legends_diamonds_coupon
]
data = PromotedProductsPresenter.new(products).present
# then send to serve API request...
Got new products to be presented using this presenter? Just implement the interface.
Interface usually defines behaviours, like: Taxable, Displayable, Shoppable, Eatable, Cacheable,
Loggable, etc.
Policy Object
Isolate complex policy logic
(permissions, roles, etc).
Policy Object
● Using pundit gem to regulate policies/authorization.
● Keep it clean and simple.
● If policy object becomes increasingly complex again, extract private
methods to reusable service classes.
● Policy only reads data. Policy has no place to create / update data. ⚠
○ If you find yourself create / update data in policy classes, stop it, get some help.
● Authorization error should return error 403.
Policy Object (continued...)
● Do not place web params into policy classes.
● You can use policy object inside form object if you want, do dependency
injection as well if you want.
● Policy object should authorize the actor (user) only.
○ When authorizing actions related to order, payment, approval, etc, validate only the user
who makes it (the one who acts on it).
○ Not the order expiry date, the payment’s amount, etc. Don’t do this in authorization.
● Policy / Authorization != Validation ⚠
○ Authorization = who are you? (you are not the roles) => error 403
○ Validation = how do you do that? (you are already authorized, but data is wrong/invalid)
=> error 422
○ If you place validation in authorization, stop it, get some help.
Trivia Conversation
● User: “I want to enter this warehouse … I Want to deal an order, and arrange
some stocks there… ”
● Security: “Sure, I need to authorize your entrance, can I see your ID or badge?”
● User: “Sure, here it is … ” (hand over the badge)
● Security: “Uhmm … okay, let me check on the database about your ID… Wait a
minute … Okay our database says that you are warehouse manager, you can
proceed to enter the warehouse.”
● User: “Btw, Do you think I have enough amount of stocks to proceed with ORDER
ID 76431 ?? Oh can you check if this customer is legit ?? Not having fraud before
?? Oh is the amount is valid though ? Does customer have enough balance ??”
● Security: “ … 😓 Sir, I apologize… , that’s not my job … ”
Example (Don’t do this...)
class OrderPolicy class MoneyTransferPolicy
def initialize(user, order,
seller, payment) # source and destination objects
# ... # are from the same class.
def initialize(source, destination, transaction)
end
# ...
end
def create?
legit_user? def create?
legit_order? allowed_source?
legit_payment? allowed_destination?
legit_seller? legit_transaction?
legit_transaction? legit_source_account?
legit_destination_account?
end
validated_amount?
create_terminal_log
private end
Don’t do this.
# ... Place this in private Don’t do this.
end validation. Place this in validation.
# ...
end Don’t create/update
any data.
Decorator Pattern
Add behaviour to objects, without
changing other objects of the same
class.
Decorator Pattern
● By using SimpleDelegator.
● Add certain behaviours for specific case, that we don’t want to add them to
the class.
○ Instead of updating, we’re extending it.
● Create decorator class.
● Pass the object to decorator class.
● Depend on duck-typing if we want to reuse the decorator class.
● Alternative to sub-classing.
● “Hi I want you to decorate this car for me, don’t copy it, just decorate it, add
paint, add custom exhaust, add sticker, etc.”
Example
class ProductDisplayableDecorator < SimpleDelegator SimpleDelegator is a ruby class, that
def barcode_url can be used to delegate all of methods to
"sample_barcode_#{url}" object passed into the contructor.
end
car and displayable_car are different,
def promo_image_with_referrer car has no displayable_car’s
add_referrer_tag_to_promo_image(promo_image) methods, but displayable_car has
end car’s methods.
end
# Assume that both car and noodle have
# url and promo_image methods.
Get the delegated object with (provided
# They don't need to be from the same class.
by SimpleDelegator):
displayable_car = ProductDisplayableDecorator.new(car)
displayable_car.barcode_url
displayable_car.__getobj__.url
displayable_noodle = ProductDisplayableDecorator.new(noodle)
displayable_noodle.barcode_url
Builder Pattern
Creational pattern for composing
object with many possible
parameters.
Builder Pattern
● “Separate the construction of a complex object from its representation so
that the same construction process can create different representations.”
-Gang of Four
● Good for building object with numerous amount of parameters (that can
be optional).
● Be very careful with method prefixes add_ and set_. Add means to add
something to the object (ex: parts, components, ingredients), set means to
set something on an object (ex: gender, birthday, etc).
● Can be applied to build ActiveRecord object, just make sure to adapt with
the validations and DB constraints.
Example
class Burger def set_cheese_amount(cheese = 1)
attr_accessor :bun, :tomato, :cheese, @burger.cheese = cheese
:patty, :lettuce, :sauce self
end end
def set_patty(patty = 'beef')
class BurgerBuilder @burger.patty = patty
attr_reader :burger self
def initialize end
@burger = Burger.new
self def set_lettuce
end @burger.lettuce = "yes"
self
end
def set_bun(bun = 'regular')
@burger.bun = bun def set_sauce(sauce = 'hot sauce')
self @burger.sauce = sauce
end self
end
def set_tomato_amount(tomato = 1)
@burger.tomato = tomato def build!
self self.burger
end
end
end
Usage
chicken_teriyaki_burger = BurgerBuilder
.new
.set_bun('flat')
.set_cheese_amount(2)
.set_patty('double chicken')
.set_tomato_amount(1)
.set_sauce('teriyaki')
.build!
breakfast_burger = BurgerBuilder
.new
.set_bun('eggs')
.set_cheese_amount(3)
.set_patty('beef')
.set_sauce('tomato')
.build!
Any Questions?