From Rails-way to modular architecture

67
From Rails-way to modular architecture Ivan Nemytchenko @inemation Ivan Nemytchenko - @inemation

Transcript of From Rails-way to modular architecture

From Rails-wayto 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 identification

• Tesing best practices

Ivan Nemytchenko - @inemation

2. Knowing about something doesn't

mean you know how to apply it

Ivan Nemytchenko - @inemation

The storyIvan 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_idend

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 :assetend

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::Validationsend

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_idend

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 objectsPro: 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 endend

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 casePro: not mixing responsobilitiesPro: not depending on ActionControllerCon: 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.firstend

Ivan Nemytchenko - @inemation

Domain-specific objects

• User

• Profile

• Image

• ImageVariant

• ImageSettings

Ivan Nemytchenko - @inemation

Domain-specific objects

• User

• Profile

• Image

• ImageVariant

• ImageSettings

Ivan Nemytchenko - @inemation

Domain-specific objects

• User

• Profile

• Image• ImageVariant• ImageSettings

Ivan Nemytchenko - @inemation

Repositories + Entities

Ivan Nemytchenko - @inemation

Entitiesclass Entity include Virtus.model def new_record? !id endend

class User < Entity attribute :id, Integer attribute :username, String

attribute :profiles, Array[Profile] attribute :roles, Arrayend

Ivan Nemytchenko - @inemation

Repositories

Ivan Nemytchenko - @inemation

Repositoriesdef 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 endend

Ivan Nemytchenko - @inemation

Repositoriesdef 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) endend

Ivan Nemytchenko - @inemation

Ivan Nemytchenko - @inemation

Ivan Nemytchenko - @inemation

Repositories/EntitiesCon: There's no AR magic anymoreCon: Had to write a lot of low-level codePro: You have control on what's happeningPro: Messy DB structure doesn't affect appPro: DDD!?

Ivan Nemytchenko - @inemation

Side effectRABL

Ivan Nemytchenko - @inemation

Presentersmodule ProfilePresenter def self.wrap_one!(obj, viewer) hash = obj.to_hash hash.delete(:secret_field) unless viewer.admin? hash endend

Ivan Nemytchenko - @inemation

Presentersdef 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

SequelVirtus

Ivan Nemytchenko - @inemation

Rom.rbLotus

Ivan Nemytchenko - @inemation

Arkencyblog.arkency.com

Adam Hawkinshawkins.io

Ivan Nemytchenko - @inemation

Ivan Nemytchenko - @inemation