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

Practical dependency injection for Ruby

Practical dependency injection for Ruby

9b6423e15df69eef44d5d34189e128f3?s=128

Stephen Best

August 18, 2016
Tweet

Transcript

  1. PRACTICAL DEPENDENCY INJECTION (for Ruby) @thebestie

  2. WHAT'S COVERED What is dependency injection? What problems can it

    solve? How do we write code that takes advantage of it?
  3. WHAT IS IT? Something, something, inversion of control technique ...

  4. WHAT PROBLEMS CAN IT SOLVE? It can be a real

    help to code that is Complex to test Slow to test Hard to change Difficult to re-use, even for *very* similar tasks
  5. The root cause is COUPLING

  6. Today we'll explore how our applications o en end up

    coupled to global variables In Ruby constants are global and can be overwritten 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
  7. The sky isn't falling. We still build stuff that works.

    We still build stuff that makes money. How on earth do we manage?
  8. MONKEY PATCHING Let's see how we can decouple from globals

    and reduce our reliance on monkey patching.
  9. FRUIT OF THE MONTH CLUB Subscribers receive a box of

    fruit Every month The fruit changes We're looking to raise $1.000.000 at a $10.000.000 valuation We're also hiring senior Rails engineers with 3+ months experience
  10. Feature 1 G i v e n I a m

    a f r u i t e n t h u s i a s t A n d I l i v e f o r t o d a y b e c a u s e t o m o r r o w m a y n e v e r c o m e W h e n I v i s i t t h e i n s e a s o n n o w p a g e T h e n I s e e a l i s t o f f r u i t s t h a t a r e a v a i l a b l e t o d a y
  11. 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 i n _ s e a s o n r e n d e r ( j s o n : F r u i t . i n _ s e a s o n ) e n d e n d
  12. 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 a l l . w h e r e ( " s e a s o n _ s t a r t > = ? " , D a t e . t o d a y ) . w h e r e ( " s e a s o n _ e n d < = ? " , D a t e . t o d a y ) e n d e n d
  13. R S p e c . 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 " d o b e f o r e { T i m e c o p . t r a v e l ( D a t e . n e w ( 2 0 1 6 , 7 , 1 ) ) } i t " r e t u r n 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 . 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 . 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 c o n t e x t " i n w i n t e r " d o # . . . e n d e n d e n d
  14. SO WHAT'S WRONG WITH THAT? Classic example of an unnecessarily

    hardcoded dependency (system time) They worked around it with monkey patching The design is rigid and limits functionality, what about next month?
  15. LISTEN TO YOUR TESTS R S p e c .

    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 " d o l e t ( : d a t e ) { D a t e . n e w ( 2 0 1 6 , 7 , 1 ) } i t " r e t u r n 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 . 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 . 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 c o n t e x t " i n w i n t e r " d o # . . . e n d e n d e n d
  16. 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 ) 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
  17. 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 i n _ s e a s o n r e n d e r ( j s o n : F r u i t . i n _ s e a s o n ( d a t e ) ) e n d p r i v a t e d e f d a t e p a r a m s . f e t c h ( " d a t e " , D a t e . t o d a y ) e n d e n d
  18. Feature 2 G i v e n I a m

    a f r u i t e n t h u s i a s t A n d I l i k e t o d r e a m a b o u t t h e f u t u r e W h e n I v i s i t t h e i n s e a s o n p a g e f o r J a n u a r y T h e n I s e e t h e f r u i t s t h a t w i l l a v a i l a b l e i n J a n u a r y
  19. Remember what we did? We just got this feature for

    free.
  20. Feature 3 G i v e n I a m

    a f r u i t e n t h u s i a s t A n d I h a v e a g o o g l e a c c o u n t W h e n I s i g n i n T h e n I c a n a u t h e n t i c a t e w i t h G o o g l e r a t h e r t h a n u s e a p a s s w o r d
  21. We may expect our controller to look something like this

    c l a s s U s e r S e s s i o n s C o n t r o l l e r d e f c r e a t e s i g n _ i n ( u s e r ) r e d i r e c t _ t o ( h o m e _ p a t h ) e n d p r i v a t e d e f u s e r # . . . e n d e n d How exactly do we get the authenticated user?
  22. Google gives us their email address (in a roundabout way)

    c l a s s U s e r S e s s i o n s C o n t r o l l e r # . . . d e f u s e r U s e r . f i n d _ b y _ e m a i l ( e m a i l ) e n d d e f e m a i l u s e r _ i n f o . f e t c h ( " e m a i l " ) e n d d e f u s e r _ i n f o # . . . e n d e n d
  23. We exchange an authentication code for the user's details c

    l a s s U s e r S e s s i o n s C o n t r o l l e r # . . . d e f u s e r _ i n f o a u t h _ c l i e n t . c o d e = p a r a m s . f e t c h ( " c o d e " ) a u t h _ c l i e n t . f e t c h _ a c c e s s _ t o k e n ! a u t h _ c l i e n t . d e c o d e _ j w t e n d d e f a u t h _ c l i e n t @ a u t h _ c l i e n t | | = S i g n e t : : O A u t h 2 : : C l i e n t . n e w ( s c o p e : " e m a i l p r o f i l e " , c l i e n t _ i d : R a i l s . a p p l i c a t i o n . s e c r e t s . g o o g l e _ c l i e n t _ i d , c l i e n t _ s e c r e t : R a i l s . a p p l i c a t i o n . s e c r e t s . g o o g l e _ c l i e n t _ s e c r e t , a u t h o r i z a t i o n _ u r i : " h t t p s : / / a c c o u n t s . g o o g l e . c o m / o / o a u t h 2 / a u t h " t o k e n _ c r e d e n t i a l _ u r i : " h t t p s : / / w w w . g o o g l e a p i s . c o m / o a u t h 2 / v 3 / t o k e n " r e d i r e c t _ u r i : " h t t p : / / e x a m p l e . c o m / u s e r _ s e s s i o n s / c r e a t e " ) e n d e n d
  24. The response from Google contains a JSON web token /

    / i d _ t o k e n h e r e i s t h e J S O N w e b t o k e n c o n t a i n i n g t h e u s e r ' s d e t a i l s { " a c c e s s _ t o k e n " : " y a 2 9 . C i 8 O A 1 n 0 b x I 5 h l K D U U C z 2 f J J Z m 9 H w c u K r q c k Y 5 S E C X T O 5 H x V i 0 6 " t o k e n _ t y p e " : " B e a r e r " , " e x p i r e s _ i n " : 3 6 0 0 , " r e f r e s h _ t o k e n " : " 1 / T - O z b 2 5 L 3 4 Z C r M S k r 1 v M 1 q H M O 2 T - 0 B t m M e X o x q 5 K E M 0 " , " i d _ t o k e n " : " e y J h b G c i O i J S U z I 1 N i I s I m t p Z C I 6 I m V k Z D F j Z m F j N T E y O D c z Z j U z Y j A 2 M G Q y }
  25. We can test this with our usual monkey patching appproach

    # T e s t h e l p e r d e f s t u b _ a c c e s s _ t o k e n ( a u t h c o d e ) s t u b _ r e q u e s t ( : p o s t , " h t t p s : / / w w w . g o o g l e a p i s . c o m / o a u t h 2 / v 3 / t o k e n " ) . w i t h ( b o d y : { " c o d e " = > a u t h c o d e , " g r a n t _ t y p e " = > " a u t h o r i z a t i o n _ c o d e " , " r e d i r e c t _ u r i " = > " h t t p : / / e x a m p l e . c o m / u s e r _ s e s s i o n s / c r e a t e " " c l i e n t _ i d " = > R a i l s . a p p l i c a t i o n . s e c r e t s . g o o g l e _ c l i e n t _ i d , " c l i e n t _ s e c r e t " = > R a i l s . a p p l i c a t i o n . s e c r e t s . g o o g l e _ c l i e n t _ s e c r e t } ) . t o _ r e t u r n ( b o d y : J S O N . d u m p ( g o o g l e _ a c c e s s _ t o k e n _ f i x t u r e ) , h e a d e r s : { " C o n t e n t - T y p e " = > " a p p l i c a t i o n / j s o n " } ) e n d
  26. Seems ok until the next day... S i g n

    a t u r e h a s e x p i r e d ( J W T : : E x p i r e d S i g n a t u r e ) . / a p p / c o n t r o l l e r s / u s e r _ s e s s i o n s _ c o n t r o l l e r . r b : 3 5 : i n ` u s e r _ i n f o ' . / a p p / c o n t r o l l e r s / u s e r _ s e s s i o n s _ c o n t r o l l e r . r b : 5 : i n ` c r e a t e ' . / f e a t u r e s / s u p p o r t / f e a t u r e _ a p p l i c a t i o n _ a c t i o n s . r b : 3 : i n ` s i m u l a t e _ g o o g l e _ a u t . / f e a t u r e s / s t e p _ d e f i n i t i o n s / a u t h e n t i c a t i o n _ s t e p s . r b : 1 6 : i n ` / ^ I s i g n i n $ / ' f e a t u r e s / a u t h e n t i c a t i o n . f e a t u r e : 5 : i n ` W h e n I s i g n i n '
  27. It turns out it was more complex than we thought.

    JWTs present a few inconveniences: Time sensitive (configurable) Opaquely encoded Optionally encryped HMAC
  28. We have two options now for our integration tests Monkeypatch

    time for every test Remove the time constraint (opening us to replay attacks) Furthermore, to sign in a different user we need another opauque awkward fixture!
  29. We need to be able to mock out complex components

    to reduce complexity. We need to create 'slots' that we can fit swappable components into. We should not rely on the presence of globals.
  30. Step 1: Wrap the authication client in an adapter c

    l a s s S i g n e t A d a p t e r d e f i n i t i a l i z e ( c l i e n t _ i d : , c l i e n t _ s e c r e t : , r e d i r e c t _ u r i : ) @ c l i e n t _ i d = c l i e n t _ i d @ c l i e n t _ s e c r e t = c l i e n t _ s e c r e t @ r e d i r e c t _ u r i = r e d i r e c t _ u r i e n d d e f a u t h e n t i c a t e ( c o d e ) c l i e n t . c o d e = c o d e c l i e n t . f e t c h _ a c c e s s _ t o k e n ! c l i e n t . d e c o d e d _ i d _ t o k e n e n d p r i v a t e d e f c l i e n t @ c l i e n t | | = S i g n e t : : O A u t h 2 : : C l i e n t . n e w ( # . . . ) e n d # . . . e n d
  31. We need somewhere to inject it. A service object! c

    l a s s A u t h e n t i c a t e U s e r d e f i n i t i a l i z e ( a u t h _ c l i e n t , u s e r s , c o d e ) @ a u t h _ c l i e n t = a u t h _ c l i e n t @ u s e r s = u s e r s @ c o d e = c o d e e n d d e f c a l l u s e r s . f i n d _ b y _ e m a i l ( e m a i l ) e n d p r i v a t e d e f e m a i l u s e r _ i n f o . f e t c h ( " e m a i l " ) e n d d e f u s e r _ i n f o a u t h _ c l i e n t . a u t h e n t i c a t e ( c o d e ) e n d e n d
  32. This is an intermediate step auth_client still has to come

    from somewhere The controller still has lots of knowledge c l a s s U s e r S e s s i o n s C o n t r o l l e r d e f c r e a t e s i g n _ i n ( u s e r ) r e d i r e c t _ t o ( h o m e _ p a t h ) e n d p r i v a t e d e f u s e r A u t h e n t i c a t e U s e r . n e w ( a u t h _ c l i e n t , U s e r , p a r a m s . f e t c h ( " c o d e " ) ) e n d e n d
  33. Add another layer to handle dependencies c l a s

    s U s e r S e s s i o n s C o n t r o l l e r d e f c r e a t e s i g n _ i n ( u s e r ) r e d i r e c t _ t o ( h o m e _ p a t h ) e n d p r i v a t e d e f u s e r a p p . a u t h e n t i c a t e _ u s e r ( p a r a m s . f e t c h ( " c o d e " ) ) e n d e n d
  34. An application is an object that offers services 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 i n i t i a l i z e ( a u t h _ c l i e n t : . . . ) # I n j e c t a l l e x t e r n a l d e p e n d e n c i e s h e r e . . . e n d d e f a u t h e n t i c a t e _ u s e r ( c o d e ) A u t h e n t i c a t e U s e r . n e w ( a u t h _ c l i e n t , U s e r , c o d e ) . c a l l e n d e n d
  35. Create a differently configured app instance for each env No

    need for env checks - a pernicious source of ifs # c o n f i g / e n v i r o n m e n t s / t e s t . 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 u t h _ c l i e n t : M O C K _ A U T H _ C L I E N T ) 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 p r i v a t e d e f a p p A P P e n d e n d
  36. Finally we create a mock authentication client. c l a

    s s M o c k G o o g l e A u t h e n t i c a t i o n d e f i n i t i a l i z e ( u s e r _ i n f o ) @ u s e r _ i n f o = u s e r _ i n f o e n d d e f a u t h e n t i c a t e ( _ c o d e ) @ u s e r _ i n f o e n d e n d M O C K _ A U T H _ C L I E N T = M o c k G o o g l e A u t h e n t i c a t i o n . n e w ( " e m a i l " = > " b e s t i e @ g m a i l . c o
  37. Now we have control over the implementation of our authentication

    mechanism. Feature tests that are not concerned with authentication can skip the process vastly reducing setup overhead.
  38. DI has helped us Isolate complex components Reduce coupling Reduce

    if statements Improve tests Features for cheap through re-use
  39. WHO WAS THAT GUY? STEPHEN BEST Freelance consultant @thebestie theaudaciouscodeexperiment.com

    github.com/bestie