Окружение разработчика - от виртуализации к контейнеризации
От Rails-way к модульной архитектуре
-
Upload
ivan-nemytchenko -
Category
Internet
-
view
136 -
download
2
description
Transcript of От Rails-way к модульной архитектуре
![Page 1: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/1.jpg)
From rails-way to modular architecture
Ivan Nemytchenko, independent consultant
DevConf 2014
@inem, @inemation
![Page 2: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/2.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/3.jpg)
![Page 5: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/5.jpg)
![Page 6: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/6.jpg)
!"---→----→----→----→----#
![Page 7: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/7.jpg)
Rails-way
![Page 8: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/8.jpg)
Модульная архитектура - это что?4 изменения в коде делать легко4 код легко переиспользовать4 код легко тестировать
![Page 9: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/9.jpg)
Чем плох rails-way?
![Page 10: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/10.jpg)
Single Responsibility Principle
- Не, не слышали
![Page 11: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/11.jpg)
Skinny controllers, fat models.
ORLY?
![Page 12: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/12.jpg)
Conventions over configuration
DB ⇆ Forms
![Page 13: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/13.jpg)
Проект4 Монолитное приложение на Grails
4 База данных на 70 таблиц4 Мы упоролись
![Page 14: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/14.jpg)
Решение заказчика4 Фронтенд на AngularJS
4 Бэкэнд на рельсе для отдачи API
![Page 15: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/15.jpg)
Ок, rails, значит rails
Точнее, rails-api
![Page 16: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/16.jpg)
Модели 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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/17.jpg)
Модели 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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/18.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/19.jpg)
!-"--→----→----→----→----#
![Page 20: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/20.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/21.jpg)
![Page 22: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/22.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/23.jpg)
bonuscode.redeem(user)или
user.redeem_bonuscode(code)?
![Page 24: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/24.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/25.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/26.jpg)
!---→-"---→----→----→----#
![Page 27: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/27.jpg)
Incoming!!!
4 В таблице "users" по факту хранятся разные типы пользователей и для них нужны разные правила валидации
4 Это как минимум
![Page 28: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/28.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/29.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/30.jpg)
!---→---"-→----→----→----#
![Page 31: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/31.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/32.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/33.jpg)
!---→----→---"-→----→----#
![Page 34: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/34.jpg)
Incoming!Заказчику нужен небольшой сервис сбоку mkdir sinatra-app cd sinatra-app git init .
![Page 35: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/35.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/36.jpg)
Sinatra4 встраивается в рельсовые роуты или запускается в связке с другими rack-приложениями
4 рельсовые роуты оказались ненужной абстракцией
4 отчего бы не применить такой подход для всего приложения?!
![Page 37: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/37.jpg)
!---→----→----→--"--→----#
![Page 38: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/38.jpg)
А как же разделение бизнес-логики и хранения данных?4 Очень хочется но непонятно как4 Репозитории?
![Page 39: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/39.jpg)
Entities
![Page 40: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/40.jpg)
Entities - зачем?statuses profiles-------- --------id idname status_id
profile.status.name
![Page 41: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/41.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/42.jpg)
Entities4 Plain old ruby objects*
class Entity include Virtus.model
def new_record? !id end end
![Page 43: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/43.jpg)
Repositories
![Page 44: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/44.jpg)
Repositories - чем занимаются?4 найди мне объект с таким-то id
4 дай мне все объекты по такому-то условию
4 сохрани вот эти данные в виде объекта
![Page 45: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/45.jpg)
AR-flavored repositories4 под капотом все те же ActiveRecord модели
4 репозиторий отдает инстансы Entities
![Page 46: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/46.jpg)
Первая попытка:
AR-flavored repositoriesNEVER AGAIN!
![Page 47: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/47.jpg)
Sequel FTW!sequel. jeremyevans.net
4 DSL для построения SQL-запросов4 реализация паттерна ActiveRecord
![Page 48: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/48.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/49.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/50.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/51.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/52.jpg)
Benefits?
![Page 53: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/53.jpg)
!---→----→----→----→---"-#
![Page 54: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/54.jpg)
Presenters
![Page 55: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/55.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/56.jpg)
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 к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/57.jpg)
!---→----→----→----→----"#
![Page 58: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/58.jpg)
Hexagonal/Clean Architecture
![Page 59: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/59.jpg)
![Page 60: От Rails-way к модульной архитектуре](https://reader033.fdocuments.in/reader033/viewer/2022061300/54c7334a4a7959126e8b4674/html5/thumbnails/60.jpg)
From rails-way to modular architecture
Ivan Nemytchenko, independent consultant
DevConf 2014
@inem, @inemation