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

クリーンアーキテクチャ について理解したこと

クリーンアーキテクチャ について理解したこと

sendai.go の Go conference で発表した資料です。

あべてつ

October 15, 2022
Tweet

More Decks by あべてつ

Other Decks in Programming

Transcript

  1. 1. クリーンアーキテクチャを知ったきっかけ Appify にはコーディングルールがあり、ファイルが domain 層や Application 層などに分かれている 層ごとに、どういう処理を書くのかが決まっており、ルールがある。 今までプログラミングをする時は大体

    1 ファイルに完結していたので、衝撃を受けた。 他の設計思想やアーキテクチャに興味が出て、調べるようになった。 クリーンアーキテクチャという設計思想があったので、それについて今回は発表する。
  2. func (s *API) GetSignUp(w http.ResponseWriter, r *http.Request) { ctx :=

    r.Context() // header から必要な情報を取得 headerName := r.Header.Get("name") headerAddress := r.Header.Get("address") headerPassword := r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.db.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.db.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id, Comment, Friend_ID, created_at ,updated_at) VALUES (?,?,?,?,?) " _, err = s.db.ExecContext(ctx, query3, userID, "こんにちは!", "", s.now(), s.now()) // レスポンスを返す responseGetSignUp := &ResponseGetSignUp{ Success: true, } if err := json.NewEncoder(w).Encode(&responseGetSignUp); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) writeHTTPError(w, http.StatusInternalServerError) return } }
  3. func (s *API) GetSignUp(w http.ResponseWriter, r *http.Request) { ctx :=

    r.Context() // header から必要な情報を取得 headerName := r.Header.Get("name") headerAddress := r.Header.Get("address") headerPassword := r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.db.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.db.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id, Comment, Friend_ID, created_at ,updated_at) VALUES (?,?,?,?,?) " _, err = s.db.ExecContext(ctx, query3, userID, "こんにちは!", "", s.now(), s.now()) // レスポンスを返す responseGetSignUp := &ResponseGetSignUp{ Success: true, } if err := json.NewEncoder(w).Encode(&responseGetSignUp); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) writeHTTPError(w, http.StatusInternalServerError) return } }
  4. func (s *API) GetSignUp(w http.ResponseWriter, r *http.Request) { ctx :=

    r.Context() // header から必要な情報を取得 headerName := r.Header.Get("name") headerAddress := r.Header.Get("address") headerPassword := r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.db.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.db.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id, Comment, created_at ,updated_at) VALUES (?,?,?,?) " _, err = s.db.ExecContext(ctx, query3, userID, "こんにちは!", s.now(), s.now()) // レスポンスを返す responseGetSignUp := &ResponseGetSignUp{ Success: true, } if err := json.NewEncoder(w).Encode(&responseGetSignUp); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) writeHTTPError(w, http.StatusInternalServerError) return } }
  5. func (s *ServiceInput) SignUpUserInputPort(ctx context.Context) { // データベースの処理をする success :=

    s.Repository.SignUpUserRepository(ctx) // レスポンスを返す s.OutputPort.SignUpUserOutputPort(success) } func (s ServiceRepo) SignUpUserRepository(ctx context.Context) bool { // header を取得 headerName := s.r.Header.Get("name") headerAddress := s.r.Header.Get("address") headerPassword := s.r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.conn.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id,Comment ,Friend_ID ,created_at ,updated_at) VALUES (?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query3, userID, "こんにちは! ", "test_1234", s.now(), s.now()) return true } func (s *ServiceOutput) GetUsersOutputPort(users []entity.User) { type getUser struct { Name string `json:"name"` ID string `json:"ID"` ChatNumber int `json:"chatNumber"` } res := struct { User []getUser `json:"Users"` }{} var getUsers []getUser for _, e := range users { getUsers = append(getUsers, getUser{ Name: e.Name, ID: e.ID, ChatNumber: e.ChatNumber, }) } res.User = getUsers if err := json.NewEncoder(s.w).Encode(&res); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) entity.WriteHTTPError(s.w, http.StatusInternalServerError) } } Usecase 層 Adapter 層 Gateway
  6. func (s *ServiceInput) SignUpUserInputPort(ctx context.Context) { // データベースの処理をする success :=

    s.Repository.SignUpUserRepository(ctx) // レスポンスを返す s.OutputPort.SignUpUserOutputPort(success) } func (s ServiceRepo) SignUpUserRepository(ctx context.Context) bool { // header を取得 headerName := s.r.Header.Get("name") headerAddress := s.r.Header.Get("address") headerPassword := s.r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.conn.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id,Comment ,Friend_ID ,created_at ,updated_at) VALUES (?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query3, userID, "こんにちは! ", "test_1234", s.now(), s.now()) return true } func (s *ServiceOutput) GetUsersOutputPort(users []entity.User) { type getUser struct { Name string `json:"name"` ID string `json:"ID"` ChatNumber int `json:"chatNumber"` } res := struct { User []getUser `json:"Users"` }{} var getUsers []getUser for _, e := range users { getUsers = append(getUsers, getUser{ Name: e.Name, ID: e.ID, ChatNumber: e.ChatNumber, }) } res.User = getUsers if err := json.NewEncoder(s.w).Encode(&res); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) entity.WriteHTTPError(s.w, http.StatusInternalServerError) } } UseCase 層 Adapter 層 Presenter
  7. func (s *ServiceInput) SignUpUserInputPort(ctx context.Context) { // データベースの処理をする success :=

    s.Repository.SignUpUserRepository(ctx) // レスポンスを返す s.OutputPort.SignUpUserOutputPort(success) } func (s ServiceRepo) SignUpUserRepository(ctx context.Context) bool { // header を取得 headerName := s.r.Header.Get("name") headerAddress := s.r.Header.Get("address") headerPassword := s.r.Header.Get("password") // address が以前登録されたものと一致しないか確認 query := "select count(*) from user where address = ?" rows, err := s.conn.QueryContext(ctx, query, headerAddress) // ユーザー登録 query2 := "INSERT INTO user (id, name, address, status, password, chat_number, token, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query2, userID, headerName, headerAddress, "online", headerPassword, 0, "", s.now(), s.now()) query3 := "INSERT INTO User_Profile (id,Comment ,Friend_ID ,created_at ,updated_at) VALUES (?,?,?,?,?) " _, err = s.conn.ExecContext(ctx, query3, userID, "こんにちは! ", "test_1234", s.now(), s.now()) return true } func (s *ServiceOutput) GetUsersOutputPort(users []entity.User) { type getUser struct { Name string `json:"name"` ID string `json:"ID"` ChatNumber int `json:"chatNumber"` } res := struct { User []getUser `json:"Users"` }{} var getUsers []getUser for _, e := range users { getUsers = append(getUsers, getUser{ Name: e.Name, ID: e.ID, ChatNumber: e.ChatNumber, }) } res.User = getUsers if err := json.NewEncoder(s.w).Encode(&res); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) entity.WriteHTTPError(s.w, http.StatusInternalServerError) } }
  8. 例2: 出力を Json じゃなくて CSV にしてほしいなぁってなったとき… { "Users": [ {

    "name": "テストユーザー", "ID": "test_1234", "chatNumber": 10 }, { "name": "テストユーザー", "ID": "test_5678", "chatNumber": 10 } ] } Users/name,Users/ID,Users/chatNumber テストユーザー,test_1234,10 テストユーザー,test_5678,10
  9. func (s *API) GetUsers(w http.ResponseWriter, r *http.Request) { ctx :=

    r.Context() query := "SELECT * FROM user" rows, err := s.db.QueryContext(ctx, query) users := make([]ResponceGetUser, 0) for rows.Next() { var v User err := rows.Scan(...) user := ResponceGetUser{ ID: v.ID, Name: v.Name, Status: v.Status, ChatNumber: v.ChatNumber, CreatedAT: v.CreatedAT, } users = append(users, user) } resp := &ResponseGetUsers{ Users: users, } // レスポンスを返す。 if err := json.NewEncoder(w).Encode(&resp); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) writeHTTPError(w, http.StatusInternalServerError) return } } 今回は GetUser(/api/users) を例に挙げているが、そ れに限った話ではなく、全てのエンドポイントで CSV に 出力するという処理に変える必要がある。 全ての関数に対して、どこで処理を返しているのかを見 つけて、編集する必要があるから大変! // レスポンスを返す。 if err := json.NewEncoder(w).Encode(&resp); err != nil { log.Printf("[ERROR] response encoding failed: %+v", err) writeHTTPError(w, http.StatusInternalServerError) return }
  10. // 1. 情報を受け取る type ServiceInputPort interface { GetUsersInputPort(context.Context) LoginUserInputPort(context.Context) SignUpUserInputPort(ctx

    context.Context) EditProfileInputPort(context.Context) } // 2. 情報を処理する type ServiceRepository interface { GetUsersRepository(context.Context) []entity.User LoginUserRepository(context.Context) (bool, *entity.User) SignUpUserRepository(ctx context.Context) bool EditProfileRepository(ctx context.Context) bool } // 3. 情報を出力する type ServiceOutputPort interface { GetUsersOutputPort([]entity.User) ErrorOutputPort(error) LoginUserOutputPort(bool, *entity.User) SignUpUserOutputPort(bool) EditProfileOutputPort(bool) }
  11. package presenters import ( "fmt" "server/server/entity" "server/server/usecase/port" ) type CSVOutput

    struct { } func NewCSVOutputPort() port.ServiceOutputPort { return &CSVOutput{} } func (C CSVOutput) GetUsersOutputPort(users []entity.User) { fmt.Println("csv 出力をする") }
  12. 簡単?に実装を変更することができた! ・慣れないと大変だけど、簡単に HTTP レスポンスから CSV に変更することができる。 -> 元々あったコードを変えるのではなく、切り替えるイメージ ・基本的には、出力を担う Presenters

    を主に編集し、あとは output port の切り替えを、driver 層で行うだけで処理 を変えることができる。 ・今回は HTTP → CSV の例を挙げたが、MySQL → SQLite にするとしても、Gateway 層を変えるだけで済む。仕 様変更が楽だと思った しかし今回はサービスが小さく、通常の実装の方を一個一個リファクタした方が早くできそうだった …
  13. 考えるのが多くて慣れないとめっちゃ大変! 例えば、inputport の実装を書いている interactor ファイルには、具体的な実装 (HTTP や MySQL に依存する コード)

    を書かない。 func (s *ServiceInput) GetUsersInputPort(ctx context.Context) { users := s.Repository.GetUsersRepository(ctx) s.OutputPort.GetUsersOutputPort(users) } UseCase は基本的に具体的な実装 はしない!