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

Dependency Injection からモックライブラリまで

daimatz
September 11, 2014

Dependency Injection からモックライブラリまで

daimatz

September 11, 2014
Tweet

More Decks by daimatz

Other Decks in Programming

Transcript

  1. Go の気に入らないところ 構文 型推論不要派。 変数の同時代入のとき型注釈をつけたい v a r n i

    n t , e r r e r r o r = b u f . R e a d ( b y t e s ) みたいに書きたい 変数と型は : で区切るでしょ普通 m a p [ i n t ] s t r i n g の特別感 型システム ジェネリクスほしいっす ライブラリのバー ジョニングの話 g o g e t するバー ジョンを指定できない 「 後方互換性をなくすような変更するな」 は理想論… それ以外は大体気に入っている 3 / 31
  2. 例: Twitter ボットアプリケー ション f u n c T w

    e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t s t r u c t { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( ) e r r o r { f o r { s t a t u s I d s , e r r : = t . A c t i o n ( 1 0 ) . . . t i m e . S l e e p ( 6 0 * t i m e . S e c o n d ) } } f u n c ( t * T w i t t e r B o t ) A c t i o n ( c o u n t i n t ) ( [ ] i n t , e r r o r ) { t l , e r r : = G e t T i m e l i n e ( c o u n t ) r e t : = m a k e ( [ ] i n t , 0 ) f o r i : = r a n g e t l { s t a t u s : = t l [ i ] s t a t u s I d , e r r : = T w e e t ( " @ " + s t a t u s . U s e r I d + " G o o d m o r n i n g ! " , s t a t u s . I d ) r e t = a p p e n d ( r e t , s t a t u s I d ) } r e t u r n r e t , n i l } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } b o t . E v e n t L o o p ( ) } 7 / 31
  3. 問題点 T w i t t e r B o

    t が直接 T w e e t , G e t T i m e l i n e に依存している モックして一時的に振る舞いを変えることができない T w i t t e r B o t を単体テストしようとしても必然的に T w e e t および G e t T i m e l i n e の 動作を前提としてしまう 粒度の大きいテストしかできない 粒度の大きいテストしかできないと、 問題が起きたときに問題箇所の特定が難 しくなる 世の中綺麗なことばかりではない。 副作用をモックしないと粒度の細かいテス トはできない 8 / 31
  4. 動的言語での例 Ruby での例 d e s c r i b

    e ' a c t i o n ' d o i t ' r e t u r n [ ] i f t i m e l i n e i s e m p t y ' d o # s t u b e x p e c t ( b o t . s e r v i c e ) . t o r e c e i v e ( : g e t _ t i m e l i n e ) . w i t h ( 1 0 ) . a n d _ r e t u r n ( [ ] ) e x p e c t ( b o t . a c t i o n ( 1 0 ) ) . t o e q [ ] e n d e n d 後から簡単に挙動を変えられるが、 静的言語ではそうはいかない 9 / 31
  5. 改善案: メソッドを引数に渡してみる f u n c ( t * T

    w i t t e r B o t ) E v e n t L o o p ( t w e e t f u n c ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) , g e t T i m e l i n e f u n c ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) , ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } b o t . E v e n t L o o p ( T w e e t , G e t T i m e l i n e ) } こうすれば、 テスト時に t w e e t と g e t T i m e l i n e の挙動を変えることができる モックして単体テストできる 究極的には、 依存しているメソッドを全部引数に渡せばいい でも面倒 この例だと E v e n t L o o p が1 回しか呼ばれてないからいいけど… なんで呼び出し側がモックしたいメソッドを気にかけてやんなきゃいけな いの? 10 / 31
  6. 改善案: メソッドをまとめたオブジェ クトを渡してみる t y p e T w i

    t t e r S e r v i c e s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( s e r v i c e * T w i t t e r S e r v i c e ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } s e r v i c e : = & T w i t t e r S e r v i c e { . . . } b o t . E v e n t L o o p ( s e r v i c e ) } 呼び出しは少し楽になったけど、 モックができない T w i t t e r S e r v i c e という実態に依存してしまっている 差し替えのためには実態でなく型シグネチャ ( インター フェー ス) を指定しなけ ればいけない 11 / 31
  7. 改善案: 依存しているインター フェー スを渡してみる t y p e T w

    i t t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( s e r v i c e T w i t t e r S e r v i c e ) e r r o r { . . . } f u n c m a i n ( ) { b o t : = & T w i t t e r B o t { . . . } s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t . E v e n t L o o p ( s e r v i c e ) } 差し替えがきくようになった 「T w i t t e r S e r v i c e のメソッドを実装しているものをちょうだい」 と言って いるだけで、 具体的な実装は指定していない ついでにこれオブジェクトの初期化時に渡して保持しとけばいいよね 12 / 31
  8. 改善案: 初期化時に渡してみる t y p e T w i t

    t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t s t r u c t { T w i t t e r S e r v i c e T w i t t e r S e r v i c e , . . . } f u n c ( t * T w i t t e r B o t ) E v e n t L o o p ( ) e r r o r { . . . } f u n c m a i n ( ) { s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t : = & T w i t t e r B o t { s e r v i c e , . . . } b o t . E v e n t L o o p ( ) } 依存オブジェクトの差し替えはきくし、 呼び出しも簡単 このように、 依存しているオブジェクトを具体的に保持せず、 初期化時に注入 する手法を Dependency Injection (DI) という 13 / 31
  9. 改善案: 徹底的にやる 本当は T w i t t e r

    B o t も別のオブジェクトに依存しているかもしれない。 T w i t t e r B o t もインター フェー スと実装を分けておく t y p e T w i t t e r S e r v i c e i n t e r f a c e { T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) } t y p e T w i t t e r S e r v i c e I m p l s t r u c t { } f u n c ( t * T w i t t e r S e r v i c e I m p l ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { . . . } f u n c ( t * T w i t t e r S e r v i c e I m p l ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { . . . } t y p e T w i t t e r B o t i n t e r f a c e { . . . } t y p e T w i t t e r B o t I m p l s t r u c t { T w i t t e r S e r v i c e T w i t t e r S e r v i c e , . . . } f u n c ( t * T w i t t e r B o t I m p l ) E v e n t L o o p ( ) e r r o r { . . . } f u n c m a i n ( ) { s e r v i c e : = & T w i t t e r S e r v i c e I m p l { . . . } b o t : = & T w i t t e r B o t I m p l { s e r v i c e , . . . } . . . / / 他の T w i t t e r B o t を使うオブジェクトを生成 b o t . E v e n t L o o p ( ) } 14 / 31
  10. アプリケー ション初期化問題 各モジュー ルでインター フェー スに対するプログラミングはできたが、 オブジ ェクトの実態はどこにあるのか? アプリケー ション起動時に実際にオブジェクトを生成し、

    順番に依存オブジェ クトを注入してやる必要がある アプリケー ションの初期化 注入されるオブジェクト間の依存性は有向無閉路グラフ (Directed Acyclic Graph, DAG) として表される DAG といえばトポロジカルソー ト UNIX にはトポロジカルソー トするための t s o r t というコマンドがあ ります ( これ書いてて知った!) 17 / 31
  11. 依存グラフとトポロジカルソー ト $ c a t d a g .

    i n T w i t t e r S e r v i c e T w i t t e r B o t A p p l i c a t i o n L o g g e r T w i t t e r S e r v i c e T h r e a d P o o l T w i t t e r B o t U s e r S t r e a m S e r v i c e T w i t t e r S e r v i c e U s e r C o n f i g A p p l i c a t i o n L o g g e r T w i t t e r C r e d e n t i a l T w i t t e r S e r v i c e A p i P a r s e r T w i t t e r S e r v i c e $ t s o r t < d a g . i n A p i P a r s e r T h r e a d P o o l T w i t t e r C r e d e n t i a l U s e r C o n f i g U s e r S t r e a m S e r v i c e A p p l i c a t i o n L o g g e r T w i t t e r S e r v i c e T w i t t e r B o t 18 / 31
  12. 初期化コー ド f u n c m a i n

    ( ) { a p i P a r s e r : = & A p i P a r s e r I m p l { . . . } t h r e a d P o o l : = & T h r e a d P o o l I m p l { . . . } t w i t t e r C r e d e n t i a l : = & T w i t t e r C r e d e n t i a l I m p l { . . . } u s e r C o n f i g : = & U s e r C o n f i g I m p l { . . . } u s e r S t r e a m S e r v i c e : = & U s e r S t r e a m S e r v i c e I m p l { . . . } a p p l i c a t i o n L o g g e r : = & A p p l i c a t i o n L o g g e r I m p l { u s e r C o n f i g , . . . } t w i t t e r S e r v i c e : = & T w i t t e r S e r v i c e I m p l { t w i t t e r C r e d e n t i a l , a p p l i c a t i o n L o g g e r , u s e r S t r e a m S e r v i c e , a p i P a r s e r , . . . } t w i t t e r B o t : = & T w i t t e r B o t I m p l { u s e r C o n f i g , t w i t t e r S e r v i c e , t h r e a d P o o l , . . . } t w i t t e r B o t . E v e n t L o o p ( ) } めんどくさいしミスりそう 19 / 31
  13. DI コンテナ 依存性を管理していい感じに扱うためのライブラリ Google Guice (Java) など https://github.com/google/guice Motivation の記事はとてもよくまとまっているので読むといい

    https://github.com/google/guice/wiki/Motivation p u b l i c c l a s s B i l l i n g M o d u l e e x t e n d s A b s t r a c t M o d u l e { @ O v e r r i d e p r o t e c t e d v o i d c o n f i g u r e ( ) { b i n d ( T r a n s a c t i o n L o g . c l a s s ) . t o ( D a t a b a s e T r a n s a c t i o n L o g . c l a s s ) ; b i n d ( C r e d i t C a r d P r o c e s s o r . c l a s s ) . t o ( P a y p a l C r e d i t C a r d P r o c e s s o r . c l a s s ) ; b i n d ( B i l l i n g S e r v i c e . c l a s s ) . t o ( R e a l B i l l i n g S e r v i c e . c l a s s ) ; } } p u b l i c c l a s s R e a l B i l l i n g S e r v i c e i m p l e m e n t s B i l l i n g S e r v i c e { p r i v a t e f i n a l C r e d i t C a r d P r o c e s s o r p r o c e s s o r ; p r i v a t e f i n a l T r a n s a c t i o n L o g t r a n s a c t i o n L o g ; @ I n j e c t p u b l i c R e a l B i l l i n g S e r v i c e ( C r e d i t C a r d P r o c e s s o r p r o c e s s o r , T r a n s a c t i o n L o g t r a n s a c t i o n L o g ) { t h i s . p r o c e s s o r = p r o c e s s o r ; t h i s . t r a n s a c t i o n L o g = t r a n s a c t i o n L o g ; } } 20 / 31
  14. インター フェー ス実装してモック (Java) @ T e s t p

    u b l i c v o i d a c t i o n W i t h E m p t y T i m e l i n e ( ) { T w i t t e r S e r v i c e s e r v i c e = n e w T w i t t e r S e r v i c e ( ) { L i s t < S t a t u s > g e t T i m e l i n e ( i n t c o u n t ) { r e t u r n n e w L i s t < S t a t u s > ( ) ; } i n t t w e e t ( S t r i n g t e x t , i n t i n R e p l y T o ) { t h r o w n e w R u n t i m e E x c e p t i o n ( ) ; } } ; T w i t t e r B o t b o t = n e w T w i t t e r B o t I m p l ( s e r v i c e ) ; a s s e r t T h a t ( b o t . a c t i o n ( 1 0 ) . s i z e , i s ( 0 ) ) ; } 25 / 31
  15. インター フェー ス実装してモック (Go) t y p e M o

    c k S e r v i c e s t r u c t { } f u n c ( x * M o c k S e r v i c e ) G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { r e t u r n m a k e ( [ ] S t a t u s , 0 ) , n i l } f u n c ( t * M o c k S e r v i c e ) T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { p a n i c ( " " ) } f u n c T e s t A c t i o n ( t * t e s t i n g . T ) { s e r v i c e : = & M o c k S e r v i c e { } b o t : = & T w i t t e r B o t I m p l { s e r v i c e } t l , _ : = b o t . A c t i o n ( 1 0 ) i f l e n ( t l ) ! = 0 { t . E r r o r ( " f a i l " ) } } Go の場合、 どのインター フェー スを実装しているかを明示的に書かない ダックタイピング Structural Subtyping もし Go に匿名インター フェー ス構文があれば次のように書ける s e r v i c e : = & i n t e r f a c e { G e t T i m e l i n e ( c o u n t i n t ) ( [ ] S t a t u s , e r r o r ) { r e t u r n m a k e ( [ ] S t a t u s , 0 ) , n i l } T w e e t ( t e x t s t r i n g , i n R e p l y T o i n t ) ( i n t , e r r o r ) { p a n i c ( " " ) } } 26 / 31
  16. gomock 使い方 $ m k d i r $ G

    O P A T H / s r c / g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e # リポジトリ名。 なんでもいい $ c d $ G O P A T H / s r c / g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e $ m k d i r a # ディレクトリ名。 なんでもいい $ c a t > a / b . g o # ファイル名。 なんでもいい p a c k a g e a / / ディレクトリ名と同じ t y p e I i n t e r f a c e { F ( ) s t r i n g } / / 名前に制限あり? X だとダメだった $ m k d i r m o c k _ a # m o c k _ ディレクトリ名 $ m o c k g e n g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e / a \ # リポジトリ名/ ディレクトリ名 I > m o c k _ a / x . g o # インター フェー ス名 > m o c k _ ディレクトリ名/ なんでもいい $ c a t > a / b _ t e s t . g o # ファイル名_ t e s t . g o p a c k a g e a _ t e s t / / ディレクトリ名_ t e s t i m p o r t ( " t e s t i n g " " c o d e . g o o g l e . c o m / p / g o m o c k / g o m o c k " " g i t h u b . c o m / d a i m a t z / g o m o c k _ u s a g e / m o c k _ a " / / リポジトリ名/ m o c k _ ディレクトリ名 ) f u n c T e s t I ( t * t e s t i n g . T ) { c t r l : = g o m o c k . N e w C o n t r o l l e r ( t ) d e f e r c t r l . F i n i s h ( ) i : = m o c k _ a . N e w M o c k I ( c t r l ) / / N e w M o c k インター フェー ス名 i . E X P E C T ( ) . F ( ) . R e t u r n ( " m o c k e d ! " ) i f i . F ( ) ! = " m o c k e d ! " { t . E r r o r ( " f a i l e d " ) } } $ c d a ; g o t e s t 29 / 31
  17. 他言語のモックライブラリとの比較 Java などのモックライブラリと比べるとかなり面倒 Mockito の例 T w i t t

    e r S e r v i c e s e r v i c e = M o c k i t o . m o c k ( T w i t t e r S e r v i c e . c l a s s ) ; M o c k i t o . w h e n ( s e r v i c e . g e t T i m e l i n e ( 1 0 ) ) . t h e n R e t u r n ( n e w L i s t ( ) ) ; Go にはジェネリクスがなく、 インター フェー スが第一級でないため、 ソー ス コー ド内で簡単にモックを生成することができない? s e r v i c e : = g o m o c k . m o c k [ T w i t t e r S e r v i c e ] ( ) / / ジェネリクスがない s e r v i c e : = g o m o c k . m o c k ( T w i t t e r S e r v i c e ) / / インター フェー ス名を渡せない 30 / 31