ºÝºÝߣ

ºÝºÝߣShare a Scribd company logo
From Rails-way
to modular architecture
Ivan Nemytchenko ¡ª @inemation
Ivan Nemytchenko - @inemation
Me:
? Came to Rails from PHP
(long time ago)
? co-founded two agencies
? co-organized two conferneces
? did a lot of management stuff
? currently freelancer
Ivan Nemytchenko - @inemation
Idea
+
RailsRumble
¡ý
tequila gem
(HAML for JSON)
Ivan Nemytchenko - @inemation
tequila gem
¡ý
inspiration for
@nesquena
¡ý
gem RABL
Ivan Nemytchenko - @inemation
Rails is awesome,
isn't it?
Ivan Nemytchenko - @inemation
Same pattern
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
1. Knowing how to
do it in a Rails-way is
not enough.
Ivan Nemytchenko - @inemation
These things might help you
? SOLID principles
? Design Pattrens
? Refactoring techniques
? Architecture types
? Code smells identi?cation
? Tesing best practices
Ivan Nemytchenko - @inemation
2. Knowing about
something doesn't
mean you know how
to apply it
Ivan Nemytchenko - @inemation
The story
Ivan Nemytchenko - @inemation
Client
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Problem:
? Current team stuck
Ivan Nemytchenko - @inemation
Problem:
? Current team stuck
Project:
? Monolythic Grails app
? Mysql database of ~70 tables
Ivan Nemytchenko - @inemation
Solution
? Split frontend and backend
? AngularJS for frontend
? Rails for API
Ivan Nemytchenko - @inemation
Solution
? Split frontend and backend
? AngularJS for frontend
? Rails for API
? Keeping DB structure
Ivan Nemytchenko - @inemation
Ok, lets do it!
Ivan Nemytchenko - @inemation
Accessing data (AR models)
class Profile < ActiveRecord::Base
self.table_name = 'profile'
belongs_to :image, foreign_key: :picture_id
end
Ivan Nemytchenko - @inemation
Accessing data (AR models)
class Image < ActiveRecord::Base
self.table_name = 'image'
has_and_belongs_to_many :image_variants,
join_table: "image_image", class_name: "Image",
association_foreign_key: :image_images_id
belongs_to :settings,
foreign_key: :settings_id, class_name: 'ImageSettings'
belongs_to :asset
end
Ivan Nemytchenko - @inemation
Presenting data (RABL)
collection @object
attribute :id, :deleted, :username, :age
node :gender do |object|
object.gender.to_s
end
node :thumbnail_image_url do |obj|
obj.thumbnail_image.asset.url
end
node :standard_image_url do |obj|
obj.standard_image.asset.url
end
Ivan Nemytchenko - @inemation
Let's implement some features!
Feature #1: Users Registration
Ivan Nemytchenko - @inemation
Let's implement some features!
Feature #1: Users Registration
Ivan Nemytchenko - @inemation
Conditional validations?
Anyone?
Ivan Nemytchenko - @inemation
Our model knows about..
? How user data is saved
? How admin form is validated
? How org_user form is validated
? How guest_user form is validated
Ivan Nemytchenko - @inemation
Singe responsibility principle:
A class should have only one
reason to change
Ivan Nemytchenko - @inemation
Form object (Input)
class Input
include Virtus.model
include ActiveModel::Validations
end
class OrgUserInput < Input
attribute :login, String
attribute :password, String
attribute :password_confirmation, String
attribute :organization_id, Integer
validates_presence_of :login, :password, :password_confirmation
validates_numericality_of :organization_id
end
Ivan Nemytchenko - @inemation
Using form object (Input)
input = OrgUserInput.new(params)
if input.valid?
@user = User.create(input.to_hash)
else
#...
end
Ivan Nemytchenko - @inemation
Form objects
Pro: We have 4 simple objects instead of one complex.
Con: You might have problems in case of nested forms.
Ivan Nemytchenko - @inemation
Let's implement more features!
Feature #2: Bonuscode redeem
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
def redeem
unless bonuscode = Bonuscode.find_by_hash(params[:code])
render json: {error: 'Bonuscode not found'}, status: 404 and return
end
if bonuscode.used?
render json: {error: 'Bonuscode is already used'}, status: 404 and return
end
unless recipient = User.find_by_id(params[:receptor_id])
render json: {error: 'Recipient not found'}, status: 404 and return
end
ActiveRecord::Base.transaction do
amount = bonuscode.mark_as_used!(params[:receptor_id])
recipient.increase_balance!(amount)
if recipient.save && bonuscode.save
render json: {balance: recipient.balance}, status: 200
else
render json: {error: 'Error during transaction'}, status: 500
end
end
end
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
bonuscode.redeem_by(user)
or
user.redeem_bonus(code)
?
Ivan Nemytchenko - @inemation
Service/Use case:
class RedeemBonuscode
def run!(hashcode, recipient_id)
raise BonuscodeNotFound.new unless bonuscode = find_bonuscode(hashcode)
raise RecipientNotFound.new unless recipient = find_recipient(recipient_id)
raise BonuscodeIsAlreadyUsed.new if bonuscode.used?
ActiveRecord::Base.transaction do
amount = bonuscode.redeem!(recipient_id)
recipient.increase_balance!(amount)
recipient.save! && bonuscode.save!
end
recipient.balance
end
end
Ivan Nemytchenko - @inemation
Using service:
def redeem
use_case = RedeemBonuscode.new
begin
recipient_balance = use_case.run!(params[:code], params[:receptor_id])
rescue BonuscodeNotFound, BonuscodeIsAlreadyUsed, RecipientNotFound => ex
render json: {error: ex.message}, status: 404 and return
rescue TransactionError => ex
render json: {error: ex.message}, status: 500 and return
end
render json: {balance: recipient_balance}
end
Ivan Nemytchenko - @inemation
Services/Use case
Pro: not mixing responsobilities
Pro: not depending on ActionController
Con: using exceptions for Flow Control is slow
Ivan Nemytchenko - @inemation
Whole picture: before
Ivan Nemytchenko - @inemation
Whole picture: after
Ivan Nemytchenko - @inemation
Are we happy now?
Ivan Nemytchenko - @inemation
Not really happy
if image = profile.image
images = [image] + image.image_variants
original = images.select do |img|
img.settings.label == 'Profile'
end.first
resized = images.select do |img|
img.settings.label == 'thumbnail'
end.first
end
Ivan Nemytchenko - @inemation
Domain-specific
objects
? User
? Pro?le
? Image
? ImageVariant
? ImageSettings
Ivan Nemytchenko - @inemation
Domain-specific
objects
? User
? Pro?le
? Image
? ImageVariant
? ImageSettings
Ivan Nemytchenko - @inemation
Domain-specific
objects
? User
? Pro?le
? Image
? ImageVariant
? ImageSettings
Ivan Nemytchenko - @inemation
Repositories + Entities
Ivan Nemytchenko - @inemation
Entities
class Entity
include Virtus.model
def new_record?
!id
end
end
class User < Entity
attribute :id, Integer
attribute :username, String
attribute :profiles, Array[Profile]
attribute :roles, Array
end
Ivan Nemytchenko - @inemation
Repositories
Ivan Nemytchenko - @inemation
Repositories
def find(id)
if dataset = table.select(:id, :username, :enabled,
:date_created, :last_updated).where(id: id)
user = User.new(dataset.first)
user.roles = get_roles(id)
user
end
end
Ivan Nemytchenko - @inemation
Repositories
def find(id)
dataset = table.join(:profile_status, id: :profile_status_id)
.join(:gender, id: :profile__gender_id)
.select_all(:profile).select_append(*extra_fields)
.where(profile__id: id)
dataset.all.map do |record|
Profile.new(record)
end
end
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Repositories/Entities
Con: There's no AR magic anymore
Con: Had to write a lot of low-level code
Pro: You have control on what's happening
Pro: Messy DB structure doesn't affect app
Pro: DDD!?
Ivan Nemytchenko - @inemation
Side effect
RABL
Ivan Nemytchenko - @inemation
Presenters
module ProfilePresenter
def self.wrap_one!(obj, viewer)
hash = obj.to_hash
hash.delete(:secret_field) unless viewer.admin?
hash
end
end
Ivan Nemytchenko - @inemation
Presenters
def show
profile = ProfileRepository.find(params[:id])
hash = ProfilePresenter.wrap_one!(profile, current_user)
render json: {profile: hash}
end
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation
Not depending on Rails anymore!
Ivan Nemytchenko - @inemation
Sequel
Virtus
Ivan Nemytchenko - @inemation
Rom.rb
Lotus
Ivan Nemytchenko - @inemation
Arkency
blog.arkency.com
Adam Hawkins
hawkins.io
Ivan Nemytchenko - @inemation
Ivan Nemytchenko - @inemation

More Related Content

From Rails-way to modular architecture