청결 건물, 2년 후

34469 단어 architecturego
2018년 2월에 나는 내가 출판한 가장 관련된 텍스트인 Clean Architecture using Golang을 썼다.105k여 개의 보기를 통해 이 글은 일부 Go와 PHP 활동에서 프레젠테이션 원고를 만들었고 몇 사람과 소프트웨어 구조를 토론할 수 있도록 허락했다.
이 구조를 이용하여 Codenation의 제품을 개발하여 우리는 경험을 얻었고 문제를 해결했다.우리는 이러한 경험을 보도하기 위해 몇 가지 댓글을 썼다.
  • Golang: using build tags to store configurations
  • Continuous integration in projects using monorepo
  • Monitoring a Golang application with Supervisor
  • Data Migration with Golang and MongoDB
  • Using Golang as a scripting language
  • Creating test mocks using GoMock
  • Using Prometheus to collect metrics from Golang applications
  • Profiling Golang applications using pprof
  • Testing APIs in Golang using apitest
  • 이 모든 것을 겪은 후에 나는 이렇게 말할 수 있다.

    Choosing Clean Architecture was the best technical decision we made!


    이 글에서 나는 repository을 공유하고 싶은데, 그 중에서 새로운 예시가 실현되었다.이것은 코드와 디렉터리 조직의 개선된 업데이트이며, 이 구조를 실현하고자 하는 사람들에게는 더욱 완전한 예이다.
    다음 주제에서, 나는 모든 디렉터리의 의미를 설명할 것이다.

    솔리드 레이어


    우리 구조의 맨 안쪽부터 시작합시다.
    기준 Uncle Bob's post:

    Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.


    구조는 다음과 같습니다.

    이 가방에서 우리는 실체와 각자의 단원 테스트를 정의했다.예를 들어, 솔리드 user:
    package entity
    
    import (
        "time"
    
        "golang.org/x/crypto/bcrypt"
    )
    
    //User data
    type User struct {
        ID        ID
        Email     string
        Password  string
        FirstName string
        LastName  string
        CreatedAt time.Time
        UpdatedAt time.Time
        Books     []ID
    }
    
    //NewUser create a new user
    func NewUser(email, password, firstName, lastName string) (*User, error) {
        u := &User{
            ID:        NewID(),
            Email:     email,
            FirstName: firstName,
            LastName:  lastName,
            CreatedAt: time.Now(),
        }
        pwd, err := generatePassword(password)
        if err != nil {
            return nil, err
        }
        u.Password = pwd
        err = u.Validate()
        if err != nil {
            return nil, ErrInvalidEntity
        }
        return u, nil
    }
    
    //AddBook add a book
    func (u *User) AddBook(id ID) error {
        _, err := u.GetBook(id)
        if err == nil {
            return ErrBookAlreadyBorrowed
        }
        u.Books = append(u.Books, id)
        return nil
    }
    
    //RemoveBook remove a book
    func (u *User) RemoveBook(id ID) error {
        for i, j := range u.Books {
            if j == id {
                u.Books = append(u.Books[:i], u.Books[i+1:]...)
                return nil
            }
        }
        return ErrNotFound
    }
    
    //GetBook get a book
    func (u *User) GetBook(id ID) (ID, error) {
        for _, v := range u.Books {
            if v == id {
                return id, nil
            }
        }
        return id, ErrNotFound
    }
    
    //Validate validate data
    func (u *User) Validate() error {
        if u.Email == "" || u.FirstName == "" || u.LastName == "" || u.Password == "" {
            return ErrInvalidEntity
        }
    
        return nil
    }
    
    //ValidatePassword validate user password
    func (u *User) ValidatePassword(p string) error {
        err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p))
        if err != nil {
            return err
        }
        return nil
    }
    
    func generatePassword(raw string) (string, error) {
        hash, err := bcrypt.GenerateFromPassword([]byte(raw), 10)
        if err != nil {
            return "", err
        }
        return string(hash), nil
    }
    

    용례층


    밥 아저씨의 말에 의하면

    The software in this layer contains application specific business rules. It encapsulates and implements all of the use cases of the system


    구조는 다음과 같습니다.
    usecase의 가방에서 우리는 제품의 기타 업무 규칙을 실현했다.
    예를 들어, 파일 usecase\loan\service.go:
    package loan
    
    import (
        "github.com/eminetto/clean-architecture-go-v2/entity"
        "github.com/eminetto/clean-architecture-go-v2/usecase/book"
        "github.com/eminetto/clean-architecture-go-v2/usecase/user"
    )
    
    //Service loan usecase
    type Service struct {
        userService user.UseCase
        bookService book.UseCase
    }
    
    //NewService create new use case
    func NewService(u user.UseCase, b book.UseCase) *Service {
        return &Service{
            userService: u,
            bookService: b,
        }
    }
    
    //Borrow borrow a book to an user
    func (s *Service) Borrow(u *entity.User, b *entity.Book) error {
        u, err := s.userService.GetUser(u.ID)
        if err != nil {
            return err
        }
        b, err = s.bookService.GetBook(b.ID)
        if err != nil {
            return err
        }
        if b.Quantity <= 0 {
            return entity.ErrNotEnoughBooks
        }
    
        err = u.AddBook(b.ID)
        if err != nil {
            return err
        }
        err = s.userService.UpdateUser(u)
        if err != nil {
            return err
        }
        b.Quantity--
        err = s.bookService.UpdateBook(b)
        if err != nil {
            return err
        }
        return nil
    }
    
    //Return return a book
    func (s *Service) Return(b *entity.Book) error {
        b, err := s.bookService.GetBook(b.ID)
        if err != nil {
            return err
        }
    
        all, err := s.userService.ListUsers()
        if err != nil {
            return err
        }
        borrowed := false
        var borrowedBy entity.ID
        for _, u := range all {
            _, err := u.GetBook(b.ID)
            if err != nil {
                continue
            }
            borrowed = true
            borrowedBy = u.ID
            break
        }
        if !borrowed {
            return entity.ErrBookNotBorrowed
        }
        u, err := s.userService.GetUser(borrowedBy)
        if err != nil {
            return err
        }
        err = u.RemoveBook(b.ID)
        if err != nil {
            return err
        }
        err = s.userService.UpdateUser(u)
        if err != nil {
            return err
        }
        b.Quantity++
        err = s.bookService.UpdateBook(b)
        if err != nil {
            return err
        }
    
        return nil
    }
    
    
    우리는 mocks에서 생성된 Gomock도 발견했다. 예를 들어 post에서 말한 바와 같다.구조의 다른 층은 테스트 기간에 이 시뮬레이션을 사용할 것이다.

    프레임 및 구동층


    밥 아저씨의 말에 의하면

    The outermost layer is generally composed of frameworks and tools such as the Database, the Web Framework, etc.This layer is where all the details go.



    예를 들어, 파일 infrastructure/repository/user_mysql.go에서 MySQL에서 인터페이스 Repository을 구현했습니다.만약 우리가 다른 데이터베이스로 전환해야 한다면, 우리는 이곳에서 새로운 실현을 만들 것이다.

    인터페이스 어댑터 레이어


    이 층의 코드는 데이터를 실체와 외부 에이전트 (예를 들어 데이터베이스, 웹 등) 용례로 조정하고 변환합니다.
    이 응용 프로그램에는 UseCases에 접근할 수 있는 두 가지 방법이 있다.첫 번째는 API을 통해, 두 번째는 명령행 응용 프로그램(CLI)을 사용하는 것이다.CLI의 구조는 매우 간단하다.

    도메인 패키지를 사용하여 도서 검색을 수행합니다.
    dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true", config.DB_USER, config.DB_PASSWORD, config.DB_HOST, config.DB_DATABASE)
    db, err := sql.Open("mysql", dataSourceName)
    if err != nil {
        log.Fatal(err.Error())
    }
    defer db.Close()
    repo := repository.NewBookMySQL(db)
    service := book.NewService(repo)
    all, err := service.SearchBooks(query)
    if err != nil {
        log.Fatal(err)
    }
    for _, j := range all {
        fmt.Printf("%s %s \n", j.Title, j.Author)
    }
    
    위의 예시에서 config 패키지의 사용을 볼 수 있습니다.아래에서 구조를 볼 수 있습니다. 자세한 내용은 post을 참조하십시오.
    API은 구조가 더욱 복잡하고 세 개의 가방이 있다. handler, presentermiddleware이다.handler 패키지 처리 HTTP requestsresponses, 그리고 usecases의 기존 업무 규칙을 사용합니다.
    presentersresponse에서 생성된 handlers 데이터를 포맷합니다.

    솔리드 User:
    type User struct {
        ID        ID
        Email     string
        Password  string
        FirstName string
        LastName  string
        CreatedAt time.Time
        UpdatedAt time.Time
        Books     []ID
    }
    
    다음과 같이 변환할 수 있습니다.
    type User struct {
        ID        entity.ID `json:"id"`
        Email     string    `json:"email"`
        FirstName string    `json:"first_name"`
        LastName  string    `json:"last_name"`
    }
    
    이것은 우리로 하여금 API을 통해 실체를 어떻게 교부하는지 제어할 수 있게 한다.API의 마지막 가방에서 middlewares을 찾았습니다. 몇 개의 endpoints이 사용했습니다.

    지원 패키지


    암호화, 로그 기록, 파일 처리 등 흔히 볼 수 있는 기능을 제공하는 패키지입니다. 이 기능들은 우리의 응용 프로그램 영역에 속하지 않고 모든 층에서 사용할 수 있습니다.심지어 다른 응용 프로그램도 이 패키지를 가져오고 사용할 수 있다.

    README.md은 컴파일 설명과 사용 예시와 같은 더 많은 세부 사항을 포함한다.
    이 글의 목표는 이 구조에 대한 건의를 강화하고 코드에 대한 피드백을 받는 것이다.
    가장 좋아하는 프로그래밍 언어에서 이 구조를 사용하는 방법을 배우고 싶다면, 이 저장소를 학습 예로 사용할 수 있습니다.이렇게 하면 우리는 서로 다른 언어로 서로 다른 기능을 실현하여 비교하기 편리하게 할 수 있다.
    특히 제 친구에게 감사드립니다. 그는 텍스트와 코드에 대해 좋은 피드백을 해 주었습니다.

    좋은 웹페이지 즐겨찾기