Gourmet Service Objects

A presentation at Lunch & Learn in December 2014 in Vancouver, BC, Canada by Brooklyn Zelenka

Slide 1

Slide 1

GOURMET SERVICE OBJECTS™ The very thinnest of classes

Slide 2

Slide 2

MODELS VS SERVICES

Slide 3

Slide 3

MODELS • Resources • A collection of attributes that models some data/state • Can touch the database — but doesn’t have to (ActiveModel rocks) • May include associations, scoping, and so on

Slide 4

Slide 4

SERVICES • Never resources • Simple! • No attributes, no accessors, no internal state • Thus no need for instances! • “Method” object — do-ers • Can be universal — “pure functions”

Slide 5

Slide 5

THE NATURE OF A SERVICE • Clean (ie: short) • • Does one thing Doesn’t depend on context • Call from anywhere • Few object dependancies (if any) • • Dependency inject if there is one (more later) Composable

Slide 6

Slide 6

GOURMET SERVICES • Has only one method (::call) • De facto standard because same as Procs, methods, etc • Takes args={}, or named params • Dependancy inject all of the things! • i.e.: when calling outside objects, give the user the option to override with a call to a different object of the same duck type

Slide 7

Slide 7

SERVICES DIRECTORY • It’s even built into Rails! • • app/services is automagically included • Models live in app/models • Services live in app/services • No need for Persistence namespace (shouldn’t matter where the data comes from) Separates services from models (easier to read)

Slide 8

Slide 8

SERVICES LAYER • A glance at the services directory shows all the things the app does • Contextual, semantic, easy to know what it does from the outside • ScheduleAction • BlockAccount • SendReminder • LaunchMissiles

Slide 9

Slide 9

EXAMPLES

Slide 10

Slide 10

EXAMPLE # app/services/accept_invite.rb class AcceptInvite def self.call(args = {}) invite = args.fetch(:invite) user = args.fetch(:user) invite.call(user) UserMailer.invite_accepted(invite).deliver end end # Somewhere else AcceptInvite.call(user: chuck_norris, invite: party_time)

Slide 11

Slide 11

CONTRIVED SUPERBOLT EXAMPLE class Email::SendAdminSample def self.call(params = {}) fail unless params[:emails].present? # Delegate to Advocato delegator = params.delete(:delegator) || Superbolt::Advocato::Enqueue delegator.call(params) end end

Slide 12

Slide 12

COMPOSITION • “has-a” rather than “is-a” • Single responsibility (by design) • Open/closed (from the perspective of a composed object) • Composition moves towards concretion • Doesn’t inherit or depend on unused methods • Create hierarchies on the fly

Slide 13

Slide 13

KNOCK-ON EFFECTS • Easy to name, because it does one thing • Adds context to code • via a semantic space of names and convention • Succinct and clear • Highly reusable (DRY)

Slide 14

Slide 14

EASY TO TEST • Does one thing • Few dependancies (by design) • Isolated unit tests stub any calls to outside objects • SOLID decoupling by design

Slide 15

Slide 15

USES • Encapsulate asynchronicity • Workers • Superbolt queueing/fetching • Hold common regexes (can compose them too) • On the more radical end, can move all verbs into services to create a “behaviour layer” • And more!

Slide 16

Slide 16

FURTHER READING • • Gourmet • http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html • https://gist.github.com/pcreux/9277929 • http://www.reddit.com/r/rails/comments/24n5s2/gourmet_service_objects/ Regular • http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ • http://stevelorek.com/service-objects.html • http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html