От rails-way к модульной архитектуре

60
Fr om r ails-way to modular ar chitectur e Ivan Nemytchenko, independent consultant DevConf 2014 @inem, @inema tion

Upload: ivan-nemytchenko

Post on 27-Jan-2015

144 views

Category:

Internet


2 download

DESCRIPTION

 

TRANSCRIPT

Page 1: От Rails-way к модульной архитектуре

From rails-way to modular architecture

Ivan Nemytchenko, independent consultant

DevConf 2014

@inem, @inemation

Page 2: От Rails-way к модульной архитектуре

Icon made by <a href="http://www.freepik.com" alt="Freepik.com">Freepik</a>from <a href="http://www.flaticon.com/free-icon/graduate-cap_46045">flaticon.com</a>

Icon made by Icons8 from <a href="http://www.flaticon.com">flaticon.com</a>

Page 3: От Rails-way к модульной архитектуре
Page 4: От Rails-way к модульной архитектуре

4 bit.ly/rails-community

Page 5: От Rails-way к модульной архитектуре
Page 6: От Rails-way к модульной архитектуре

!"---→----→----→----→----#

Page 7: От Rails-way к модульной архитектуре

Rails-way

Page 8: От Rails-way к модульной архитектуре

Модульная архитектура - это что?4 изменения в коде делать легко4 код легко переиспользовать4 код легко тестировать

Page 9: От Rails-way к модульной архитектуре

Чем плох rails-way?

Page 10: От Rails-way к модульной архитектуре

Single Responsibility Principle

- Не, не слышали

Page 11: От Rails-way к модульной архитектуре

Skinny controllers, fat models.

ORLY?

Page 12: От Rails-way к модульной архитектуре

Conventions over configuration

DB ⇆ Forms

Page 13: От Rails-way к модульной архитектуре

Проект4 Монолитное приложение на Grails

4 База данных на 70 таблиц4 Мы упоролись

Page 14: От Rails-way к модульной архитектуре

Решение заказчика4 Фронтенд на AngularJS

4 Бэкэнд на рельсе для отдачи API

Page 15: От Rails-way к модульной архитектуре

Ок, rails, значит rails

Точнее, rails-api

Page 16: От Rails-way к модульной архитектуре

Модели class ImageSettings < ActiveRecord::Base end

class Profile < ActiveRecord::Base self.table_name = 'profile' belongs_to :image, foreign_key: :picture_id end

Page 17: От Rails-way к модульной архитектуре

Модели 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

Page 18: От Rails-way к модульной архитектуре

RABL - github.com/nesquena/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

Page 19: От Rails-way к модульной архитектуре

!-"--→----→----→----→----#

Page 20: От Rails-way к модульной архитектуре

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 unless ['regular', 'paying'].include?(recipient.type) render json: {error: 'Incorrect user type'}, 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 and return else render json: {error: 'Error during transaction'}, status: 500 and return end end end

Page 21: От Rails-way к модульной архитектуре
Page 22: От Rails-way к модульной архитектуре

def redeem begin recipient_balance = ?????????? rescue BonuscodeNotFound, BonuscodeIsAlreadyUsed, RecipientNotFound => ex render json: {error: ex.message}, status: 404 and return rescue IncorrectRecipientType => ex render json: {error: ex.message}, status: 403 and return rescue TransactionError => ex render json: {error: ex.message}, status: 500 and return end

render json: {balance: recipient_balance} end

Page 23: От Rails-way к модульной архитектуре

bonuscode.redeem(user)или

user.redeem_bonuscode(code)?

Page 24: От Rails-way к модульной архитектуре

Service/Use case: 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 IncorrectRecipientType => ex render json: {error: ex.message}, status: 403 and return rescue TransactionError => ex render json: {error: ex.message}, status: 500 and return end

render json: {balance: recipient_balance} end

Page 25: От Rails-way к модульной архитектуре

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? raise IncorrectRecipientType.new unless correct_user_type?(recipient.type)

ActiveRecord::Base.transaction do amount = bonuscode.redeem!(recipient_id) recipient.increase_balance!(amount) recipient.save! && bonuscode.save! end recipient.balance end

private ... end

Page 26: От Rails-way к модульной архитектуре

!---→-"---→----→----→----#

Page 27: От Rails-way к модульной архитектуре

Incoming!!!

4 В таблице "users" по факту хранятся разные типы пользователей и для них нужны разные правила валидации

4 Это как минимум

Page 28: От Rails-way к модульной архитектуре

Themis - modular and switchable validations for ActiveRecord models class User < ActiveRecord::Base has_validation :admin, AdminValidation has_validation :enduser, EndUserValidation ...

user.use_validation(:enduser) user.valid? # => true

4 github.com/TMXCredit/themis

Page 29: От Rails-way к модульной архитектуре

module EndUserValidation extend Themis::Validation

validates_presence_of :username, :password, :email validates :password, length: { minimum: 6 } validates :email, email: true end

Page 30: От Rails-way к модульной архитектуре

!---→---"-→----→----→----#

Page 31: От Rails-way к модульной архитектуре

Form objects (Inputs)4 Plain old ruby objects*

class BonuscodeRedeemInput < Input attribute :hash_code, String attribute :receptor_id, Integer

validates_presence_of :hash_code, :receptor_id validates_numericality_of :receptor_id end

Page 32: От Rails-way к модульной архитектуре

Form objects (Inputs) class Input include Virtus.model include ActiveModel::Validations

class ValidationError < StandardError; end

def validate! raise ValidationError.new, errors unless valid? end end

Page 33: От Rails-way к модульной архитектуре

!---→----→---"-→----→----#

Page 34: От Rails-way к модульной архитектуре

Incoming!Заказчику нужен небольшой сервис сбоку mkdir sinatra-app cd sinatra-app git init .

Page 35: От Rails-way к модульной архитектуре

class SenderApp < Sinatra::Base get '/sms/create/:message/:number' do input = MessageInput.new(params) sender = TwilioMessageSender.new('xxxxxx', 'xxxxxx', '+15005550006') use_case = SendSms.new(input, sender)

begin use_case.run! rescue ValidationError => ex status 403 and return ex.message rescue SendingError => ex status 500 and return ex.message end status 201 end end

Page 36: От Rails-way к модульной архитектуре

Sinatra4 встраивается в рельсовые роуты или запускается в связке с другими rack-приложениями

4 рельсовые роуты оказались ненужной абстракцией

4 отчего бы не применить такой подход для всего приложения?!

Page 37: От Rails-way к модульной архитектуре

!---→----→----→--"--→----#

Page 38: От Rails-way к модульной архитектуре

А как же разделение бизнес-логики и хранения данных?4 Очень хочется но непонятно как4 Репозитории?

Page 39: От Rails-way к модульной архитектуре

Entities

Page 40: От Rails-way к модульной архитектуре

Entities - зачем?statuses profiles-------- --------id idname status_id

profile.status.name

Page 41: От Rails-way к модульной архитектуре

Entities4 Plain old ruby objects*

class User < Entity attribute :id, Integer attribute :username, String attribute :max_profiles, Integer, default: 3

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

Page 42: От Rails-way к модульной архитектуре

Entities4 Plain old ruby objects*

class Entity include Virtus.model

def new_record? !id end end

Page 43: От Rails-way к модульной архитектуре

Repositories

Page 44: От Rails-way к модульной архитектуре

Repositories - чем занимаются?4 найди мне объект с таким-то id

4 дай мне все объекты по такому-то условию

4 сохрани вот эти данные в виде объекта

Page 45: От Rails-way к модульной архитектуре

AR-flavored repositories4 под капотом все те же ActiveRecord модели

4 репозиторий отдает инстансы Entities

Page 46: От Rails-way к модульной архитектуре

Первая попытка:

AR-flavored repositoriesNEVER AGAIN!

Page 47: От Rails-way к модульной архитектуре

Sequel FTW!sequel. jeremyevans.net

4 DSL для построения SQL-запросов4 реализация паттерна ActiveRecord

Page 48: От Rails-way к модульной архитектуре

Repositories def find(id) 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

Page 49: От Rails-way к модульной архитектуре

Repositoriesdef find(id) to_add = [:gender__name___gender, :profile_status__name___status, :picture_id___image_id]

dataset = table.join(:profile_status, id: :profile_status_id) .join(:gender, id: :profile__gender_id) .select_all(:profile).select_append(*to_add) .where(profile__id: id)

all = dataset.all.map do |record| Profile.new(record) end all.size > 1 ? all : all.firstend

Page 50: От Rails-way к модульной архитектуре

Repositoriesdef persist(profile) status_id = DB[:online_profile_status].select(:id).where(name: profile.status).get(:id)

gender_id = DB[:gender].select(:id).where(name: profile.gender).get(:id)

hash = { username: profile.username, profile_status_id: status_id, gender_id: gender_id, picture_id: profile.image_id }

if profile.new_record? dates = { date_created: Time.now.utc, last_updated: Time.now.utc } profile.id = table.insert(hash.merge! dates) else table.where(id: profile.id).update(hash) endend

Page 51: От Rails-way к модульной архитектуре

Repositoriesclass ImageRepo class ImageData < Sequel::Model set_dataset DB[:image].join(:asset, asset__id: :image__asset_id) many_to_many :images, join_table: :image_image, left_key: :image_id, right_key: :image_images_id, class: self end...

Page 52: От Rails-way к модульной архитектуре

Benefits?

Page 53: От Rails-way к модульной архитектуре

!---→----→----→----→---"-#

Page 54: От Rails-way к модульной архитектуре

Presenters

Page 55: От Rails-way к модульной архитектуре

class ProfilePresenter def initialize(p, viewer) ...

def wrap! hash = { id: p.id, deleted: p.deleted, username: p.username, is_online: p.is_online } hash[:user_id] = p.user_id if viewer.admin? hash[:image] = p.image.to_hash if add_image? hash end

def self.wrap!(profiles, viewer) profiles.map do |profile| new(profile, viewer).wrap! end end ...end

Page 56: От Rails-way к модульной архитектуре

Presenters post '/profile' do begin use_case = CreateProfile.new(current_user) profile = use_case.run!(params) rescue Input::ValidationError => ex halt 403 end

wrapped_profiles = ProfilePresenter.wrap!([profile], current_user) json(data: wrapped_profiles) end

Page 57: От Rails-way к модульной архитектуре

!---→----→----→----→----"#

Page 58: От Rails-way к модульной архитектуре

Hexagonal/Clean Architecture

Page 59: От Rails-way к модульной архитектуре
Page 60: От Rails-way к модульной архитектуре

From rails-way to modular architecture

Ivan Nemytchenko, independent consultant

DevConf 2014

@inem, @inemation