Slide 1

Slide 1 text

クリーンアーキテクチャ について理解したこと 法政大学 情報科学部 3 年 阿部哲也

Slide 2

Slide 2 text

1. クリーンアーキテクチャを知ったきっかけ Appify にはコーディングルールがあり、ファイルが domain 層や Application 層などに分かれている 層ごとに、どういう処理を書くのかが決まっており、ルールがある。 今までプログラミングをする時は大体 1 ファイルに完結していたので、衝撃を受けた。 他の設計思想やアーキテクチャに興味が出て、調べるようになった。 クリーンアーキテクチャという設計思想があったので、それについて今回は発表する。

Slide 3

Slide 3 text

2. クリーンアーキテクチャのメリットとデメリット メリット! usecase 層が分離されているから、仕様の変更が楽! デメリット! クリーンアーキテクチャに慣れていないので、抽象的な コードを書く場所、具体的なコードを書く場所を考えてプ ログラミングをするのが大変 by Takuya Ueda 
 The Gopher character is based on the Go mascot designed by Renée French .

Slide 4

Slide 4 text

usecase 層が分離されているから、仕様の変更が楽! 例1: DB のカラムを変えたい時...

Slide 5

Slide 5 text

通常の実装の場合

Slide 6

Slide 6 text

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 } }

Slide 7

Slide 7 text

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 } }

Slide 8

Slide 8 text

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 } }

Slide 9

Slide 9 text

クリーンアーキテクチャの場合

Slide 10

Slide 10 text

クリーンアーキテクチャと通常の実装の違い

Slide 11

Slide 11 text

クリーンアーキテクチャと通常の実装の違い DB の処理をする repository port 出力の処理をする output port

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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) } }

Slide 15

Slide 15 text

例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

Slide 16

Slide 16 text

通常の実装の場合

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

クリーンアーキテクチャの場合

Slide 19

Slide 19 text

// 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) }

Slide 20

Slide 20 text

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 出力をする") }

Slide 21

Slide 21 text

func NewOutputFactory() controller.OutputFactory { return presenters.NewServiceOutputPort } func NewOutputFactory() controller.OutputFactory { return presenters.NewCSVOutputPort }

Slide 22

Slide 22 text

簡単?に実装を変更することができた! ・慣れないと大変だけど、簡単に HTTP レスポンスから CSV に変更することができる。 -> 元々あったコードを変えるのではなく、切り替えるイメージ ・基本的には、出力を担う Presenters を主に編集し、あとは output port の切り替えを、driver 層で行うだけで処理 を変えることができる。 ・今回は HTTP → CSV の例を挙げたが、MySQL → SQLite にするとしても、Gateway 層を変えるだけで済む。仕 様変更が楽だと思った しかし今回はサービスが小さく、通常の実装の方を一個一個リファクタした方が早くできそうだった …

Slide 23

Slide 23 text

考えるのが多くて慣れないとめっちゃ大変! 例えば、inputport の実装を書いている interactor ファイルには、具体的な実装 (HTTP や MySQL に依存する コード) を書かない。 func (s *ServiceInput) GetUsersInputPort(ctx context.Context) { users := s.Repository.GetUsersRepository(ctx) s.OutputPort.GetUsersOutputPort(users) } UseCase は基本的に具体的な実装 はしない!

Slide 24

Slide 24 text

ファイル数や書くコード量が多くなるため、プログラミングは 大変になると感じた。 ディレクトリーの数は 約 4 倍多い!

Slide 25

Slide 25 text

まとめ! クリーンアーキテクチャでプログラミングをすることは、大人数で大規模なサービスであったり、複雑なサービスを 作りたい時は良いと思った。 小規模なサービスだと、複雑になりすぎて逆に分かりにくそう … 今回の発表を通してクリーンアーキテクチャについてかなり理解を深めることができた。しかし、まだ Mock を 使ったテストの書き方など、わからないところが多いので、これからもっと勉強したいと思った!