인터페이스 추상 응용 프로그램 IO 사용

소프트웨어 개발과 응용 프로그램 디자인에서 데이터베이스, API와 사용자 입력 등 입력과 출력(IO) 자원에 대한 의존은 불가피하다.만약에 코드에 이러한 IO가 있는 접촉점을 포함시키면 코드의 장기적인 회복과 확장성, 테스트 능력에 중대한 영향을 미칠 것이다.인터페이스는 IO 상호작용에 필요한 세부 사항을 숨기는 방법을 제공합니다. 그러면 대부분의 코드를 작성할 수 있고 데이터가 어디에서 나오는지 걱정할 필요가 없습니다.
함수와 방법을 어떻게 재구성하는지 설명하기 위해서, 응용 프로그램의 IO를 완전히 이해하는 것부터 응용 프로그램의 IO를 전혀 모르는 것까지, 우리는 작은 응용 프로그램을 만들어서, 행 항목 목록에서 판매 주문서의 총 가격을 계산할 것이다.주문 정보는 응용 프로그램이 로컬로 저장한 JSON 파일에서 검색합니다.모든 줄의 항목은 설명과 가격을 가진 구조로 표시된다.
type LineItem struct {
    Description string  `json:"description"`
    Price       float64 `json:"price"`
}
첫 번째 시도로서, 우리는 함수를 작성하여 주문서의 세부 사항이 어디에 저장되어 있는지, 그리고 그것을 어떻게 검색하는지 이해할 수 있다.이 함수는 파일을 읽고 내용을 반서열화한 다음에 줄마다 항목을 교체해서 총 원가를 생성합니다.
func CalculateOrderTotal() float64 {
    filePath := "data/order_items.json"
    file, err := ioutil.ReadFile(filePath)
    if err != nil {
        log.Fatalf("unable to read file %s", filePath)
    }

    var lineItems []LineItem
    err = json.Unmarshal(file, &lineItems)
    if err != nil {
        log.Fatalf("unable to parse json for file %s", filePath)
    }

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}
이런 방법에는 많은 문제가 있다.첫 번째 위험 신호는 이 기능이 한 가지 일에만 집중하는 것이 아니라는 것이다.그것은 말 그대로 주문 총수를 계산하는 것이 아니다.파일을 읽고 JSON을 역서열화하고 있습니다.
이 함수는 주문 정보를 특정한 출처에서 검색하는 데도 한정된다.이것은 파일 경로를 매개 변수로 전달함으로써 해결할 수 있지만, 만약 수요에 변화가 발생하고, 반드시 다른 형식의 지속적인 저장소 (예를 들어 관계 데이터베이스나 클라우드의blob저장소) 에서 주문 정보를 검색해야 한다면 어떤 상황이 발생합니까?첫 번째 시도CalculateOrderTotal는 order total 계산 논리를 JSON 파일 이외의 모든 내용에 다시 사용하는 것을 허용하지 않습니다.
이 함수는 또 하나의 문제가 있다.이것은 테스트하기 매우 어렵다.
func TestCalculateOrderTotal(t *testing.T) {
    var expected float64 = 3476
    actual := CalculateOrderTotal()

    if actual != expected {
        t.Fatalf("expected %.2f, actual %.2f", expected, actual)
    }
}
이 코드를 실행하면 실제적으로 ioutil.ReadFile '파일을 찾을 수 없음' 오류를 되돌려줍니다. 하드코딩된 파일 경로는 상대적이기 때문에 상대적인 파일 경로를 분석할 때 테스트 디렉터리에서 사용하는 루트 경로는 응용 프로그램main 함수에서 사용하는 루트 경로와 다르기 때문입니다.마찬가지로 파일 경로를 매개 변수로 전달함으로써 해결할 수 있지만 테스트는 파일 시스템에 의존할 뿐만 아니라 CalculateOrderTotal 너무 많은 일을 책임지고 매우 구체적인 데이터 검색에 의존할 것이다.
파일 읽기와 JSON 반서열화 코드를 추출하여 파일의 반서열화 JSON 구조 방법에서 정확한 방향으로 한 걸음 나아갈 수 있습니다.
type JsonOrderProvider struct {
    FilePath string
}

func (r JsonOrderProvider) GetLineItems() []LineItem {
    file, err := ioutil.ReadFile(r.FilePath)
    if err != nil {
        log.Fatalf("unable to read file %s", r.FilePath)
    }

    var lineItems []LineItem
    err = json.Unmarshal(file, &lineItems)
    if err != nil {
        log.Fatalf("unable to parse json for file %s", r.FilePath)
    }

    return lineItems
}
현재, 파일 경로는 더 이상 하드코딩이 아니기 때문에 읽을 파일을 바꾸기 쉽다.CalculateOrderTotal 이 새로운 구조를 받아들이는 매개 변수로 다시 쓸 수 있으며, 현재는 로컬 파일 시스템을 처리하는 것이 아니라 주문 총수를 계산하는 데 주로 사용된다.
func CalculateOrderTotal(provider JsonOrderProvider) float64 {
    lineItems := provider.GetLineItems()

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}
테스트도 훨씬 간단합니다. 우리는 현재 상대적인 파일 경로를 조정할 수 있습니다. 이렇게 하면 테스트 항목은 JSON 파일을 포지셔닝할 수 있습니다. 우리는 심지어 몇 개의 JSON 파일을 사용하여 각종 입력을 사용하여 우리의 함수를 테스트할 수 있습니다.
func TestCalculateOrderTotal(t *testing.T) {
    var expected float64 = 3476
    provider := JsonOrderProvider{FilePath: "../data/order_items.json"}
    actual := CalculateOrderTotal(provider)

    if actual != expected {
        t.Fatalf("expected %.2f, actual %.2f", expected, actual)
    }
}
그러나 이 해결 방안은 여전히 이상적이지 않다. 왜냐하면 우리의 테스트와 CalculateOrderTotal 함수는 모두 로컬 파일 시스템을 통해 지속되는 주문 정보에 의존하기 때문이다.만약에 지속성에 대한 수요가 바뀌면 SQL 데이터베이스나 클라우드 데이터베이스에 저장된 정보에 따라 주문 총수를 계산해야 한다. 그러면 우리는 새로운 공급자 구조를 작성해야 할 뿐만 아니라 다른 CalculateOrderTotal 함수도 작성해야 한다. 이 함수는 서로 다른 매개 변수 유형을 가지고 있다.
이 두 문제는 모두 도입 인터페이스를 통해 해결할 수 있다.
type OrderProvider interface {
    GetLineItems() []LineItem
}
OrderProvider 인터페이스를 만들면 우리가 원하는 행위 유형에 대한 계약을 설명할 수 있으며 세부 사항을 알 필요가 없습니다.JsonOrderProvider는 이 인터페이스의 요구를 충족시켰기 때문에 (Go에서 인터페이스는 은밀하게 이루어진다) 메모리에 있는 줄 항목 목록에서 주문 정보를 제공하는 새로운 공급자를 만들면 테스트 능력을 향상시킬 수 있다.
type InMemoryOrderProvider struct {
    LineItems []LineItem
}

func (r InMemoryOrderProvider) GetLineItems() []LineItem {
    return r.LineItems
}
우리는 구체적인 유형CalculateOrderTotal이 아닌 OrderProvider의 매개 변수를 JsonOrderProvider 인터페이스 유형으로 바꾸어야 한다.
func CalculateOrderTotal(provider OrderProvider) float64 {
    lineItems := provider.GetLineItems()

    var orderTotal float64
    for _, lineItem := range lineItems {
        orderTotal += lineItem.Price
    }

    return orderTotal
}
우리의 이전 매개 변수 유형JsonOrderProviderGetLineItems 인터페이스의 OrderProvider 방법을 만족시켰기 때문에 CalculateOrderTotal의 주체는 변경할 필요가 없고 함수의 매개 변수 유형만 변경할 수 있다.다른 건 다 똑같아.
이제 구체적인 유형에서 인터페이스 유형으로의 전환이 우리의 테스트 능력을 향상시켰는지 살펴보자.
var testCases = []struct {
    provider InMemoryOrderProvider
    expected float64
}{
    {
        provider: InMemoryOrderProvider{
            LineItems: []LineItem{
                {Description: "A", Price: 85},
                {Description: "B", Price: 15},
            },
        },
        expected: 100,
    },
    {
        provider: InMemoryOrderProvider{
            LineItems: []LineItem{
                {Description: "A", Price: 35.25},
                {Description: "B", Price: 95.5},
            },
        },
        expected: 130.75,
    },
}

func TestCalculateOrderTotal(t *testing.T) {
    for _, test := range testCases {
        if actual := CalculateOrderTotal(test.provider); actual != test.expected {
            t.Fatalf("expected %.2f, actual %.2f", test.expected, actual)
        }
    }
}
이 테스트에는 공급자와 예상된 출력 필드를 포함하는 익명 구조 형식의 추가 코드가 있습니다.이 목록은 테스트 방법TestCalculatedOrderTotal에 사용되는 테스트 용례이다.이것은 흔히 볼 수 있는 단원 테스트 관례로 모든 테스트 용례에 대해 단독 방법이 있는 더욱 흔히 볼 수 있는 관례의 대체이다.
테스트 관례를 제외하고, 우리는 현재 같은 파일에서 몇 가지 테스트 용례를 정의할 수 있으며, 그 중에서 로컬 파일 시스템에 의존하지 않고 우리의 테스트 방법을 정의할 수 있다.함수CalculateOrderTotal는 검색 데이터의 실현 세부 사항도 포함하지 않고 데이터 출처에 대한 힌트도 포함하지 않습니다.OrderProvider 인터페이스는 호출 방법GetLineItems을 호출하면 함수CalculateOrderTotalLineItem 구조의 목록을 받을 수 있도록 확보한다. 이것은 이 함수가 예상 작업을 수행하는 데 필요한 유일한 보증이다.main 함수에서 우리는 CalculateOrderTotal가 사용하든 JsonOrderProviderInMemoryOrderProvider든 똑같다는 것을 증명할 수 있다.
func main() {
    jsonProvider := JsonOrderProvider{FilePath: "data/order_items.json"}

    orderTotal := CalculateOrderTotal(jsonProvider)
    fmt.Printf("Your total comes to %.2f", orderTotal)

    inMemoryProvider := InMemoryOrderProvider{
        LineItems: []LineItem{
            {Description: "Leather Recliner", Price: 2499},
            {Description: "End Table", Price: 249},
        },
    }

    orderTotal = CalculateOrderTotal(inMemoryProvider)
    fmt.Printf("Your total comes to %.2f", orderTotal)
}
어느 순간, 당신은 "왜 줄 항목 목록을 매개 변수로 전달하지 않고, 인터페이스를 만들려고 애쓰지 않습니까?"라고 생각할 수 있습니다.CalculateOrderTotal와 같은 함수는 실제 응용 프로그램에서 그것을 작성하는 방식일 가능성이 높다.그러나 이것은 데이터를 검색할 필요가 없다는 것을 의미하지는 않는다.CalculateOrderTotal 인터페이스가 아닌 줄 항목 목록을 재구성해서 데이터 검색의 책임을 호출자에게 미루십시오.
호출 함수는 파일 시스템을 이해하고 OrderProvider 같은 함수를 사용하는 데 의미가 있을 수 있지만, 대부분의 응용 프로그램에서 중간 코드층이 있고, 최종적으로 IO 자원에서 온 데이터를 검색해야 하며, 어느 자원에서 온 세부 사항을 응용 프로그램의 가장자리로 미루어야 한다.핵심 업무 논리에 이런 세부 사항이 없도록 유지하다.
인터페이스를 사용하여 IO 작업의 세부 사항을 추상화하는 방법은 이러한 IO 구현 세부 사항을 더 먼 곳으로 미루어 가장자리에 가깝게 하는 동시에 중간 함수에 더 가깝게 하면 검색 데이터의 내용과 시간에 영향을 줄 수 있다.이러한 유연성을 통해 변경 사항에 적합한 솔루션을 설계하고 입출력 의존 항목에 독립적으로 테스트할 수 있습니다.
더 많은 것을 알고 싶으세요?나는 고급 함수를 사용하여 응용 프로그램 IO의 실현에 대한 세부 사항을 추상화하는 글을 쓴 적이 있다.
두 편의 블로그의 모든 예시 코드를 찾을 수 있다here.

좋은 웹페이지 즐겨찾기