適切に構造化されたロジック:GolangOOPチュートリアル

公開: 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のオプションの2番目の部分は、ヨーロッパのVINのみの機能です。 この関数は、メーカーコードの3桁目が9である米国車の誤ったIDを返します。

これらの問題を解決するために、オブジェクト指向パターンを使用してリファクタリングします。

オブジェクト指向:関数を型にバインドする

最初のリファクタリングは、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()関数のテストでは、無効なVINのテストを省略できるようになりました。これは、無効なVINがすでにNewVINコンストラクターによって拒否されているためです。

OOPの落とし穴に行く:ポリモーフィズムは間違った方法

次に、ヨーロッパのVINとヨーロッパ以外のVINを区別します。 1つのアプローチは、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言語では、サブタイプEUVINは、 VINタイプが指定されているすべての場所で使用できると予想されます。 残念ながら、これはGolangOOPでは機能しません。

 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タイプが一般的なタイプのサブタイプである必要はないことに注意してください。

 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インターフェイスを指定するすべての場所で使用できるようになりました。

オブジェクト指向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呼び出しの結果がわかっているので、それをモックに置き換えることができるはずです。 残念ながら、上記のコードでは、 VINService自体がAPIクライアントを作成するため、簡単に置き換える方法はありません。 これを可能にするには、APIクライアントの依存関係をVINServiceに注入する必要があります。 つまり、 VINServiceコンストラクターを呼び出す前に作成する必要があります。

ここでのGolangOOPガイドラインは、コンストラクターが別のコンストラクターを呼び出さないようにすることです。 これを完全に適用すると、アプリケーションで使用されるすべてのシングルトンが最上位レベルで作成されます。 通常、これはブートストラップ関数であり、コンストラクターを適切な順序で呼び出し、プログラムの目的の機能に適した実装を選択することにより、必要なすべてのオブジェクトを作成します。

最初のステップは、 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の呼び出し中に1回実行されるため、このテストは合格です。

Goでのオブジェクト指向プログラミング:勝利の組み合わせ(正しく行われた場合)

批評家は、「とにかくOOPを実行しているのなら、なぜJavaを使用しないのですか?」と言うかもしれません。 ええと、リソースを大量に消費するVM / JITを回避しながら、Goの他のすべての気の利いた利点を得ることができるので、テストの実行中に注釈ブードゥー、例外処理、およびコーヒーブレイクを備えたフレームワークを大胆に扱います(後者は一部の人にとって問題になる可能性があります)。

上記の例では、Goでオブジェクト指向プログラミングを実行すると、単純な命令型の実装と比較して、理解しやすく、実行速度の速いコードを生成できることは明らかです。 GoはOOP言語を意図したものではありませんが、オブジェクト指向の方法でアプリケーションを構造化するために必要なツールを提供します。 パッケージ内の機能をグループ化するとともに、GolangのOOPを活用して、大規模なアプリケーションのビルディングブロックとして再利用可能なモジュールを提供できます。


GoogleCloudPartnerバッジ。

Google Cloudパートナーとして、ToptalのGoogle認定エキスパートは、最も重要なプロジェクトのオンデマンドで企業に提供されます。