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
These things might help you• SOLID principles
• Design Pattrens
• Refactoring techniques
• Architecture types
• Code smells identification
• Tesing best practices
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
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
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
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
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
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
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
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
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
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