It is common for rails-teams to stuck at some point. It happens when team doesn't try to manage complexity of their app.
In this talk I demonstrated my path from a Rails-application to somewhat modular architecture app: things like Form Objects, Repositories, Entities etc. emerged naturally, as a solutions for existing problems, not because we were told by someone to use them.
March 15, Wroclaw, wroc_love.rb 2015
2. 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
11. 1. Knowing how to
do it in a Rails-way is
not enough.
Ivan Nemytchenko - @inemation
12. These things might help you
? SOLID principles
? Design Pattrens
? Refactoring techniques
? Architecture types
? Code smells identi?cation
? Tesing best practices
Ivan Nemytchenko - @inemation
23. Accessing data (AR models)
class Profile < ActiveRecord::Base
self.table_name = 'profile'
belongs_to :image, foreign_key: :picture_id
end
Ivan Nemytchenko - @inemation
24. 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
25. 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
26. Let's implement some features!
Feature #1: Users Registration
Ivan Nemytchenko - @inemation
27. Let's implement some features!
Feature #1: Users Registration
Ivan Nemytchenko - @inemation
29. 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
31. 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
32. Using form object (Input)
input = OrgUserInput.new(params)
if input.valid?
@user = User.create(input.to_hash)
else
#...
end
Ivan Nemytchenko - @inemation
33. 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
34. Let's implement more features!
Feature #2: Bonuscode redeem
Ivan Nemytchenko - @inemation
37. 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
40. 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
41. 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
42. Services/Use case
Pro: not mixing responsobilities
Pro: not depending on ActionController
Con: using exceptions for Flow Control is slow
Ivan Nemytchenko - @inemation
51. 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
53. 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
54. 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
57. 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