[Go] 와이어를 통해서 꼭 필요한 충분한 디.

11822 단어 GoDI
이것은 Advent Calendar 2018 선택 24일째 글입니다.
나는 서버에 가까운 엔지니어@sakushin다.Qiita의 첫 투고입니다.
업무는 주로 Go를 이용하여 웹 API와 대량 서비스를 개발한다.
복잡성이 높은 시스템은 DI 실천을 통해 응용 프로그램의 층별 구성화를 추진한다.
최근 한 프로젝트에서 시험적으로 도입한 DI 보조 도구Wire가 익숙한 것 같아서 이번에는 이를 주제로 토론하고자 합니다.
go-cloud에 첨부된 Golang은 DI 도구인 "Wire"를 사용하면 좋습니다.API는 구현과 인터페이스 연결에 집중할 수 있고 생성된 코드도 간단하다.의존 관계의 변경(= 공장 방법의 서명 변경)도 강하다.https://t.co/lrehTcAbVo - 호박산 에스테르(@sakushin)July 30, 2018
도입과 상세한 이용 방법공식 강좌과 다른 사람의 보도의 총결이 좋아서 사랑을 끊는다. 여기서 매우 간단한 예로 감각을 섞으면서 그 효용을 설명한다.

TL;DR

  • 어리석은 DI 논리는 독자에게는 우호적이지만 작가에게는 낭비의 부담이 있다
  • Wire를 이용하면 작성자의 부담을 크게 줄일 수 있다
  • Wire의 기술 규칙과 생성 코드는 간단하고 독자에게도 필요하다
  • 구상 과제


    다음과 같은 구성 요소와 의존 관계가 있다고 가정합니다.

    하나의 구성 요소는 여러 구성 요소에 의존할 수도 있고, 여러 구성 요소에 의존할 수도 있다.
    우리의 목표는 초기화 Server 와 그 기능을 호출하는 것이다.
    또한 모든 구성 요소에 대해 자신이 의존하는 구성 요소를 매개 변수로 하는 공장 방법이 component 패키지 아래에 존재한다고 가정한다(= 구조 함수 주입이 가능한 상태).

    경솔하게 하다


    우선 직접 쓰면 코드가 다음과 같은 형태로 바뀌겠죠.
    main.go
    package main
    
    import (
        . "github.com/sakushin/wire-example/component"
    )
    
    func main() {
        // 各種コンポーネントの手動インジェクション
        // 依存ツリーの葉の部分から依存関係を意識しつつ完全なワイヤリングをする必要がある
        currentTimeProvider := NewCurrentTimeProvider()
        repository1 := NewRepository1(currentTimeProvider)
        repository2 := NewRepository2(currentTimeProvider)
        application1 := NewApplication1(currentTimeProvider, repository1)
        application2 := NewApplication2(currentTimeProvider, repository2)
        handler1 := NewHandler1(application1)
        handler2 := NewHandler2(application2)
        handler3 := NewHandler3(application1, application2)
        server := NewServer(handler1, handler2, handler3)
    
        // インジェクションが完了したコンポーネントにディスパッチ
        server.Start()
    }
    
    단도직입적으로 쓰는 것은 좋은 일이다.
    독자들은 구성 요소의 의존 관계와 초기화 방법을 간단하게 이해할 수 있다.
    하지만 저자 입장에서는 어떨까?
    구성 요소의 초기화는 다른 구성 요소에 의존하지 않고 순서대로 새로운 해결 방안을 찾아야 한다.
    함수 분할과 기록을 통해 초기화 순서를 고려하는 방법을 피할 수도 있지만 이에 따라 군더더기와 복잡도가 증가한다.
    이런 규모라면 유지보수가 쉽지만 구성 요소의 수가 증가함에 따라 기능 추가와 재구성에 필요한 비용도 무의식적으로 증가하겠지.

    무선 네트워크 사용


    의존 관계도 특성상 전제 조건으로 표시된 모든 구성 요소에 대해 자신이 의존하는 구성 요소를 변수로 하는 공장 방법이 존재한다면만약 구성 요소의 공장 방법을 열거한다면, 어떤 구성 요소의 생성 논리도 기계적으로 확정할 수 있어야 한다.
    이러한 요구를 실현하는 프레임워크로서 DI 용기가 곳곳에 존재하고 Go로 그것을 실현하는 도구 중 하나로 이번에 소개된 와이어입니다.
    Wire는 특수 구축 태그가 있는 Go 코드를 데이터 원본으로 사용하고 컴파일 시간에 주입기 코드를 생성합니다.
    주입기 생성 규칙을 작성하는 코드는 다음과 같다.
    wire.go
    // +build wireinject
    
    package main
    
    import (
        "github.com/google/wire"
        . "github.com/sakushin/wire-example/component"
    )
    
    // 自動的にインジェクションを行いたいコンポーネントの型が戻り値のメソッドを定義する
    // Wireにヒントを与えるための手続き
    func InitializeServer() Server {
        // インジェクション対象と依存コンポーネントのファクトリメソッドの参照を列挙する
        // 並び順は任意
        wire.Build(
            NewServer,
            NewHandler1,
            NewHandler2,
            NewHandler3,
            NewApplication1,
            NewApplication2,
            NewRepository1,
            NewRepository2,
            NewCurrentTimeProvider,
        )
        // コンパイルエラーを避けるため形式的にnilを返却する
        return nil
    }
    
    댓글에 주목해야 할 점을 덧붙였다.
    논리에서 보듯이 자신이 사용하는 공장 방법을 열거하면 모든 구성 요소의 생성 논리는 기계적으로 확정할 수 있다. wire.Build 방법의 매개 변수로 각 구성 요소의 공장 방법의 참조를 나열했다.(함수 참조이므로 실제 매개변수가 필요하지 않음)
    또 어리석게 쓸 때 알아차린 초기화 순서는 여기서도 모른다.
    언뜻 보기에는 매우 간단한 형식이지만 이를 바탕으로 자동으로 생성되는 코드는 어떤 형식일까?
    wire_gen.go
    // Code generated by Wire. DO NOT EDIT.
    
    //go:generate wire
    //+build !wireinject
    
    package main
    
    import (
        "github.com/sakushin/wire-example/component"
    )
    
    // Injectors from wire.go:
    
    func InitializeServer() component.Server {
        currentTimeProvider := component.NewCurrentTimeProvider()
        repository1 := component.NewRepository1(currentTimeProvider)
        application1 := component.NewApplication1(currentTimeProvider, repository1)
        handler1 := component.NewHandler1(application1)
        repository2 := component.NewRepository2(currentTimeProvider)
        application2 := component.NewApplication2(currentTimeProvider, repository2)
        handler2 := component.NewHandler2(application2)
        handler3 := component.NewHandler3(application1, application2)
        server := component.NewServer(handler1, handler2, handler3)
        return server
    }
    
    생성 결과도 솔직한 형식으로 바뀌었다.
    어리석게 수동으로 기술한 예와 비교하면 거의 차이가 없다.
    통상적으로 이런 틀의 마술적인 구조는 자주 독자를 난처하게 한다.
    그러나 와이어는 사전에 코드를 생성하는 방법으로 논리적으로 명백한 것을 제외하고 생성된 코드도 읽을 수 있기 때문에 팀에 배치할 때도 쉽게 협의할 수 있다.
    개인적으로는 이런 단순성이 가져온 고도의 보수와 팀 배치의 난이도가 낮은 것이 와이어에서 가장 언급할 만한 부분이라고 생각한다.
    마지막으로 자신이 준비한 스케줄러의 코드는 상술한 내용에 따라 다음과 같다.
    main.go
    package main
    
    func main() {
        // 自動生成されたインジェクタからインジェクションを実行
        server := InitializeServer()
    
        // インジェクションが完了したコンポーネントにディスパッチ
        server.Start()
    }
    
    그것의 형식은 매우 간단해서 자동으로 생성된 주입 함수만 호출하면 된다.

    보조 어셈블리 추가


    예를 들어 앞으로 여러 개의 구성 요소에 새로운 의존 구성 요소Hoge를 추가할 경우 각 구성 요소의 공장 방법의 정의 변경을 제외하고wire의 구성 요소 열거 블록에 1행Hoge을 추가하는 공장 방법코드 생성을 다시 실행하기만 하면 됩니다.
    코드가 생성될 때 필요한 구성 요소의 열거가 부족하더라도 와이어는 간단명료하게 우리에게 일깨워 주기 때문에 큰 수정과 재구성을 할 때도 위험과 비용이 되기 어렵다.

    끝날 때


    이번에는 와이어의 도입 원가의 낮음과 소박함에 주목했지만, 얻을 수 있는 결과의 소박 잠금 정도가 낮아진 것도 주목해야 할 점이다.
    Go는 많은 다른 언어에 비해 추상화의 표현력은 높지 않지만 필요에 따라 코드 자체를 생성하여 추상도를 높이는 방법을 자주 볼 수 있다.
    생성된 코드는 지루해지지만 자주 다시 생성할 수 있기 때문에 지루함에 대해 비관할 필요가 없고 Go를 넘지 않는 표현력도 있기 때문에 많은 사람들에게 이해하기 쉽다는 장점이 있다.
    필자는 Go를 처음 접한 지 1년도 되지 않았지만'고에 들어간 후 Go를 따라간다'는 정신을 유지했다. 특히 팀 개발에서 문제의 본질에 직면하기 쉬워 익숙한 도구라고 느꼈다.

    좋은 웹페이지 즐겨찾기