Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Improve your (Ruby) code with dependency injection

Improve your (Ruby) code with dependency injection

Both the why and how of improving your Ruby code with dependency injection.

9b6423e15df69eef44d5d34189e128f3?s=128

Stephen Best

July 27, 2013
Tweet

Transcript

  1. IMPROVE YOUR CODE WITH DEPENDENCY INJECTION EXAMPLES IN RUBY :)

    Hand crafted by: Stephen Best / / github.com/bestie @thebestie
  2. WHAT IS DEPENDENCY INJECTION? (QUICKLY BECAUSE WE'RE BORED ALREADY!) "DI

    is the practice of replacing hardcoded classes or types with dynamically configurable objects."
  3. WHY IS IT USEFUL? Enables true isloated unit testing Increases

    re-usability of individual objects Loosens coupling system wide, increases flexibility
  4. SO MY CODE IS STICKY AND RIGID. WHAT ELSE?

  5. IT'S FULL OF GLOBAL VARIBLES In Ruby constants are global

    And can be overwritten Generally a Ruby app will have class objects stored in constants Classes == Global variables c l a s s F o o # . . . e n d F o o = C l a s s . n e w d o # . . . e n d $ f o o _ c l a s s = C l a s s . n e w d o # . . . e n d
  6. AND YOU'VE GOT MONKEY PATCHING EVERYWHERE TOO We frequently write

    code like this # T h i s i s y o u r t e s t i n g f r a m e w o r k i s m o n k e y p a t c h i n g f o r y o u . U s e r . s t u b ( : n e w ) . a n d _ r e t u r n ( d o u b l e )
  7. FRUIT OF THE MONTH CLUB Subscribers receive a box of

    fruit Every month The fruit changes It's going to be HUGE
  8. WHICH FRUITS ARE IN SEASON NOW? c l a s

    s F r u i t < A c t i v e R e c o r d : : B a s e d e f s e l f . i n _ s e a s o n d a t e = D a t e . t o d a y a l l . w h e r e ( [ " s e a s o n _ s t a r t < = ? " , d a t e ] ) . w h e r e ( [ " s e a s o n _ e n d > = ? " , d a t e ] ) e n d e n d
  9. WHICH FRUITS ARE IN SEASON NOW? d e s c

    r i b e F r u i t d o d e s c r i b e " . i n _ s e a s o n " d o b e f o r e { s e e d _ f r u i t s } c o n t e x t " i n s u m m e r t i m e " d o b e f o r e { D a t e . s t u b ( : t o d a y ) . a n d _ r e t u r n ( " 2 0 1 3 - 0 7 - 0 7 " ) } i t " i n c l u d e s s u m m e r f r u i t s " d o e x p e c t ( F r u i t s . i n _ s e a s o n ) . t o i n c l u d e ( s u m m e r _ f r u i t s ) e n d i t " e x c l u d e s w i n t e r f r u i t s " d o e x p e c t ( F r u i t s . i n _ s e a s o n ) . n o t _ t o i n c l u d e ( w i n t e r _ f r u i t s ) e n d e n d e n d e n d
  10. SO WHAT WAS WRONG? Classic example of an unnecessarily hardcoded

    global Which we promptly remedied with some monkey patching Ruby is awesome at this Rigid design that also limits functionality, what about next month?
  11. A SMALL BUT CRUCIAL CHANGE c l a s s

    F r u i t s d e f s e l f . i n _ s e a s o n ( d a t e = D a t e . t o d a y ) a l l . w h e r e ( [ " s e a s o n _ s t a r t < = ? " , d a t e ] ) . w h e r e ( [ " s e a s o n _ e n d > = ? " , d a t e ] ) e n d e n d
  12. TESTS BECOME LESS COMPLEX d e s c r i

    b e F r u i t d o d e s c r i b e " . i n _ s e a s o n " d o b e f o r e { s e e d _ f r u i t s _ i n _ d b } c o n t e x t " i n s u m m e r t i m e " d o l e t ( : d a t e ) { " 2 0 1 3 - 0 7 - 0 7 " } i t " i n c l u d e s s u m m e r f r u i t s " d o e x p e c t ( F r u i t s . i n _ s e a s o n ( d a t e ) ) . t o i n c l u d e ( s u m m e r _ f r u i t s ) e n d i t " e x c l u d e s w i n t e r f r u i t s " d o e x p e c t ( F r u i t s . i n _ s e a s o n ( d a t e ) ) . n o t _ t o i n c l u d e ( w i n t e r _ f r u i t s ) e n d e n d e n d e n d
  13. FRUIT SEARCH FEATURE For the connoisseur Lots of search options,

    origin, colour, sweetness etc...
  14. The crack team write something reasonable Starting with a service

    object c l a s s F r u i t s C o n t r o l l e r < A p p l i c a t i o n C o n t r o l l e r d e f s e a r c h @ f r u i t s = C o m p l e x F r u i t S e a r c h . n e w . r e s u l t s ( p a r a m s ) e n d e n d
  15. The crack team write something reasonable c l a s

    s C o m p l e x F r u i t S e a r c h d e f r e s u l t s ( p a r a m s ) @ p a r a m s = p a r a m s F r u i t . w h e r e ( o r m _ f r i e n d l y _ p a r a m s ) e n d p r i v a t e d e f o r m _ f r i e n d l y _ p a r a m s # l o t s o f c o m p l e x l o g i c e n d e n d
  16. WHAT COULD POSSIBLY BE WRONG NOW? Fruit happens to be

    an ActiveRecord This makes tests slow (loading Rails) Not open to re-use
  17. THE ONE CONSTANT IS CHANGE The boss asks for scoped

    search pages Exotic fruits Seasonal fruits Seedless fruits How do we do this?
  18. c l a s s C o m p l

    e x F r u i t S e a r c h d e f r e s u l t s ( p a r a m s , o p t i o n s = { } ) @ p a r a m s = p a r a m s s c o p e = o p t i o n s . f e t c h ( : s c o p e ) { F r u i t . a l l } s c o p e . w h e r e ( o r m _ f r i e n d l y _ p a r a m s ) e n d p r i v a t e d e f o r m _ f r i e n d l y _ p a r a m s # l o t s o f c o m p l e x l o g i c e n d e n d This refactoring gives us variable ORM scope and isolated / fast tests achieved with some 'fake DI'
  19. ONE LAST THING. WHO ARE YOU? SOME KIND OF STEVE

    JOBS? Product manager: "Loved the work on Fruits, we're adding vegetables too" Developer: "OK cool well that should be easy since we've been leveraging DI in our app!" Product manager: "Whatever dork, just get on with it!"
  20. SURE THERE'S DI, BUT IT COULD BE BETTER c l

    a s s V e g e t a b l e s S e a r c h C o n t r o l l e r < A p p l i c a t i o n C o n t r o l l e r d e f i n _ s e a s o n @ r e s u l t s = C o m p l e x P r o d u c e S e a r c h . n e w . r e s u l t s ( s c o p e : V e g e t a b l e . i n _ s e a s o n , p a r a m s : p a r a m s , ) e n d d e f p u l s e s @ r e s u l t s = C o m p l e x P r o d u c e S e a r c h . n e w . r e s u l t s ( s c o p e : V e g e t a b l e . p u l s e s , p a r a m s : p a r a m s , ) e n d e n d
  21. DI + CONFIGURABLE OBJECTS = JOY c l a s

    s V e g e t a b l e s S e a r c h C o n t r o l l e r < A p p l i c a t i o n C o n t r o l l e r d e f i n _ s e a s o n @ r e s u l t s = v e g _ s e a r c h . r e s u l t s ( s c o p e : : i n _ s e a s o n , p a r a m s : p a r a m s , ) e n d d e f p u l s e s @ r e s u l t s = v e g _ s e a r c h . r e s u l t s ( s c o p e : : p u l s e s , p a r a m s : p a r a m s , ) e n d p r i v a t e d e f v e g _ s e a r c h C o m p l e x P r o d u c e S e a r c h . n e w ( t y p e : V e g e t a b l e ) e n d e n d
  22. FRUIT + VEGETABLES = PRODUCE c l a s s

    C o m p l e x P r o d u c e S e a r c h d e f i n i t i a l i z e ( d e p e n d e n c i e s ) # N o w t h i s c a n s e a r c h e v e n v e g e t a b l e s ! @ t y p e = d e p e n d e n c i e s . f e t c h ( : t y p e ) e n d d e f r e s u l t s ( a r g s ) @ p a r a m s = a r g s . f e t c h ( : p a r a m s ) @ s c o p e = a r g s . f e t c h ( : s c o p e , : a l l ) t y p e . p u b l i c _ s e n d ( s c o p e ) . w h e r e ( o r m _ f r i e n d l y _ p a r a m s ) e n d p r i v a t e a t t r _ r e a d e r : t y p e , : p a r a m s , : s c o p e d e f o r m _ f r i e n d l y _ p a r a m s # l o t s o f c o m p l e x l o g i c e n d e n d
  23. TWO IMPORTANT THINGS JUST HAPPENED A hardcoded dependency became a

    runtime configuration option Eliminated the default value and decoupled from the specific class name The Gang of Four would be proud, we just programmed to an interface not an implementation.
  24. PROGRAMME TO A DUCK, NOT A MALLARD # C o

    m p l e x P r o d u c e S e a r c h i s a m a l l a r d @ r e s u l t s = C o m p l e x P r o d u c e S e a r c h . n e w ( t y p e : F r u i t ) . r e s u l t s ( s c o p e : : i n _ s e a s o n , p a r a m s : p a r a m s , ) # f r u i t _ s e a r c h i s a d u c k @ r e s u l t s = f r u i t _ s e a r c h . r e s u l t s ( s c o p e : : i n _ s e a s o n , p a r a m s : p a r a m s , ) Ducks and Mallards will preferably have different names Name classes according to their implementations Name collaborators according to role
  25. THAT'S ALL VERY WELL BUT THE MALLARDS HAVE GOTTEN EVERYWHERE

    WHAT CAN WE DO? TODO: INSERT SOLUTION
  26. OBJECT CONSTRUCTION Consider the difference between the following. # C

    l a s s m e t h o d C o m p l e x P r o d u c e S e a r c h . r e s u l t s ( F r u i t . i n _ s e a s o n , p a r a m s ) Not OO More like a namespaced procedure Implementation will be hard to refactor
  27. OBJECT CONSTRUCTION # P a s s e v e

    r y t h i n g t o n e w C o m p l e x P r o d u c e S e a r c h . n e w ( F r u i t . i n _ s e a s o n , p a r a m s ) . r e s u l t s We're instantiating an object (great) But the object isn't re-usable The client or creator of this object needs to know all the dependencies at once
  28. SEPARATE 'COMPILE TIME' AND 'RUNTIME' DEPENDENCIES OK so Ruby doesn't

    exactly compile BUT... # S e p a r a t e d e p e n d e n c i e s a n d i n p u t s C o m p l e x P r o d u c e S e a r c h . n e w ( F r u i t ) . r e s u l t s ( : i n _ s e a s o n , p a r a m s ) The Fruit type is known well in advance The params and the scope are user inputs Instantiation and invocation have now been separated
  29. INSTANTIATION AS A SEPERATE CONCERN c l a s s

    V e g e t a b l e s S e a r c h C o n t r o l l e r < A p p l i c a t i o n C o n t r o l l e r d e f i n _ s e a s o n @ r e s u l t s = a p p . v e g _ s e a r c h . c a l l ( s c o p e : : i n _ s e a s o n , p a r a m s : p a r a m s , ) e n d # . . . e n d No instantiation What's app exactly?
  30. YOUR APP COULD BE AN OBJECT TOO! c l a

    s s F r u i t O f T h e M o n t h A p p d e f v e g _ s e a r c h C o m p l e x P r o d u c e S e a r c h . n e w ( V e g e t a b l e ) e n d # . . . e n d This is the one place class names can be found Contains no logic beyond simple factory methods Is essentially a config file This pattern is called Service Locator Keeps all your mallards in one place
  31. HOW DO I GET THIS INTO RAILS? # c o

    n f i g / i n i t i a l i z e r s / f r u i t _ o f _ t h e _ m o n t h _ a p p . r b A P P = F r u i t O f T h e M o n t h A p p . n e w # a p p / c o n t r o l l e r s / a p p l i c a t i o n _ c o n t r o l l e r . r b c l a s s A p p l i c a t i o n C o n t r o l l e r d e f a p p A P P e n d # . . . e n d Your app object need not be a singleton, use a constant and instantiate just one.
  32. SOME FUNCTIONAL TRICKS c l a s s C r

    e a t e U s e r d e f c a l l ( p a r a m s ) u s e r = U s e r . n e w ( p a r a m s ) # o t h e r c o m p l e x l o g i c . . . e n d e n d Now with DI ...
  33. c l a s s C r e a t

    e U s e r d e f i n i t i a l i z e ( o p t i o n s ) @ u s e r _ c l a s s = o p t i o n s . f e t c h ( : u s e r _ c l a s s ) e n d d e f c a l l ( p a r a m s ) u s e r = @ u s e r _ c l a s s . n e w ( p a r a m s ) # o t h e r c o m p l e x l o g i c . . . e n d e n d User constuction is now injected It's still coupled to the class level implementation and we hate those
  34. DECOUPLE WITH THE #CALL PROTOCOL c l a s s

    C r e a t e U s e r d e f i n i t i a l i z e ( o p t i o n s ) @ u s e r _ b u i l d e r = o p t i o n s . f e t c h ( : u s e r _ b u i l d e r ) e n d d e f c a l l ( p a r a m s ) u s e r = @ u s e r _ b u i l d e r . c a l l ( p a r a m s ) # o t h e r c o m p l e x l o g i c . . . e n d e n d User builder can be anything that responds to call Procs, blocks and lambdas Your objects with a #call method Ruby method objects
  35. DID YOU ACTUALLY SAY METHOD METHOD? c l a s

    s F r u i t O f T h e M o n t h A p p d e f c r e a t e _ u s e r C r e a t e U s e r . n e w ( u s e r _ b u i l d e r : U s e r . m e t h o d ( : n e w ) , ) e n d e n d #method plucks the method off the object Behaves like a lambda responds to #call Doesn't lose its binding like in Javascript Easily replaced with a more complex object later Great trick for interface segregation
  36. CURRYING > f u n c = l a m

    b d a { | a , b , c | [ a , b , c ] . j o i n ( " a n d " ) } > f u n c . c u r r y . c a l l ( " A " ) . c a l l ( " B " ) . c a l l ( " C " ) = > " A a n d B a n d C " Another great tool for separating timings of arguments Only works on positional arguments :( Both your FP and OOP friends will think it's cool No one will even notice it's a factory factory
  37. COMING IN RUBY 2.1 Required keyword arguments > d e

    f s a y _ h e l l o ( t o : , f r o m : ) > p u t s " # { t o } , # { f r o m } s a y s h i ! " > e n d = > : s a y _ h e l l o > s a y _ h e l l o ( f r o m : " B e s t i e " ) A r g u m e n t E r r o r : m i s s i n g k e y w o r d : t o > s a y _ h e l l o ( f r o m : " B e s t i e " , t o : " S c o t R U G " ) S c o t R U G B e s t i e s a y s h i ! = > n i l
  38. CURRYING KEYWORD ARGUMENTS There's a gem for that! github.com/bestie/keyword_curry Works

    for required keywords only
  39. KEYWORD CURRYING EXAMPLE > r e q u i r

    e " k e y w o r d _ c u r r y " = > t r u e > K e y w o r d C u r r y . m o n k e y _ p a t c h _ p r o c = > P r o c > d e f s a y _ h e l l o ( t o : , f r o m : ) > p u t s " # { t o } , # { f r o m } s a y s h i ! " > e n d = > : s a y _ h e l l o > m e t h o d ( : s a y _ h e l l o ) . t o _ p r o c . c u r r y . c a l l ( f r o m : " B e s t i e " ) . c a l l ( t o : " S c o t R U G " ) S c o t R U G B e s t i e s a y s h i ! = > n i l
  40. IF LIKE ME YOU ALSO LOVE #FETCH There's a gem

    for that too. github.com/bestie/fetchable
  41. SUMMARY DI gives us ... More focused AND faster testing

    More flexible code Code that can do different things when introduced to new collaborators Reduced need to change existing objects A clear path to acheiving the 'O', 'I' and 'D' of SOLID
  42. START TODAY! With unevaluated default arguments c l a s

    s D I H a t e r d e f i n i t i a l i z e ( o u t l e t = T w i t t e r . n e w ) # . . . e n d e n d
  43. START TODAY! Or with Hash#fetch c l a s s

    D I H a t e r d e f i n i t i a l i z e ( a r g s ) @ h a t e _ o u t l e t = a r g s . f e t c h ( : h a t e _ o u t l e t ) { T w i t t e r . n e w } e n d e n d
  44. WHO WAS THAT GUY? STEPHEN BEST Contract Ruby / Javascript

    developer Cambridge Healthcare - howareyou.com @thebestie github.com/bestie