11Apr, 2020
Language blog :
English
Share blog : 
11 April, 2020
English

Service Objects with ActiveModel and SimpleDelegator

By

5 mins read
Service Objects with ActiveModel and SimpleDelegator

One of the worst practices in Ruby On Rails is to clutter the Gemfile to do basics as authentication, authorization, validation, etc. considering the fact that they are practiced for so long and they can be accomplished using just the batteries already included in Ruby On Rails; service objects are no different for which developers often end up using different gems to do such a simple pattern.

In the favour of simplicity and the fact that I never trust dependencies due to the complexity they bring in I’ve been using ActiveModel and SimpleDelegator to apply service objects through a consistent interface across the controllers as well as avoiding dependency clutter and here are the results.

Login case

Let’s consider a user logging in use case;

> Users can login with password and email> Users cannot login with wrong password / email

So we have 2 cases which we have to cover in our implementation to do that I use ActiveModel::Validations to validate the parameters I receive from the controller and using save method to keep interface consistency.

class Login  include ActiveModel::Model  attr_accessor :email, :password, :user  validates :email, presence: true  validates :password, presence: true  validates :user, presence: {message: "email or password is wrong"}  def initialize(params)    super(params.require(:user).permit(:email, :password))    @user = User.find_by(email: email, status: :active)&.authenticate(password)  end  def save    return if invalid?    # This will generate and refresh user's token and return it    Tokenizer.generate_and_refresh!(@user)  end
end

Here if no user found it will set @user variable to nil and user presence validation will handle the error which keeps us from writing an error handling part except a message.

And the controller would be like;

class LoginController < ApplicationController  skip_before_action :authenticate  INCLUDED = %i[positions]  def create    login = Login.new(params)    if (user = login.save)      render json: user, include: INCLUDED    else      render json: login, status: :bad_request    end  end  private  def serializer    UserSerializer  end  def user_params    params.require(:user).permit(:email, :password)  end
end

This is already battle-tested in production and it works like a charm.

Approval flow

Let’s consider a use case that your flow needs to go through multiple approvals and each approval stage has its own validations and business logic. There are a lot of ways to handle this;

ActiveRecord callbacks

It may seem a simple solution but you have to write tons of conditions to avoid conflict between different use cases and hard to debug.

Contextual validations with on: :context

This works just fine but it’ll clutter your models with a lot of business logic which it needs to somewhere else and it’s hard to test and modify due to it’s coupled with your model. e.g.

validate :order_is_picked_before_packing, on: :packing

Decorators + State machines

State machines need to be force-fed to developers as they make handling data logic so convenient but to provide a good interface for your controllers you cannot treat state machine transitions or guard errors as you best bet you still need something in-between which in this case is a decorator which give you best of both worlds validations, callbacks, separation of concern, ease of testing and etc. In our case we need some conditions are met before going to another state so the uses cases are;

> MVPs cannot be approved before they're submitted> MVPs cannot be approved more than once by same user> MVPs cannot be approved unless they have enough approvals from different users

Let’s take look at the code;

class ApprovedMvp < SimpleDelegator  include ActiveModel::Validations  def save(user)    return unless valid?    if in_state?(:draft)      errors.add(:minimal_viable_product, "it should be submitted first")      return    end    if approvals.find_by(user: user)      errors.add(:minimal_viable_product, "already been approved by user")      return    else      approvals.create!(user: user)    end    if has_enough_approvals?      transition_to!(:approved)    else      super()    end  end
end

Here we populate a decorated MVP with contextual errors and pass it up to the controller

class UseCases::MinimalViableProductsController < ApplicationController  INCLUDED = UseCasesController::INCLUDED  include UseCaseScoped  def approve    result = ApprovedMvp.new(minimal_viable_product)    if result.save(@current_user)      render json: result.use_case, include: INCLUDED    else      render json: result    end  end

This will be handled by a ErrorSerializer and a concern called ActsAsJSONAPI which I’ve written to handle different cases of serialization and error handling without dependencies and a lot of complexity which you can check out here.

Conclusion

Codes that are written 50 years ago with zero-dependencies are working fine today and the ones are written today with dependencies may not work tomorrow :)

And don’t get me wrong I don’t mean to reinvent the wheel but don’t be afraid to build your own tool since each app has different needs and not all gems/libraries are going to fit yours. Bad cases that you should never implement by yourself except for practice are encryption or a premature abstraction (framework, etc) that takes your more than 1 day or 2.

I hope you’ve enjoyed it

Written by
Senna Labs
Senna Labs

Subscribe to follow product news, latest in technology, solutions, and updates

- More than 120,000 people/day visit to read our blogs

Other articles for you

12
November, 2024
JS class syntax
12 November, 2024
JS class syntax
เชื่อว่าหลายๆคนที่เขียน javascript กันมา คงต้องเคยสงสัยกันบ้าง ว่า class ที่อยู่ใน js เนี่ย มันคืออะไร แล้วมันมีหน้าที่ต่างกับการประกาศ function อย่างไร? เรามารู้จักกับ class ให้มากขึ้นกันดีกว่า class เปรียบเสมือนกับ blueprint หรือแบบพิมพ์เขียว ที่สามารถนำไปสร้างเป็นสิ่งของ( object ) ตาม blueprint หรือแบบพิมพ์เขียว( class ) นั้นๆได้ โดยภายใน class

By

4 mins read
Thai
12
November, 2024
15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G
12 November, 2024
15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G
ผู้ให้บริการเครือข่ายในสหรัฐฯ ได้เปิดตัว 5G ในหลายรูปแบบ และเช่นเดียวกับผู้ให้บริการเครือข่ายในยุโรปหลายราย แต่… 5G มันคืออะไร และทำไมเราต้องให้ความสนใจ บทความนี้ได้รวบรวม 15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G เพราะเราปฏิเสธไม่ได้เลยว่ามันกำลังจะถูกใช้งานอย่างกว้างขวางขึ้น 1. 5G หรือ Fifth-Generation คือยุคใหม่ของเทคโนโลยีเครือข่ายไร้สายที่จะมาแทนที่ระบบ 4G ที่เราใช้อยู่ในปัจจุบัน ซึ่งมันไม่ได้ถูกจำกัดแค่มือถือเท่านั้น แต่รวมถึงอุปกรณ์ทุกชนิดที่เชื่อมต่ออินเตอร์เน็ตได้ 2. 5G คือการพัฒนา 3 ส่วนที่สำคัญที่จะนำมาสู่การเชื่อมต่ออุปกรณ์ไร้สายต่างๆ ขยายช่องสัญญาณขนาดใหญ่ขึ้นเพื่อเพิ่มความเร็วในการเชื่อมต่อ การตอบสนองที่รวดเร็วขึ้นในระยะเวลาที่น้อยลง ความสามารถในการเชื่อมต่ออุปกรณ์มากกว่า 1 ในเวลาเดียวกัน 3. สัญญาณ 5G นั้นแตกต่างจากระบบ

By

4 mins read
Thai
12
November, 2024
จัดการ Array ด้วย Javascript (Clone Deep)
12 November, 2024
จัดการ Array ด้วย Javascript (Clone Deep)
ในปัจจุบันนี้ ปฏิเสธไม่ได้เลยว่าภาษาที่ถูกใช้ในการเขียนเว็บต่าง ๆ นั้น คงหนีไม่พ้นภาษา Javascript ซึ่งเป็นภาษาที่ถูกนำไปพัฒนาเป็น framework หรือ library ต่าง ๆ มากมาย ผู้พัฒนาหลายคนก็มีรูปแบบการเขียนภาษา Javascript ที่แตกต่างกัน เราเลยมีแนวทางการเขียนที่หลากหลาย มาแบ่งปันเพื่อน ๆ เกี่ยวกับการจัดการ Array ด้วยภาษา Javascript กัน เรามาดูตัวอย่างกันเลยดีกว่า โดยปกติแล้วการ copy ค่าจาก value type ธรรมดา สามารถเขียนได้ดังนี้

By

4 mins read
Thai

Let’s build digital products that are
simply awesome !

We will get back to you within 24 hours!Go to contact us
Please tell us your ideas.
- Senna Labsmake it happy
Contact ball
Contact us bg 2
Contact us bg 4
Contact us bg 1
Ball leftBall rightBall leftBall right
Sennalabs gray logo28/11 Soi Ruamrudee, Lumphini, Pathumwan, Bangkok 10330+66 62 389 4599hello@sennalabs.com© 2022 Senna Labs Co., Ltd.All rights reserved.