Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える

19,575 views

Published on

devfest tokyo 2017

Published in: Software
  • Hello! Get Your Professional Job-Winning Resume Here - Check our website! https://vk.cc/818RFv
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える

  1. 1. Goのサーバサイド実装における レイヤ設計とレイヤ内実装について 考える
  2. 2. 自己紹介 twitter pospome 読み方 ポスポメ 職種 サーバサイドエンジニア 興味 クラス設計全般, DDD ここら辺の技術に興味ある方は   フォローしてくださると嬉しいです
  3. 3. 発表する前に ・基本的にはレイヤ構造の話なのでDDDは関係ありませんが、  微妙にDDDに関する単語や概念が出てきます ・マサカリ歓迎です  「これは良い」「これは間違っている」などなど  twitterでご意見いただければと思います ・後からスライド単体でも見直せるように文字多めです
  4. 4. 目次 レイヤとは? レイヤ設計について レイヤ内実装について
  5. 5. 目次 レイヤとは? レイヤ設計について レイヤ内実装について
  6. 6. コードを性質別にザックリと分けるモジュール戦略 代表例 ・レイヤアーキテクチャ ・クリーンアーキテクチャ ・ヘキサゴナルアーキテクチャ
  7. 7. なぜレイヤが必要なのか?
  8. 8. 説明不要だと思いますが・・・ ・コードの依存関係を整理できる ・レイヤ間の差し替えを担保することができる ・レイヤ内のパッケージの凝集度を高めることができる
  9. 9. コード量が多くなってくると、 何がどこに影響するのか? を管理しづらくなり、 可読性も低下する システムの保守性が低下しないようにするためのアプローチ
  10. 10. 目次 レイヤとは? レイヤ設計について レイヤ内実装について
  11. 11. どういうレイヤ設計が適切なのか?
  12. 12. UI レイヤアーキテクチャ(DIP適用) アプリケーション モデル インフラストラクチャ
  13. 13. クリーンアーキテクチャ Entities Use Cases Controllers, Gateways, Presenters UI, DB, Web, Devices
  14. 14. ヘキサゴナルアーキテクチャ Application Adapter
  15. 15. 大体どれも似たような感じなので、 そんなに悩まなくて良い印象
  16. 16. 世の中には色々あるが、 大体どれを採用しても以下は担保されると思う ・モデルはどのレイヤにも依存しない ・似たような責務を持ったレイヤがある ・各レイヤ間の依存が単一方向依存になる
  17. 17. pospome が個人的に利用するレイヤ構造を紹介
  18. 18. handler レイヤ構造 usecase domain infra
  19. 19. レイヤ(ディレクトリ)構造 handler presenter usecase input output domain model service repository registry infra dao mail レイヤアーキテクチャをベースに 色々付け足したもの クリーンアーキテクチャになってきた感があるので、 それに寄せようかと思っている
  20. 20. レイヤ(ディレクトリ)構造 adapter handler presenter cmd presenter registry usecase input output domain model service repository infra dao mail adapter を置くこともあるが、 Webサーバ実装だと handler があれば事足りる事が多 いので、 最近は handler だけ置いて、 adapter 欲しくなったら adapter を置いている
  21. 21. ネット上にレイヤ設計の記事が溢れているので、 それを参考に考えていくことができる チームのスキルや要件によって 多少カスタマイズすると思うが、 大体同じような構造に落ち着くと思う
  22. 22. 今さらレイヤ構造について説明しても仕方ない 各レイヤの具体的な実装に踏み込んだ話にしたい
  23. 23. 目次 レイヤとは? レイヤ設計について レイヤ内実装について(今日のメイン)
  24. 24. 以下を保証できない実装になっていると レイヤ構造を導入した意味がない ・コードの依存関係を整理できる ・レイヤ間の差し替えを担保することができる ・レイヤ内のパッケージの凝集度を高めることができる
  25. 25. 実装する時に意識している点を紹介
  26. 26. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  27. 27. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  28. 28. handler レイヤ構造 usecase domain infra
  29. 29. レイヤには責務がある
  30. 30. handler ・HTTPを受け取り、usecase を呼び、結果を出力する ・結果は json だったり、HTML だったりする ・基本的に薄い実装になるが、  HTTPの body, header のパース処理など  場合によっては薄くならない可能性もある
  31. 31. usecase ・いわゆるアプリケーションレイヤ ・システム仕様上のユースケースを表現する  ex. ユーザー登録、アイテム消費、チーム一覧表示 ・handler から呼び出される  1つの handler に対応する専用 usecase が1つ存在する ・基本的に domain を触る
  32. 32. domain ・いわゆるモデルレイヤ ・ドメイン(業務領域)に関する値と振る舞いを持つ ・他のレイヤに依存してはいけない ・DDDの実装パターン別に model, repository, service  というパッケージが存在する
  33. 33. infra ・技術的関心事を扱うレイヤ  ex. DB, Mail, MessageQueue ・直接 handler, usecase から呼ばれることもあるが、  基本的に domain の interface によって抽象化される
  34. 34. レイヤの責務 handler HTTPを受け取り、usecase を呼び、結果を出力する usecase システムのユースケースを満たす処理の流れを実装する domain(model) システムが扱う業務領域に関するコードを置く infra 具体的な技術に関するコードを置く
  35. 35. レイヤの責務 = 改修起点
  36. 36. ・HTTP POST が PUT に変わる  →handler が起点 ・データ保存先が RDB から API になる  →infra が起点 ・ユーザー登録後にメール送信する処理を追加する  →model が起点
  37. 37. handler 改修起点から他レイヤに改修影響が波及していくはず usecase domain infra
  38. 38. 責務があいまいだとどうなる?
  39. 39. 例えば domainレイヤまで http.Request を引き回してしまう場合
  40. 40. type User struct { Name string Address string } func NewUser(r *http.Request) *User { //省略 } http.Request を元に User モデルを生成してしまうかも
  41. 41. handler HTTPに関する変更が全レイヤに影響する可能性 usecase domain infra
  42. 42. HTTP を domain まで引き回してしまうと、 HTTP を触るコードが各レイヤに散ってしまう レイヤのコード量に応じて、 実装対象レイヤを変えるという謎実装になる可能性も
  43. 43. type User struct { Name string Address string } func NewUser(name, address string) *User { //省略 } domain で扱っても問題ない概念に依存させた方がいい
  44. 44. レイヤの責務があいまいだと改修影響を制御できなくなる レイヤの責務を意識して、 レイヤ内で触るオブジェクト(概念)を見極める必要がある
  45. 45. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  46. 46. レイヤは依存する側が変更されても、 依存される側は影響を受けないのが基本
  47. 47. handler レイヤ構造 usecase domain infra
  48. 48. func AddUser(r *http.Request) error { //実装は省略するが、http.Request を触っている想定 } 例えば usecase の引数で http.Request を受け取ってる場合に
  49. 49. デバッグツール用の コマンドラインツール でも usecase を利用したいと思ったら???
  50. 50. handler レイヤ構造 usecase domain infra cmd
  51. 51. func AddUser(r *http.Request) error { //実装は省略するが、http.Request を触っている想定 } cmd で http.Request を生成する必要がある わざわざ http.Request を生成するのは不自然では? usecase が暗黙的に handler に依存している証拠
  52. 52. func AddUser(i input.AddUser) error { //実装は省略する } 個人的に usecase の引数は専用の struct にする
  53. 53. package input type UserAdd struct { Name string Address string TEL string } func (u *UserAdd) CreateUser() (*model.User, error) { //Userモデルを生成する } input struct は可能な限り外部レイヤに依存しない値にする モデルを生成するメソッドも生やす
  54. 54. package input type UserAdd struct { User *model.User } usecase に戻り値がある場合、 同じような理由で専用の struct を用意する 戻り値の struct はモデルを格納するだけの DTO
  55. 55. レイヤ(ディレクトリ)構造 handler presenter usecase input output domain model service repository registry infra dao mail 時間ないので細かく説明しません
  56. 56. ちなみに input, output 用の struct は interface にしません 具体的な struct に依存させるようにしています ・外部レイヤに影響されない実装が前提  影響されるのであれば、実装が間違っている ・振る舞いに着目し、抽象化するものでもない  実装差し替えは不要 過度な抽象化はしない
  57. 57. handler が扱う HTTP という概念に usecase が依存しているのが原因で レイヤがレイヤとして機能していなかった 標準パッケージに依存しているコードは 一見問題ないように思えてしまうことがあるが、 そんなことはない
  58. 58. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  59. 59. 技術的関心事を扱うコード群(infra)は interface で 差し替え可能にするテクニックは有名 具体的な実装に依存しないインターフェースを提供できる 結果的にテストもしやすくなる 機械的にできる実装パターン
  60. 60. interface は抽象として扱う対象がポイント 対象の粒度によってパッケージの安定度が変わる
  61. 61. type UserTable interface { Get(id int) *model.User Add(u *model.User) *model.User Update(u *model.User) *model.User Remove(id int) *model.User } 例えば、ユーザーの CRUD を表現する interface 一見問題なさそう?
  62. 62. type UserTable interface { Get(id int) *model.User Add(u *model.User) *model.User Update(u *model.User) *model.User Remove(id int) *model.User } UserTable = RDBの概念? 実装が RDB から変更されると interface 名を修正する? interface の実装内で memcache とか叩けない?
  63. 63. interface によって抽象を扱うはずが、 命名が具体的なためにいまいち抽象とし扱いきれていない この interface が扱っている抽象は RDBにおけるCRUDの実装であって、 CRUD自体を抽象として扱っているわけではない
  64. 64. 仮に RDB の CRUD 実装のみを抽象化する目的であれば、 この interface 粒度でも問題ない ex. MySQL → PostgreSQL
  65. 65. type UserRepository interface { Get(id int) *model.User Add(u *model.User) *model.User Update(u *model.User) *model.User Remove(id int) *model.User } 抽象的な Repository という命名に変更する CRUD できれば実装は何でもいい
  66. 66. type UserRepository interface { Get(con *db.Con, id int) *model.User } interface の引数、戻り値にも注意 例えば、引数にRDBのコネクションを渡してしまうと、 interface とRDBの結合度が高くなる ・interface を実装する struct に持たせる ・実装内に持たせる *とはいえ、引数とか戻り値は特定の技術に依存してしまうこともあるのでな かなか面倒だったりする。
  67. 67. 以下に気をつけることでinterface の安定度が増し、 interface を利用するコードは変更に強くなる ・interface の命名粒度 ・interface の引数と戻り値 *変更に強くなるだけで変更不要になるわけではない
  68. 68. 「テストをしやすくする」という目的しか持たない人が interface を扱うとこーなる印象 interface を扱う以上、 何を抽象として表現するかを意識するべき
  69. 69. おまけ
  70. 70. Repository 以外の技術的関心事が忘れられがち??? ex. Mail
  71. 71. 技術的関心事を interface として扱うのであれば、 具体的な命名は避けた方がいい SendGrid interface -> Mail interface
  72. 72. レイヤ(ディレクトリ)構造 handler presenter usecase input output domain model service repository registry infra dao mail service or infra に interface を置く 細かい説明は省略 repository 以外の技術的関心事も 忘れないようにした方がいいよというお話
  73. 73. おまけ
  74. 74. infra で発生する error を そのままハンドリングするのはやめた方がいい
  75. 75. package dao type User struct { } func (user *User) Get(id int) (*model.User, error) { return memcache.Get(id) } memcache にアクセスする DAO のコード これだけだと問題なさそう
  76. 76. func GetUser(id int) (*model.User, error) { u, err := dao.User{}.Get(id) if err == memcache.ErrCacheMiss { } } DAOを利用する側のコードが memcache のエラーに依存してしまっている interface 上は error で抽象化されているが、 利用する側のコードで依存してしまうこともあるので注意
  77. 77. package dao type User struct { } func (user *User) Get(id int) (*model.User, error) { u, err := memcache.Get(id) if err == memcache.ErrCacheMiss { return domain.ErrNoSuchModel } } アプリケーション独自の error でラップすればOK
  78. 78. 独自 error の定義で気をつける点は以下 ・error を捕捉するレイヤにとって問題ない抽象度にする  独自例外で ErrMemcache 的なやつを  再定義しても意味がない ・各レイヤの責務ごとに適切な error を考える ・ラップする必要のない error はラップしない
  79. 79. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  80. 80. レイヤ構造を扱うのであれば、 技術的関心事に関する interface と その実装の置き場所は意識しなければならない
  81. 81. type UserRepository interface { Get(id int) *model.User } UserRepository(interface)と・・・
  82. 82. package dao type User struct { } func (user *User) Get(id int) *model.User { } UserRepository(interface)の実装をどこに置く?
  83. 83. レイヤの責務 handler HTTPを受け取り、usecase を呼び、結果を出力する usecase システムのユースケースを満たす処理の流れを実装する domain(model) システムが扱う業務領域に関するコードを置く  ↑  interfaceを置く infra 具体的な技術に関するコードを置く  ↑  interfaceの実装を置く
  84. 84. handler infra を domain に依存させている usecase domain infra
  85. 85. interfaceと実装を domain に置く場合 ・実装が domain に存在するので、  MySQL を WebAPI にするという変更で  domain に修正が必要になる ・domain は業務領域に関するコードを置くレイヤなので、  MySQL が業務領域に関するものでない限り、  domain に修正が入るのはおかしい  修正は infra に閉じられるべき ・実装が infra に存在すれば、これを回避できる
  86. 86. interfaceと実装を infra に置く場合 ・システム仕様に「ユーザー削除」が追加されると、  infra に修正が必要になる ・業務領域に関するコードを置くレイヤである domain に  影響がない ・本来であれば、domain に修正影響が発生し、  その影響が infra に伝搬するはず ・interface が domain に存在すれば、これを回避できる
  87. 87. レイヤの責務を考慮すると、 interface は domain に置き、 実装は infra に置くことになる
  88. 88. ただし、 interface、実装共に infra に置く場合もあるし、 interface すら用意しない場合もある なかなか難しいところ レイヤの責務と抽象の必要性を考慮して 一貫性を持った実装方針であれば問題ないはず
  89. 89. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  90. 90. infra に実装を置き、domain に interface を置き、 実際に usecase から interface を利用してみる interface と実装を結びつけるために registry を利用する
  91. 91. レイヤ(ディレクトリ)構造 handler presenter usecase input output domain model service repository registry infra dao mail
  92. 92. package registry type Repository interface { NewUser() reposiroty.User } type repositoryImpl struct { } func NewRepository() Repository { return &repositoryImpl{} } //引数は interface だが、戻り値は実装になっている func (r *repositoryImpl) NewUser() reposiroty.User { return &dao.User{} } repository 用の registry で 各種 repository を取得できる registry 自体を interface にする ことで、テスト時に必要なメ ソッドだけ実装すればいい service 用の registry もある
  93. 93. レイヤ(ディレクトリ)構造 handler presenter usecase input output domain model service repository registry repository.go service.go infra dao mail
  94. 94. メソッド名で取得対象の repository を明示できるのも利点 実装の関係上、 以下のような Master, Slave の使い分けが必要な場合に役立つ package registry func (r *repositoryImpl) NewUserMaster() reposiroty.User { return &dao.User{“master”} } func (r *repositoryImpl) NewUserSlave() reposiroty.User { return &dao.User{“slave”} }
  95. 95. 具体的な実装を紹介
  96. 96. package handler type User struct { repo registry.Repository } func NewUser(repo registry.Repository) *User { return &User{repo} } func (u *User) Add(w http.ResponseWriter, r *http.Request) { userRepo := u.repo.NewUser() userUsecase := usecase.NewUser(userRepo) } handler は registry を持ち、 registry から repository を取得することができる 取得した repository は usecase にセットする
  97. 97. package main func init() { repo := registry.NewRepository() userHandler := handler.NewUser(repo) http.HandleFunc("/add", userHandler.Add()) } handler の registry は main でセットする *GAEのコードなので、純粋なGo実装とは main の書き方が異なります
  98. 98. package usecase type User struct { userRepo repository.User } func NewUser(u repository.User) *User { return &User{u} } func (user *User) Add() *output.UserAdd { // repository の Add() を呼ぶことができる user.userRepo.Add() } usecase は repository を持つ registry を持たない
  99. 99. handler handler は registry に依存し、 registry は infra に依存している usecase domain infra registry
  100. 100. package usecase type User struct { userRepo repository.User } func NewUser(u repository.User) *User { return &User{u} } func (user *User) Add() *output.UserAdd { // repository の Add() を呼ぶことができる user.userRepo.Add() } なぜ usecase は registry を持たない????
  101. 101. usecase が registry を持つと usecase が依存する repository, service が大きくなってしまう 依存が明確ではなくなってしまう usecase は処理の流れを表現するので、 サービスロケータに依存させたくない
  102. 102. package handler type User struct { repo registry.Repository } func NewUser(repo registry.Repository) *User { return &User{repo} } func (u *User) Add(w http.ResponseWriter, r *http.Request) { userRepo := u.repo.NewUser() userUsecase := usecase.NewUser(userRepo) } なぜ handler は registry を持つのか?
  103. 103. 本来であれば、具体的な repository, service を DIする方がいーとは思うけど handler は domain に依存しないので、 registry に依存させても問題なさそう usecase で利用する repository, service に引っ張られて main & handlerを修正するのが面倒 要は実害なさそうだし、 面倒だから registry を突っ込んでる
  104. 104. こんな感じで infra の実装と domain の interface を 紐付けています
  105. 105. おまけ
  106. 106. package usecase type User struct { userRepo repository.User } func NewUser(u repository.User) *User { return &User{u} } func (user *User) Add() *output.UserAdd { // repository の Add() を呼ぶことができる user.userRepo.Add() } 以下の実装だと、 User の CRUD を User struct に紐付ける感じになり、 User struct が持つ repository, service が肥大化しそうなので、 input struct に持たせよーかなと思ってる
  107. 107. ・レイヤ責務と改修起点とレイヤで扱う概念 ・レイヤ間の実装差し替え ・infraを抽象として扱う ・infraの抽象とレイヤ構造 ・infra実装のDI ・レイヤの必要性
  108. 108. 今まで説明してきたポイントを 考えながら実装できていますか? 「これ考えてなかったな」とかありませんか?
  109. 109. レイヤ実装は予想以上に難易度が高い
  110. 110. システム仕様の複雑さと規模に見合ったリターンを 得ることができなければ 単なる複雑なアーキテクチャで終わってしまう 理解した気で導入すると確実に失敗するので、 事前にプロトタイプ実装とかした方がいいです
  111. 111. 最初は薄いレイヤ構造にして、 耐えられなくなってから改修すればいいのでは?
  112. 112. あとから改修はなかなか上手くいかない印象 ・改修範囲が大きい ・コードが密結合すぎてレイヤに散らすことができない ・改修のタイミングでレイヤの学習コストがかかる  チームにレイヤ知識があれば、問題ないですが ・既存コードがカオスすぎて触りたくない ・新規コードのみレイヤ構造を導入すると、  場所によってアーキテクチャが異なってしまう
  113. 113. システムとチームは時間と共に成長するので、 初期実装時に適切なレイヤを選定するのはなかなか難しい 色々な要素に左右されてしまう ・チームメンバーのレベル ・チームの規模 ・実装言語の仕様 & フレームワークとの相性 ・システム仕様
  114. 114. 伝えたいこと 今回の発表が皆さんのチームにとって最適とは限らない レイヤ設計が必ずしも必要とは限らない
  115. 115. まとめ
  116. 116. ・レイヤ設計におけるルールを可能な限り紹介したつもり ・それらを時間内に全て説明できないので、  スライド内の記述には正確ではない点がある  ex. レイヤの依存関係の図は正確ではない ・それでもなるべく矛盾しないように作ったつもり
  117. 117. ・レイヤ構造は実装によって破綻する ・破綻しててもコードは動くのでなかなか気づきにくい ・このスライドがあらゆるケースで正しい保証もない ・レイヤの責務、依存方向を意識してコードを書こう
  118. 118. ・レイヤ関連の実装パターンは機械的に扱えるので、  エンジニアの実装スキルとしてはあまり価値がないと思う  知ってればいいだけ ・実装スキルが試されるのはドメインレイヤを  どう実装するかだと思っている ・今日は時間なくて、そーゆーところには一切触れてない ・どこかで話す機会あれば話します
  119. 119. おわり

×