잘 구조화된 논리: Golang OOP 자습서

게시 됨: 2022-03-11

Go는 객체 지향입니까? 그럴 수 있습니까? Go(또는 "Golang")는 Algol/Pascal/Modula 언어 계열에서 구조(패키지, 유형, 함수)를 차용한 포스트 OOP 프로그래밍 언어입니다. 그럼에도 불구하고 Go에서 객체 지향 패턴은 명확하고 이해할 수 있는 방식으로 프로그램을 구성하는 데 여전히 유용합니다. 이 Golang 튜토리얼은 간단한 예를 들어 유형(일명 클래스), 생성자, 하위 유형 지정, 다형성, 종속성 주입 및 모의 테스트에 함수 바인딩 개념을 적용하는 방법을 보여줍니다.

Golang OOP의 사례 연구: 차량 식별 번호(VIN)에서 제조업체 코드 읽기

모든 자동차의 고유한 차량 식별 번호에는 "실행 중인"(즉, 일련 번호) 번호 외에 제조업체, 생산 공장, 자동차 모델 및 왼쪽에서 운전하는지 여부와 같은 자동차에 대한 정보가 포함됩니다. 오른편.

제조업체 코드를 결정하는 함수는 다음과 같습니다.

 package vin func Manufacturer(vin string) string { manufacturer := vin[: 3] // if the last digit of the manufacturer ID is a 9 // the digits 12 to 14 are the second part of the ID if manufacturer[2] == '9' { manufacturer += vin[11: 14] } return manufacturer }

다음은 예제 VIN이 작동하는지 증명하는 테스트입니다.

 package vin_test import ( "vin-stages/1" "testing" ) const testVIN = "W09000051T2123456" func TestVIN_Manufacturer(t *testing.T) { manufacturer := vin.Manufacturer(testVIN) if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

따라서 이 함수는 올바른 입력이 제공될 때 올바르게 작동하지만 몇 가지 문제가 있습니다.

  • 입력 문자열이 VIN이라는 보장은 없습니다.
  • 3자보다 짧은 문자열의 경우 함수는 panic 을 일으킵니다.
  • ID의 선택적 두 번째 부분은 유럽 VIN에만 있는 기능입니다. 이 함수는 제조업체 코드의 세 번째 숫자로 9가 있는 미국 자동차에 대해 잘못된 ID를 반환합니다.

이러한 문제를 해결하기 위해 객체 지향 패턴을 사용하여 리팩토링합니다.

Go OOP: 유형에 함수 바인딩

첫 번째 리팩토링은 VIN을 고유한 유형으로 만들고 여기에 Manufacturer() 함수를 바인딩하는 것입니다. 이것은 함수의 목적을 보다 명확하게 하고 무분별한 사용을 방지합니다.

 package vin type VIN string func (v VIN) Manufacturer() string { manufacturer := v[: 3] if manufacturer[2] == '9' { manufacturer += v[11: 14] } return string(manufacturer) }

그런 다음 테스트를 조정하고 잘못된 VIN 문제를 소개합니다.

 package vin_test import( "vin-stages/2" "testing" ) const ( validVIN = vin.VIN("W0L000051T2123456") invalidVIN = vin.VIN("W0") ) func TestVIN_Manufacturer(t * testing.T) { manufacturer := validVIN.Manufacturer() if manufacturer != "W0L" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, validVIN) } invalidVIN.Manufacturer() // panic! }

마지막 줄은 Manufacturer() 함수를 사용하는 동안 panic 을 유발하는 방법을 보여주기 위해 삽입되었습니다. 테스트 외부에서 실행 중인 프로그램이 충돌합니다.

Golang의 OOP: 생성자 사용

유효하지 않은 VIN을 처리할 때 panic 을 피하기 위해 Manufacturer() 함수 자체에 유효성 검사를 추가할 수 있습니다. 단점은 Manufacturer() 함수에 대한 모든 호출에 대해 검사가 수행되고 오류 반환 값이 도입되어야 하므로 중간 변수 없이 직접 반환 값을 사용할 수 없다는 것입니다(예: 지도 키).

보다 우아한 방법은 유효성 검사를 VIN 유형에 대한 생성자에 두어 Manufacturer() 함수가 유효한 VIN에 대해서만 호출되고 검사 및 오류 처리가 필요하지 않도록 하는 것입니다.

 package vin import "fmt" type VIN string // it is debatable if this func should be named New or NewVIN // but NewVIN is better for greping and leaves room for other // NewXY funcs in the same package func NewVIN(code string)(VIN, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return VIN(code), nil } func (v VIN) Manufacturer() string { manufacturer := v[: 3] if manufacturer[2] == '9' { manufacturer += v[11: 14] } return string(manufacturer) }

물론 NewVIN 기능에 대한 테스트를 추가합니다. 유효하지 않은 VIN은 이제 생성자에 의해 거부됩니다.

 package vin_test import ( "vin-stages/3" "testing" ) const ( validVIN = "W0L000051T2123456" invalidVIN = "W0" ) func TestVIN_New(t *testing.T) { _, err := vin.NewVIN(validVIN) if err != nil { t.Errorf("creating valid VIN returned an error: %s", err.Error()) } _, err = vin.NewVIN(invalidVIN) if err == nil { t.Error("creating invalid VIN did not return an error") } } func TestVIN_Manufacturer(t *testing.T) { testVIN, _ := vin.NewVIN(validVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W0L" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

Manufacturer() 함수에 대한 테스트는 이제 NewVIN 생성자에 의해 이미 거부되었을 것이기 때문에 잘못된 VIN 테스트를 생략할 수 있습니다.

Go OOP의 함정: 다형성이 잘못된 방법

다음으로 유럽 VIN과 비유럽 VIN을 구분하고자 합니다. 한 가지 접근 방식은 VIN typestruct 로 확장하고 VIN이 유럽인지 여부를 저장하여 그에 따라 생성자를 향상시키는 것입니다.

 type VIN struct { code string european bool } func NewVIN(code string, european bool)(*VIN, error) { // ... checks ... return &VIN { code, european }, nil }

보다 우아한 솔루션은 유럽 VIN 에 대한 VIN의 하위 유형을 만드는 것입니다. 여기에서 플래그는 유형 정보에 암시적으로 저장되며 비유럽 VIN에 대한 Manufacturer() 함수는 멋지고 간결해집니다.

 package vin import "fmt" type VIN string func NewVIN(code string)(VIN, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return VIN(code), nil } func (v VIN) Manufacturer() string { return string(v[: 3]) } type EUVIN VIN func NewEUVIN(code string)(EUVIN, error) { // call super constructor v, err := NewVIN(code) // and cast to subtype return EUVIN(v), err } func (v EUVIN) Manufacturer() string { // call manufacturer on supertype manufacturer := VIN(v).Manufacturer() // add EU specific postfix if appropriate if manufacturer[2] == '9' { manufacturer += string(v[11: 14]) } return manufacturer }

Java와 같은 OOP 언어에서는 VIN 유형이 지정된 모든 위치에서 하위 유형 EUVIN 을 사용할 수 있을 것으로 예상합니다. 불행히도 이것은 Golang OOP에서 작동하지 않습니다.

 package vin_test import ( "vin-stages/4" "testing" ) const euSmallVIN = "W09000051T2123456" // this works! func TestVIN_EU_SmallManufacturer(t *testing.T) { testVIN, _ := vin.NewEUVIN(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } // this fails with an error func TestVIN_EU_SmallManufacturer_Polymorphism(t *testing.T) { var testVINs[] vin.VIN testVIN, _ := vin.NewEUVIN(euSmallVIN) // having to cast testVIN already hints something is odd testVINs = append(testVINs, vin.VIN(testVIN)) for _, vin := range testVINs { manufacturer := vin.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } }

이 동작은 Go 개발 팀이 비인터페이스 유형에 대한 동적 바인딩을 지원하지 않도록 의도적으로 선택하여 설명할 수 있습니다. 이를 통해 컴파일러는 컴파일 시간에 어떤 함수가 호출되는지 알 수 있으며 동적 메서드 디스패치의 오버헤드를 피할 수 있습니다. 이 선택은 또한 상속을 일반적인 구성 패턴으로 사용하는 것을 권장하지 않습니다. 대신 인터페이스가 가야 할 길입니다(말장난을 용서하십시오).

Golang OOP 성공: 올바른 방식의 다형성

Go 컴파일러는 선언된 함수(덕 타이핑)를 구현할 때 유형을 인터페이스의 구현으로 취급합니다. 따라서 다형성을 활용하기 위해 VIN 유형을 일반 및 유럽 VIN 유형으로 구현되는 인터페이스로 변환합니다. 유럽 ​​VIN 유형이 일반 VIN 유형의 하위 유형일 필요는 없습니다.

 package vin import "fmt" type VIN interface { Manufacturer() string } type vin string func NewVIN(code string)(vin, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return vin(code), nil } func (v vin) Manufacturer() string { return string(v[: 3]) } type vinEU vin func NewEUVIN(code string)(vinEU, error) { // call super constructor v, err := NewVIN(code) // and cast to own type return vinEU(v), err } func (v vinEU) Manufacturer() string { // call manufacturer on supertype manufacturer := vin(v).Manufacturer() // add EU specific postfix if appropriate if manufacturer[2] == '9' { manufacturer += string(v[11: 14]) } return manufacturer }

다형성 테스트는 이제 약간의 수정으로 통과합니다.

 // this works! func TestVIN_EU_SmallManufacturer_Polymorphism(t *testing.T) { var testVINs[] vin.VIN testVIN, _ := vin.NewEUVIN(euSmallVIN) // now there is no need to cast! testVINs = append(testVINs, testVIN) for _, vin := range testVINs { manufacturer := vin.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } }

사실, 두 VIN 유형 모두 VIN 인터페이스 정의를 준수하므로 이제 VIN 인터페이스를 지정하는 모든 위치에서 두 VIN 유형을 모두 사용할 수 있습니다.

객체 지향 Golang: 의존성 주입 사용 방법

마지막으로 VIN이 유럽식인지 아닌지를 결정해야 합니다. 이 정보를 제공하는 외부 API를 찾았고 이에 대한 클라이언트를 구축했다고 가정해 보겠습니다.

 package vin type VINAPIClient struct { apiURL string apiKey string // ... internals go here ... } func NewVINAPIClient(apiURL, apiKey string) *VINAPIClient { return &VINAPIClient {apiURL, apiKey} } func (client *VINAPIClient) IsEuropean(code string) bool { // calls external API and returns correct value return true }

우리는 또한 VIN을 처리하는 서비스를 구축했으며 특히 VIN을 생성할 수 있습니다.

 package vin type VINService struct { client *VINAPIClient } type VINServiceConfig struct { APIURL string APIKey string // more configuration values } func NewVINService(config *VINServiceConfig) *VINService { // use config to create the API client apiClient := NewVINAPIClient(config.APIURL, config.APIKey) return &VINService {apiClient} } func (s *VINService) CreateFromCode(code string)(VIN, error) { if s.client.IsEuropean(code) { return NewEUVIN(code) } return NewVIN(code) }

수정된 테스트에서 다음과 같이 잘 작동합니다.

 func TestVIN_EU_SmallManufacturer(t *testing.T) { service := vin.NewVINService( & vin.VINServiceConfig {}) testVIN, _ := service.CreateFromCode(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

여기서 유일한 문제는 테스트에 외부 API에 대한 라이브 연결이 필요하다는 것입니다. API가 오프라인이거나 연결할 수 없기 때문에 이는 불행한 일입니다. 또한 외부 API를 호출하는 데는 시간과 비용이 소요될 수 있습니다.

API 호출의 결과를 알 수 있으므로 이를 mock으로 대체할 수 있어야 합니다. 안타깝게도 위의 코드에서 VINService 자체가 API 클라이언트를 생성하므로 이를 쉽게 대체할 수 있는 방법이 없습니다. 이를 가능하게 하려면 API 클라이언트 종속성을 VINService 에 주입해야 합니다. 즉, VINService 생성자를 호출하기 전에 생성되어야 합니다.

여기서 Golang OOP 지침은 생성자가 다른 생성자를 호출해서는 안 된다는 것입니다. 이를 철저히 적용하면 응용 프로그램에서 사용되는 모든 싱글톤이 최상위 수준에서 생성됩니다. 일반적으로 이것은 적절한 순서로 생성자를 호출하고 프로그램의 의도된 기능에 적합한 구현을 선택하여 필요한 모든 객체를 생성하는 부트스트랩 기능입니다.

첫 번째 단계는 VINAPIClient 를 인터페이스로 만드는 것입니다.

 package vin type VINAPIClient interface { IsEuropean(code string) bool } type vinAPIClient struct { apiURL string apiKey string // .. internals go here ... } func NewVINAPIClient(apiURL, apiKey string) *VINAPIClient { return &vinAPIClient {apiURL, apiKey} } func (client *VINAPIClient) IsEuropean(code string) bool { // calls external API and returns something more useful return true }

그런 다음 새 클라이언트를 VINService 에 주입할 수 있습니다.

 package vin type VINService struct { client VINAPIClient } type VINServiceConfig struct { // more configuration values } func NewVINService(config *VINServiceConfig, apiClient VINAPIClient) *VINService { // apiClient is created elsewhere and injected here return &VINService {apiClient} } func (s *VINService) CreateFromCode(code string)(VIN, error) { if s.client.IsEuropean(code) { return NewEUVIN(code) } return NewVIN(code) }

이를 통해 이제 테스트에 API 클라이언트 모의를 사용할 수 있습니다. 테스트 중에 외부 API에 대한 호출을 피하는 것 외에도 모의는 API 사용에 대한 데이터를 수집하는 프로브 역할을 할 수도 있습니다. 아래 예에서는 IsEuropean 함수가 실제로 호출되었는지 확인합니다.

 package vin_test import ( "vin-stages/5" "testing" ) const euSmallVIN = "W09000051T2123456" type mockAPIClient struct { apiCalls int } func NewMockAPIClient() *mockAPIClient { return &mockAPIClient {} } func (client *mockAPIClient) IsEuropean(code string) bool { client.apiCalls++ return true } func TestVIN_EU_SmallManufacturer(t *testing.T) { apiClient := NewMockAPIClient() service := vin.NewVINService( & vin.VINServiceConfig {}, apiClient) testVIN, _ := service.CreateFromCode(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } if apiClient.apiCalls != 1 { t.Errorf("unexpected number of API calls: %d", apiClient.apiCalls) } }

IsEuropean 프로브가 CreateFromCode 를 호출하는 동안 한 번 실행되기 때문에 이 테스트는 통과합니다.

Go의 객체 지향 프로그래밍: 성공적인 조합(올바른 경우)

비평가는 "어쨌든 OOP를 수행하는 경우 Java를 사용하지 않는 이유는 무엇입니까?"라고 말할 수 있습니다. 리소스를 많이 사용하는 VM/JIT, 주석 부두, 예외 처리 및 테스트를 실행하는 동안 휴식 시간이 포함된 프레임워크를 피하면서 Go의 다른 모든 멋진 이점을 얻을 수 있기 때문입니다(후자는 일부에게 문제가 될 수 있음).

위의 예를 보면 Go에서 객체 지향 프로그래밍을 수행하는 것이 일반 명령적 구현과 비교하여 더 이해하기 쉽고 더 빠르게 실행되는 코드를 생성할 수 있다는 것이 분명합니다. Go는 OOP 언어가 아니지만 객체 지향 방식으로 애플리케이션을 구성하는 데 필요한 도구를 제공합니다. 패키지의 그룹화 기능과 함께 Golang의 OOP를 활용하여 재사용 가능한 모듈을 대규모 애플리케이션의 빌딩 블록으로 제공할 수 있습니다.


Google Cloud 파트너 배지.

Google Cloud 파트너로서 Toptal의 Google 인증 전문가는 회사의 가장 중요한 프로젝트에 대한 수요가 있을 때 사용할 수 있습니다.