Slide 1

Slide 1 text

PRACTICAL DEPENDENCY INJECTION (for Ruby) @thebestie

Slide 2

Slide 2 text

WHAT'S COVERED What is dependency injection? What problems can it solve? How do we write code that takes advantage of it?

Slide 3

Slide 3 text

WHAT IS IT? Something, something, inversion of control technique ...

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

The root cause is COUPLING

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

The sky isn't falling. We still build stuff that works. We still build stuff that makes money. How on earth do we manage?

Slide 8

Slide 8 text

MONKEY PATCHING Let's see how we can decouple from globals and reduce our reliance on monkey patching.

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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?

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Remember what we did? We just got this feature for free.

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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?

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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 '

Slide 27

Slide 27 text

It turns out it was more complex than we thought. JWTs present a few inconveniences: Time sensitive (configurable) Opaquely encoded Optionally encryped HMAC

Slide 28

Slide 28 text

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!

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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.

Slide 38

Slide 38 text

DI has helped us Isolate complex components Reduce coupling Reduce if statements Improve tests Features for cheap through re-use

Slide 39

Slide 39 text

WHO WAS THAT GUY? STEPHEN BEST Freelance consultant @thebestie theaudaciouscodeexperiment.com github.com/bestie