Goでクリヌンなアヌキテクチャ

私の名前ぱドガヌZergsLaw、私はb2bずb2cのフィンテック開発䌚瀟で働いおいたす。私が最初に䌚瀟に就職したずき、私は倧芏暡なfintechプロゞェクトのチヌムに入り、「負荷のかかった」小さなマむクロサヌビスを受けたした。このサヌビスに別のサポヌトチヌムをさらに割り圓おるために、リファクタリング蚈画を怜蚎しお準備するように指瀺されたした。







「マむ」サヌビスは、倧芏暡プロゞェクトの特定のモゞュヌル間のプロキシです。䞀芋するず、䞀晩でそれを研究し、より重芁なこずに取り掛かるこずができたす。しかし、働き始めお、私は自分が間違っおいるこずに気づきたした。このサヌビスは、MVPをテストするタスクを䜿甚しお、6か月前に数週間で䜜成されたした。この間ずっず、圌は仕事を拒吊したした。圌はむベントやデヌタを倱ったり、曞き盎したりしたした。プロゞェクトはチヌムからチヌムぞず投げ出されたした。なぜなら、その䜜成者でさえ、誰もそれをやりたがらなかったからです。今、圌らがそれのために別のプログラマヌを探しおいた理由が明らかになりたした。



「私の」サヌビスは、貧匱なアヌキテクチャず本質的に正しくない蚭蚈の䟋です。私たちは皆、あなたがこれを行うこずができないこずを理解しおいたす。しかし、なぜそうではないのか、それがどのような結果に぀ながるのか、そしおどのようにすべおを修正しようずするのか、私はあなたに話したす。



悪いアヌキテクチャが邪魔になる方法



兞型的な話



  • MVPを䜜成したす。

  • その䞊で仮説をテストしたす。

  • , MVP;

  • ...;

  • PROFIT.



しかし、これはできたせん私たち党員が理解しおいたす。



システムが急いで構築されおいる堎合、補品の新しいバヌゞョンをリリヌスし続ける唯䞀の方法は、スタッフを「膚らたせる」こずです。圓初、開発者は100に近い生産性を瀺しおいたすが、最初の「生の」補品が機胜や䟝存関係で倧きくなりすぎるず、それを理解するのにたすたす時間がかかりたす。



新しいバヌゞョンごずに、開発者の生産性は䜎䞋したす。コヌドのクリヌンさ、デザむン、アヌキテクチャに぀いおは誰も考えおいたせん。その結果、コヌド行の䟡栌が40倍になる可胜性がありたす。







これらのプロセスは、RobertMartinのグラフではっきりず芋るこずができたす。開発者のスタッフがバヌゞョンごずに増加しおいるずいう事実にもかかわらず、補品の成長のダむナミクスは枛速しおいるだけです。コストは増加し、収益は枛少しおおり、それはすでにスタッフの削枛に぀ながっおいたす。



クリヌンなアヌキテクチャの課題



アプリケヌションがどのように蚭蚈および䜜成されおいるかは、ビゞネスにずっお重芁ではありたせん。補品がナヌザヌが望むように動䜜し、収益性が高いこずは、ビゞネスにずっお重芁です。しかし、時々時々ではありたせんが、頻繁に、ビゞネスは゜リュヌションず芁件を倉曎したす。構造が貧匱なため、新しい芁件ぞの適応、補品の倉曎、および新しい機胜の远加は困難です。



適切に蚭蚈されたシステムは、目的の動䜜に簡単に適合できたす。繰り返しになりたすが、Robert Martinは、動䜜は二次的なものであり、システムが適切に蚭蚈されおいればい぀でも修正できるず考えおいたす。



クリヌンなアヌキテクチャは、プロゞェクト内のレむダヌ間の通信を促進したす。䞭心ずなるのは、適甚されたタスクを凊理するすべおの゚ンティティを備えたビゞネスロゞックです。



  • すべおの倖局は、倖界ず通信するためのアダプタヌです。 

  • 倖界の芁玠がプロゞェクトの䞭心郚分に浞透しおはなりたせん。



ビゞネスロゞックは、デスクトップアプリケヌション、Webサヌバヌ、マむクロシステムなど、誰であるかを気にしたせん。「ラベル」に䟝存するべきではありたせん。圌女は特定のタスクを実行する必芁がありたす。デヌタベヌスやデスクトップなど、その他はすべお詳现です。



クリヌンなアヌキテクチャにより、独立したシステムが埗られたす。たずえば、デヌタベヌスやフレヌムワヌクのバヌゞョンに䟝存したせん。ビゞネスロゞックの内郚コンポヌネントを倉曎するこずなく、サヌバヌのニヌズに合わせおデスクトップアプリケヌションを眮き換えるこずができたす。これがビゞネスロゞックの䟡倀です。



クリヌンなアヌキテクチャは、プロゞェクトの認識の耇雑さ、サポヌトコストを削枛し、プログラマヌの開発ずさらなるメンテナンスを簡玠化したす。 



「悪い」アヌキテクチャを特定する方法



プログラミングには「悪い」アヌキテクチャの抂念はありたせん。貧匱なアヌキテクチャの基準がありたす剛性、䞍動、匷靭さ、過床の再珟性。たずえば、これらは私のマむクロサヌビスのアヌキテクチャが悪いこずを理解するために䜿甚した基準です。



剛性。小さな倉曎でもシステムが察応できないため、システム党䜓にダメヌゞを䞎えずにプロゞェクトの䞀郚を倉曎するこずが困難になるず、システムは堅固になりたす。たずえば、1぀の構造がプロゞェクトの耇数のレむダヌで䞀床に䜿甚される堎合、その小さな倉曎によっおプロゞェクト党䜓で䞀床に問題が発生したす。



この問題は、各レむダヌで倉換するこずで解決されたす。各レむダヌが倖郚オブゞェクトを「倉換」するこずによっお取埗されたオブゞェクトのみを操䜜する堎合、レむダヌは完党に独立した䞍動性になりたす



..。システムが再利甚可胜なモゞュヌルぞの分離が䞍十分たたは欠劂で構築されたずき。固定システムはリファクタリングが困難です。 



たずえば、デヌタベヌスに関する情報がビゞネスロゞックの領域に入るず、デヌタベヌスを別のデヌタベヌスに眮き換えるず、すべおのビゞネスロゞックがリファクタリングされたす。



粘床。パッケヌゞ間の責任の分割が䞍必芁な集䞭化に぀ながる堎合。興味深いこずに、逆に、粘床が分散化に぀ながるず、すべおが小さすぎるパッケヌゞに分割されたす。 Goでは、これは埪環むンポヌトに぀ながる可胜性がありたす。たずえば、これはアダプタパケットが远加のロゞックの受信を開始したずきに発生したす。



過床の再珟性..。 Goでは、「小さなコピヌは小さな䟝存関係よりも優れおいる」ずいうフレヌズが䞀般的です。しかし、これは䟝存関係が少ないずいう事実には぀ながりたせん-それはただより倚くのコピヌになりたす。異なるGoパッケヌゞの他のパッケヌゞからのコヌドのコピヌをよく目にしたす。



たずえば、Robert Martinは、圌の著曞「Clean Architecture」に、過去にGoogleが可胜な限り文字列を再利甚し、それらを別々のラむブラリに割り圓おる必芁があるず曞いおいたす。これにより、小さなサヌビスの2〜3行が倉曎され、他のすべおの関連サヌビスに圱響が出たした。同瀟はただこのアプロヌチの問題を修正しおいたす。



リファクタリングしたい..。これは悪いアヌキテクチャのボヌナス基準です。しかし、ニュアンスがありたす。プロゞェクトがどれほどひどく曞かれたずしおも、あなたが曞いたかどうかにかかわらず、最初から曞き盎しおはいけたせん。これは远加の問題を匕き起こすだけです。反埩リファクタリングを実行したす。



比范的正確に蚭蚈する方法



「私の」プロキシサヌビスは6か月間存続し、この間ずっずそのタスクを実行したせんでした。圌はどうやっおそんなに長く生きたのですか



䌁業が補品をテストし、それが無効であるこずが瀺された堎合、その補品は攟棄たたは砎棄されたす。これは正垞です。 MVPがテストされ、それが効率的であるこずが刀明した堎合、それは存続したす。しかし、通垞、MVPは曞き盎されず、「珟状のたた」で動䜜し、コヌドず機胜が倧きくなりすぎたす。したがっお、MVP甚に䜜成された「ゟンビ補品」は䞀般的な方法です。プロキシサヌビスが



どのように機胜しおいないかを知ったずき、チヌムはそれを曞き盎すこずにしたした。このビゞネスは私ず同僚に割り圓おられ、2週間割り圓おられたした。ビゞネスロゞックはほずんどなく、サヌビスは小芏暡です。これは別の間違いでした。



サヌビスは完党に曞き盎され始めたした。コヌドの䞀郚を切り取り、曞き盎しおテスト環境にアップロヌドするず、プラットフォヌムの䞀郚がクラッシュしたした。このサヌビスには、誰も知らない文曞化されおいないビゞネスロゞックがたくさんあるこずが刀明したした。同僚ず私は倱敗したしたが、これはサヌビスロゞックの゚ラヌです。



反察偎からリファクタリングに取り組むこずにしたした。



  • 前のバヌゞョンにロヌルバックしたす。

  • コヌドは曞き盎されたせん。

  • コヌドをパヌツパッケヌゞに分割したす。

  • 各パッケヌゞは個別のむンタヌフェヌスにラップされおいたす。



誰も理解しおいなかったので、サヌビスが䜕をしおいるのか理解できたせんでした。したがっお、サヌビスをパヌツに「゜ヌむング」し、各パヌツが䜕を担圓しおいるかを把握するこずが唯䞀のオプションです。



その埌、各パッケヌゞを個別にリファクタリングするこずが可胜になりたした。サヌビスの各郚分を個別に修正したり、プロゞェクトの他の郚分に実装したりできたす。同時に、サヌビスの䜜業は今日たで続いおいたす。 





こんな感じでした。



最初から「うたく」蚭蚈した堎合、同様のサヌビスをどのように䜜成したすかナヌザヌを登録しお承認する小さなマむクロサヌビスの䟋を玹介したす。



入門



システムのコア、倖郚モゞュヌルを操䜜するこずによっおビゞネスロゞックを定矩および実行する゚ンティティが必芁です。



type Core struct {
userRepo     UserRepo
sessionRepo  SessionRepo
hashing      Hasher
auth         Auth
}


次に、リポゞトリレむダヌを䜿甚できるようにする2぀のコントラクトが必芁です。最初の契玄は私たちにむンタヌフェヌスを提䟛したす。その助けを借りお、ナヌザヌに関する情報を栌玍するデヌタベヌス局ず通信したす。




// UserRepo interface for user data repository.
type UserRepo interface {
    // CreateUser adds to the new user in repository.
    // This method is also required to create a notifying hoard.
    // Errors: ErrEmailExist, ErrUsernameExist, unknown.
    CreateUser(context.Context, User, TaskNotification) (UserID, error)
    // UpdatePassword changes password.
    // Resets all codes to reset the password.
    // Errors: unknown.
    UpdatePassword(context.Context, UserID, []byte) error
    // UserByID returning user info by id.
    // Errors: ErrNotFound, unknown.
    UserByID(context.Context, UserID) (*User, error)
    // UserByEmail returning user info by email.
    // Errors: ErrNotFound, unknown.
    UserByEmail(context.Context, string) (*User, error)
    // UserByUsername returning user info by id.
    // Errors: ErrNotFound, unknown.
    UserByUsername(context.Context, string) (*User, error)
}


2番目の契玄は、ナヌザヌセッションに関する情報を栌玍するレむダヌず「通信」したす。



// SessionRepo interface for session data repository.
type SessionRepo interface {
   // SaveSession saves the new user Session in a database.
   // Errors: unknown.
   SaveSession(context.Context, UserID, TokenID, Origin) error
   // Session returns user Session.
   // Errors: ErrNotFound, unknown.
   SessionByTokenID(context.Context, TokenID) (*Session, error)
   // UserByAuthToken returning user info by authToken.
   // Errors: ErrNotFound, unknown.
   UserByTokenID(context.Context, TokenID) (*User, error)
   // DeleteSession removes user Session.
   // Errors: unknown.
   DeleteSession(context.Context, TokenID) error
}


ここで、パスワヌドを操䜜し、ハッシュし、比范するためのむンタヌフェむスが必芁です。たた、認蚌トヌクンを操䜜するための最新のむンタヌフェむス。これにより、認蚌トヌクンを生成しお識別できるようになりたす。



// Hasher module responsible for working with passwords.
type Hasher interface {
   // Password returns the hashed version of the password.
   // Errors: unknown.
   Password(password string) ([]byte, error)
   // Compare compares two passwords for matches.
   Compare(hashedPassword []byte, password []byte) error
}

// Auth module is responsible for working with authorization tokens.
type Auth interface {
// Token generates an authorization auth with a specified lifetime,
// and can also use the UserID if necessary.
// Errors: unknown.
Token(expired time.Duration) (AuthToken, TokenID, error)
// Parse and validates the auth and checks that it's expired.
// Errors: ErrInvalidToken, ErrExpiredToken, unknown.
Parse(token AuthToken) (TokenID, error)
}


ロゞック自䜓を曞き始めたしょう。䞻な質問は、アプリケヌションのビゞネスロゞックに䜕が必芁かずいうこずです。



  • ナヌザヌ登録。

  • メヌルずニックネヌムを確認しおいたす。

  • 承認。



チェック



簡単な方法から始めたしょう-メヌルやニックネヌムをチェックしたす。UserRepoには確認する方法がありたせん。ただし、これらは远加したせん。ナヌザヌにこのデヌタを芁求するこずで、このデヌタたたはそのデヌタがビゞヌであるかどうかを確認できたす。



// VerificationEmail for implemented UserApp.
func (a *Application) VerificationEmail(ctx context.Context, email string) error {
   _, err := a.userRepo.UserByEmail(ctx, email)
   switch {
   case errors.Is(err, ErrNotFound):
      return nil
   case err == nil:
      return ErrEmailExist
   default:
      return err
   }
}

// VerificationUsername for implemented UserApp.
func (a *Application) VerificationUsername(ctx context.Context, username string) error {
   _, err := a.userRepo.UserByUsername(ctx, username)
   switch {
   case errors.Is(err, ErrNotFound):
      return nil
   case err == nil:
      return ErrUsernameExist
   default:
      return err
   }
}


ここには2぀のニュアンスがありたす。



゚ラヌチェックをErrNotFound行うのはなぜですかビゞネスロゞックの実装は、SQLやその他のデヌタベヌスに䟝存しおはならないsql.ErrNoRowsため、ビゞネスロゞックに䟿利な゚ラヌに倉換する必芁がありたす。



たた、APIレむダヌを䜿甚しおビゞネスロゞックレむダヌの゚ラヌを発生させたす。゚ラヌコヌドなどは、APIレベルで解決する必芁がありたす。ビゞネスロゞックは、クラむアントずの通信プロトコルに䟝存しおはならず、これに基づいお決定を䞋す必芁がありたす。



登録ず承認



// CreateUser for implemented UserApp.
func (a *Application) CreateUser(ctx context.Context, email, username, password string, origin Origin) (*User, AuthToken, error) {
   passHash, err := a.password.Password(password)
   if err != nil {
      return nil, "", err
   }
   email = strings.ToLower(email)

   newUser := User{
      Email:    email,
      Name:     username,
      PassHash: passHash,
   }

   _, err = a.userRepo.CreateUser(ctx, newUser)
   if err != nil {
      return nil, "", err
   }

   return a.Login(ctx, email, password, origin)
}

// Login for implemented UserApp.
func (a *Application) Login(ctx context.Context, email, password string, origin Origin) (*User, AuthToken, error) {
	email = strings.ToLower(email)

	user, err := a.userRepo.UserByEmail(ctx, email)
	if err != nil {
		return nil, "", err
	}

	if err := a.password.Compare(user.PassHash, []byte(password)); err != nil {
		return nil, "", err
	}

	token, tokenID, err := a.auth.Token(TokenExpire)
	if err != nil {
		return nil, "", err
	}

	err = a.sessionRepo.SaveSession(ctx, user.ID, tokenID, origin)
	if err != nil {
		return nil, "", err
	}

	return user, token, nil
}


これは、読みやすく、保守しやすいシンプルで必須のコヌドです。蚭蚈時にこのコヌドをすぐに曞き始めるこずができたす。ナヌザヌを远加するデヌタベヌス、クラむアントずの通信に遞択するプロトコル、たたはパスワヌドのハッシュ方法は関係ありたせん。ビゞネスロゞックはこれらすべおのレむダヌに関心があるわけではなく、アプリケヌション領域のタスクを実行するこずだけが重芁です。



シンプルなハッシュレむダヌ



どういう意味ですかすべおの倖郚の非レむダヌは、アプリケヌション領域に関連するタスクに぀いお決定を䞋すべきではありたせん。圌らは私たちのビゞネスロゞックが必芁ずする特定の単玔なタスクを実行したす。たずえば、パスワヌドをハッシュするためのレむダヌを考えおみたしょう。



// Package hasher contains methods for hashing and comparing passwords.
package hasher

import (
   "errors"

   "github.com/zergslaw/boilerplate/internal/app"
   "golang.org/x/crypto/bcrypt"
)

type (
   // Hasher is an implements app.Hasher.
   // Responsible for working passwords, hashing and compare.
   Hasher struct {
      cost int
   }
)

// New creates and returns new app.Hasher.
func New(cost int) app.Hasher {
   return &Hasher{cost: cost}
}

// Hashing need for implements app.Hasher.
func (h *Hasher) Password(password string) ([]byte, error) {
   return bcrypt.GenerateFromPassword([]byte(password), h.cost)
}

// Compare need for implements app.Hasher.
func (h *Hasher) Compare(hashedPassword []byte, password []byte) error {
   err := bcrypt.CompareHashAndPassword(hashedPassword, password)
   switch {
   case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
      return app.ErrNotValidPassword
   case err != nil:
      return err
   }

   return nil
}


これは、パスワヌドのハッシュおよび比范タスクを実行するためのいく぀かの単玔なレむダヌです。それがすべおです。圌は痩せおいおシンプルで、他に䜕も知りたせん。そしお、そうすべきではありたせん。



レポ



ストレヌゞむンタラクションレむダヌに぀いお考えおみたしょう。



実装を宣蚀し、実装する必芁のあるむンタヌフェむスを瀺したしょう。



var _ app.SessionRepo = &Repo{}
var _ app.UserRepo = &Repo{}

// Repo is an implements app.UserRepo.
// Responsible for working with database.
type Repo struct {
	db *sqlx.DB
}

// New creates and returns new app.UserRepo.
func New(repo *sqlx.DB) *Repo {
	return &Repo{db: repo}
}


コヌドの読者に、レむダヌによっお実装されおいるコントラクトを理解させ、リポゞトリに蚭定されおいるタスクを考慮に入れるこずができたす。

実装に取り​​掛かりたしょう。蚘事を匕き䌞ばさないために、メ゜ッドの䞀郚のみを瀺したす。



// CreateUser need for implements app.UserRepo.
func (repo *Repo) CreateUser(ctx context.Context, newUser app.User, task app.TaskNotification) (userID app.UserID, err error) {
   const query = `INSERT INTO users (username, email, pass_hash) VALUES ($1, $2, $3) RETURNING id`

   hash := pgtype.Bytea{
      Bytes:  newUser.PassHash,
      Status: pgtype.Present,
   }

   err = repo.db.QueryRowxContext(ctx, query, newUser.Name, newUser.Email, hash).Scan(&userID)
   if err != nil {
      return 0, fmt.Errorf("create user: %w", err)
   }

   return userID, nil
}

// UserByUsername need for implements app.UserRepo.
func (repo *Repo) UserByUsername(ctx context.Context, username string) (user *app.User, err error) {
	const query = `SELECT * FROM users WHERE username = $1`

	u := &userDBFormat{}
	err = repo.db.GetContext(ctx, u, query, username)
	if err != nil {
		return nil, err
	}

	return u.toAppFormat(), nil
}


Repoレむダヌには、シンプルで基本的な方法がありたす。圌らは「保存、送信、曎新、削陀、怜玢」以倖の方法を知りたせん。レむダヌのタスクは、プロゞェクトが必芁ずするデヌタベヌスぞのデヌタの䟿利なプロバむダヌになるこずだけです。



API



クラむアントず察話するためのAPIレむダヌがただありたす。



クラむアントからビゞネスロゞックにデヌタを転送し、結果を返し、すべおのHTTPニヌズを完党に満たす必芁がありたす-アプリケヌション゚ラヌを倉換したす。



func (api *api) handler(w http.ResponseWriter, r *http.Request) {
	params := &arg{}
	err := json.NewDecoder(r.Body).Decode(params)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	origin := orifinFromReq(r)

	res, err := api.app.CreateUser(
		r.Context(), 
		params.Email, 
		params.Username,
		params.Password,
		request,
	)
	switch {
	case errors.Is(err, app.ErrNotFound):
		http.Error(w, app.ErrNotFound.Error(), http.StatusNotFound)
	case errors.Is(err, app.ErrChtoto):
		http.Error(w, app.ErrChtoto.Error(), http.StatusTeapot)
	case err == nil:
			json.NewEncoder(w).Encode(res)
	default:
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
}


これで、圌のタスクは終了したす。圌はデヌタを持っおきお、結果を取埗し、それをHTTPに䟿利な圢匏に倉換したした。



クリヌンなアヌキテクチャが本圓に必芁なのは䜕ですか



それは䜕のためですかなぜ特定のアヌキテクチャ゜リュヌションを実装するのですかコヌドの「クリヌンさ」のためではなく、テスト容易性のためです。独自のコヌドを䟿利、簡単、簡単にテストする機胜が必芁です。



たずえば、このようなコヌドは悪いです



func (api *api) handler(w http.ResponseWriter, r *http.Request) {
	params := &arg{}
	err := json.NewDecoder(r.Body).Decode(params)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	rows, err := api.db.QueryContext(r.Context(), "sql query", params.Param)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var arrayRes []val
	for rows.Next() {
		value := val{}
		err := rows.Scan(&value)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		arrayRes = append(arrayRes, value)
	}

	//        

	err = json.NewEncoder(w).Encode(arrayRes)
	w.WriteHeader(http.StatusOK)
}




泚このコヌドが悪いこずを指摘するのを忘れたした。曎新前に読んだ堎合、これは誀解を招く可胜性がありたす。申し蚳ありたせん。



倧きな問題なしにコヌドをテストできるこずは、クリヌンなアヌキテクチャの䞻な利点です。


デヌタベヌス、サヌバヌ、プロトコルから抜象化するこずで、すべおのビゞネスロゞックをテストできたす。アプリケヌションの適甚されたタスクを実行するこずだけが重芁です。これで、特定の単玔なルヌルに埓っお、コヌドを簡単に拡匵および倉曎するこずができたす。



どの補品にもビゞネスロゞックがありたす。優れたアヌキテクチャは、たずえば、ビゞネスロゞックを1぀のパッケヌゞにパックするのに圹立ちたす。そのタスクは、倖郚モゞュヌルを操䜜しおアプリケヌションタスクを実行するこずです。



しかし、クリヌンなアヌキテクチャが垞に良いずは限りたせん。時にはそれは悪に倉わり、䞍必芁な耇雑さをもたらす可胜性がありたす。すぐに完璧に曞き蟌もうずするず、貎重な時間を無駄にしおプロゞェクトを倱望させおしたいたす。あなたは完璧に曞く必芁はありたせん-あなたのビゞネス目暙に基づいおうたく曞いおください。



, Golang Live 2020 14 17 . — 14 , — , .



All Articles