Fighting Fat Models (Богдан Гусев)

Preview:

Citation preview

FightingwithfatmodelsBogdanGusiev

BogdanG.

is9yearsinIT6yearswithRubyandRails

LongRunRailsContributor

Someofmygemshttp://github.com/bogdan

Datagridjs-routesaccepts_values_forfuri

MyBlog

http://gusiev.com

http://talkable.com

Asmallstartupisagreatplacetomovefrommiddletoseniorandabove

FatModelsWhytheproblemappears?

Allbusinesslogiccodegoestomodelbydefault.

IntheMVC:Whyitshouldnotbeincontrollerorview?

Becausetheyarehardto:

testmaintainreuse

Adefinitionofbeingfat

1000LinesofcodeButitdependson:

DocsWhitespaceComments

$ wc -l app/models/* | sort -n | tail 532 app/models/incentive.rb 540 app/models/person.rb 544 app/models/visitor_offer.rb 550 app/models/reward.rb 571 app/models/web_hook.rb 786 app/models/site.rb 790 app/models/referral.rb 943 app/models/campaign.rb 998 app/models/offer.rb 14924 total

Existingtechniques

Existingtechniques

ServicesSeparatedutilityclass

ConcernsModulesthatgetincludedtomodels

Presenters/WrappersClassesthatwrapexistingmodeltoplugnewmethods

Whatdoweexpect?

Standard:ReusablecodeEasytotestGoodAPI

Advanced:EffectivedatamodelMOREfeaturespersecondDataSafety

GoodAPI

GoodAPIIsauserconnectedtofacebook?

user.connected_to_facebook?# ORFacebookService.connected_to_facebook?(user)# ORFacebookWrapper.new(user) .connected_to_facebook?

TheneedofServices

WhenamountofutilsthatsupportModelgoeshigher

extractthemtoserviceisgoodidea.

Moveclassmethodsbetweenfilesischeap

# move(1) User.create_from_facebook# to(2) UserService.create_from_facebook# or(3) FacebookService.create_user

Organiseservicesbyprocessratherthanobjecttheyoperateon

OtherwiseatsomemomentUserServicewouldnotbeenough

OtherwiseatsomemomentUserServicewouldnotbeenough

TheproblemofservicesServiceisseparatedutilityclass.

module CommentService

module CommentService def self.create(attributes) comment = Comment.create!(attributes) deliver_notification(comment) endend

"Язнаюоткудачтоберется"

Servicesdon't

providedefaultbehavior

providedefaultbehavior

TheNeedofDefaultBehaviorObjectshouldencapsulatebehavior:

DataRulesSetofrulesthatamodelshouldfitattheprogramming

SetofrulesthatamodelshouldfitattheprogramminglevelEx:Acommentshouldhaveanauthor

BusinessRulesSetofrulesthatamodelshouldfittoexistintherealworldEx:Acommentshoulddeliveranemailnotification

Whatisamodel?Themodelisanimitationofrealobject

thatreflectssomeit'sbehaviors

thatwearefocusedon.

Wikipedia

Modelisabestplacefordefaultbehaviour

MVCauthorsmeantthat

ImplementationUsingbuilt-inRailsfeatures:

ActiveRecord::Callbacks

HooksinmodelsWecreatedefaultbehaviorandourdataissafe.

Example:Commentcannotbecreatedwithoutnotification.

class Comment < AR::Base after_create :send_notification

end

APIcomparison

Comment.create# orCommentService.create

SuccessfulProjectstendtodo

onethinginmanydifferentwaysratherthanalotofthings

CommentonawebsiteCommentinnativemobileiOSappCommentinnativemobileAndroidappCommentbyreplyingtoanemailletterAutomaticallygeneratecomments

TeamGrowthProblemHowwouldyoudeliveraknowledgethatcommentshould

bemadelikethisto10people?

CommentService.create(...)

Reimplementotherperson'sAPIhasmorewisdomthaninventnewone.

Comment.create(...)

EdgecasesInallcasesdatacreatedinregularway

Inoneedgecasesspecialrulesapplied

Servicewithoptions

module CommentService def self.create( attrs, skip_notification = false)end

Defaultbehavior

andedgecasesHeymodel,createmycomment.

Ok

Heymodel,whydidyousendthenotification?Becauseyoudidn'tsayyoudon'tneedit

Becauseyoudidn'tsayyoudon'tneedit

Heymodel,createmodelwithoutnotificationOk

Supportparameterinmodelclass Comment < AR::Base attr_accessor :skip_comment_notification after_create do unless self.skip_comment_notification send_notification end endend

end

#skip_comment_notificationisusedonlyinedgecases.

DefaultBehaviourishardtomakeButitsolvescommunicationproblems

thatwillonlyincreaseovertime

Whatisthedifference?

FacebookService.register_user(...)

Comment.after_create :send_notification

Businessrules:UsercouldberegisteredfromfacebookCommentshouldsendanemailnotification

Modelstandsforshould

ServicestandsforcouldPleasedonotconfuseshouldwithmust

Wherearepresenters?

UserPresenter.new(user)# ORclass User include UserPresenterend

TradeanAPIforlessmethodsinobject

Moreeffectivepresenters?

ExampleofServiceimplementationwithwrapperMoreexampleatActiveRecordsourcecode

class StiTools def self.run(from_model, to_model) new(from_model, to_model).perform end

private def initialize(from_model, to_model)

def perform shift_id_info

DatagridGemExampleofcollectionwrapper

https://github.com/bogdan/datagrid

UsersGrid.new( last_request: Date.today, created_at: 1.month.ago..Time.now)

class UsersGrid scope { User }

filter(:created_at, :date, range: true) filter(:last_request_at, :datetime, range: true

WrappingDatahttps://github.com/bogdan/furi

u = Furi.parse( "http://bogdan.github.com/index.html")u.subdomain # => 'bogdan'u.extension # => 'html'u.ssl? # => false

module Furi def self.parse(string)

Serviceusageisinconvinientbecauseofvalidation

Customer.has_many :purchasesPurchase.has_many :ordered_itemsOrderItem.belongs_to :product

ManualOrder.ancestors.include?( ActiveRecord::Base) # => false

order = ManualOrder.new(attributes)if order.valid? order.save_all_those_records_at_once!

Wrappers/PresentersVeryspecificuse

WrapperaroundcollectionParsingserialisedobjectUnder-the-hoodclassinsideaserviceServiceusageisinconvinient

Themodelisstillfat.Whattodo?

UseConcerns

UseConcerns

class Comment < AR::Base include CommentNotification include FeedActivityGeneration include Archivableend

Railsdefault:app/models/concerns/*

Attention!

Attention!Peoplewithhighpressureorpropensitytosuicide

Nextslidecanbeconsideredoffensivetoyourreligion

SingleResponsibilityPrinciple

SUCKSTheprooffollows

ThereisnoasinglethingintheuniversethatfollowstheSRP

intheuniversethatfollowstheSRP

class Proton include Gravitation include ElectroMagnetism include StrongNuclearForce include WeekNuclearForceend

Whymanmadethingsshould?

Whymanmadethingsshould?TheworldisunreasonablycomplexttofollowSRP

Howamodelthatsupposetosimulatethosethingscanhaveasingleresponsibility?

Itcan't!

ModelConcernsareunavoidableifyouwanttohaveagoodmodel

ifyouwanttohaveagoodmodel

ConcernsareVerticalslicingUnlikeMVCwhichishorizontalslicing.

SplitmodelintoConcernsclass User < AR::Base

class User < AR::Base include FacebookProfileend

# Hybrid Concern that provides # instance and class methodsmodule FacebookProfile has_one :facebook_profile # simplified def connected_to_facebook? def self.register_from_facebook(attributes)

Ex.1User+Facebook

has_one :facebook_profile=>Model

#register_user_from_facebook=>Service

#register_user_from_facebook=>Serviceconnect_facebook_profile=>Serviceconnected_to_facebook?=>Model

Everyusershouldknowifitisconnectedtofacebookornot

Ex.2Delivercommentnotification

Comment#send_notification=>ModelDefaultBehaviourEvenifexceptionsexist

Evenifexceptionsexist

Basicapplicationarchitecture

View

Controller

Model

Model

Services Presenters

Concern Concern Concern

ConcernsBaseAttributesAssociations

has_one

has_onehas_manyhas_and_belongs_to_many

Butrarely

LibrariesusingConcerns

ActiveRecordActiveModelDeviseDatagrid

Datagrid

Summary

InjectServicebetweenModelandControllerifyouneedthem

Could?=>Service

Should?=>Model

SRPisamisleadingprincipleItshouldnotinhibityoufromhaving

aBetterApplicationModel

Fatmodels=>ThinConcerns

Reimplementotherperson'sAPIhasmorewisdomthaninventnewone.

Presentersareprettyspecific

Usethemin

Wrappingthecollection"private"classServiceusageisinconvenient

TheEndThanksforyourtime

http://gusiev.com

https://github.com/bogdan