fighting fat models (Богдан Гусев)
TRANSCRIPT
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