KubernetesからGo Interface Encapsulationを学ぶ
Olivia Novak
Dev Intern · Leapcell

インターフェースを使用した入力パラメータの詳細の隠蔽
メソッドの入力パラメータが構造体の場合、内部呼び出しで入力の詳細が過剰に明らかになります。このような場合、入力を暗黙的にインターフェースに変換して、内部実装が必要なメソッドのみを参照するようにすることができます。
type Kubelet struct{} func (kl *Kubelet) HandlePodAdditions(pods []*Pod) { for _, pod := range pods { fmt.Printf("create pods : %s\n", pod.Status) } } func (kl *Kubelet) Run(updates <-chan Pod) { fmt.Println(" run kubelet") go kl.syncLoop(updates, kl) } func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) { for { select { case pod := <-updates: handler.HandlePodAdditions([]*Pod{&pod}) } } } type SyncHandler interface { HandlePodAdditions(pods []*Pod) }
ここで、Kubelet自体がいくつかのメソッドを持っていることがわかります。
syncLoop
: 状態を同期するためのループRun
: リスニングループを開始するために使用HandlePodAdditions
: Podの追加を処理するためのロジック
syncLoop
がkubeletの他のメソッドを知る必要がないため、SyncHandler
インターフェースを定義し、kubeletにこのインターフェースを実装させ、kubeletをSyncHandler
としてsyncLoop
に引数として渡します。これにより、kubeletはSyncHandler
として型キャストされます。
この変換後、kubeletの他のメソッドは入力パラメータに表示されなくなり、コーディング中にsyncLoop
内のロジックに集中できます。
ただし、このアプローチはいくつかの問題も引き起こす可能性があります。最初の抽象化は最初の要件セットには十分かもしれませんが、要件が拡大および反復するにつれて、インターフェースでラップされていないkubeletの他のメソッドを使用する必要がある場合、kubeletを明示的に渡すか、インターフェースに追加する必要があります。どちらもコーディングの労力を増やし、元のカプセル化を壊します。
レイヤー化されたカプセル化と隠蔽は、設計における私たちの究極の目標です。コードの各部分が、気にする必要のあることだけに集中できるようにすることです。
より簡単なモックテストのためのインターフェースカプセル化
インターフェースによる抽象化を通じて、テスト中に気にしない部分のモック構造体を直接インスタンス化できます。
type OrderAPI interface { GetOrderId() string } type realOrderImpl struct{} func (r *realOrderImpl) GetOrderId() string { return "" } type mockOrderImpl struct{} func (m *mockOrderImpl) GetOrderId() string { return "mock" }
ここで、テスト中にGetOrderId
が正しく動作するかどうかを気にしない場合は、OrderAPI
をmockOrderImpl
で直接初期化できます。モックのロジックは必要に応じて複雑にすることができます。
func TestGetOrderId(t *testing.T) { orderAPI := &mockOrderImpl{} // 注文IDを取得する必要があるが、テストの焦点ではない場合は、モック構造体で初期化するだけです fmt.Println(orderAPI.GetOrderId()) }
gomonkey
をテストインジェクションに使用することもできます。したがって、既存のコードがインターフェースを通じてカプセル化されていなかった場合でも、モックを実現できます。この方法はさらに強力です。
patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order { return Order{ OrderId: orderId, OrderState: delivering, } }) return func() { patches.Reset() }
gomonkey
を使用すると、より柔軟なモックが可能になります。関数の戻り値を直接設定できますが、インターフェースの抽象化は構造体からインスタンス化されたコンテンツのみを処理できます。
複数の基盤となる実装のためのインターフェースカプセル化
iptables
やipvs
のような実装は、すべてのネットワーク設定がServiceとEndpointの両方を処理する必要があるため、インターフェースの抽象化を通じて実現されます。したがって、ServiceHandler
とEndpointSliceHandler
を抽象化しました。
// ServiceHandler は、サービスオブジェクトの変更に関する通知を受信するために使用される抽象インターフェースです。 type ServiceHandler interface { // OnServiceAdd は、新しいサービスオブジェクトが作成されたことが検出されたときに呼び出されます。 OnServiceAdd(service *v1.Service) // OnServiceUpdate は、既存のサービスオブジェクトが変更されたことが検出されたときに呼び出されます。 OnServiceUpdate(oldService, service *v1.Service) // OnServiceDelete は、既存のサービスオブジェクトが削除されたことが検出されたときに呼び出されます。 OnServiceDelete(service *v1.Service) // OnServiceSynced は、すべての初期イベントハンドラが呼び出され、状態がローカルキャッシュに完全に伝播されたときに一度呼び出されます。 OnServiceSynced() } // EndpointSliceHandler は、エンドポイントスライスオブジェクトの変更に関する通知を受信するために使用される抽象インターフェースです。 type EndpointSliceHandler interface { // OnEndpointSliceAdd は、新しいエンドポイントスライスオブジェクトが作成されたことが検出されたときに呼び出されます。 OnEndpointSliceAdd(endpointSlice *discoveryv1.EndpointSlice) // OnEndpointSliceUpdate は、既存のエンドポイントスライスオブジェクトが変更されたことが検出されたときに呼び出されます。 OnEndpointSliceUpdate(oldEndpointSlice, newEndpointSlice *discoveryv1.EndpointSlice) // OnEndpointSliceDelete は、既存のエンドポイントスライスオブジェクトが削除されたことが検出されたときに呼び出されます。 OnEndpointSliceDelete(endpointSlice *discoveryv1.EndpointSlice) // OnEndpointSlicesSynced は、すべての初期イベントハンドラが呼び出され、状態がローカルキャッシュに完全に伝播されたときに一度呼び出されます。 OnEndpointSlicesSynced() }
その後、Provider
を通じて注入できます。
type Provider interface { config.EndpointSliceHandler config.ServiceHandler }
これは、コンポーネントに取り組むときに私が最もよく使用するコーディング手法でもあります。同様の操作を抽象化することで、上位レイヤーのコードは、基盤となる実装を置き換えた後でも変更する必要がありません。
例外処理のカプセル化
goroutineの起動後に例外をキャプチャしない場合、例外が発生するとgoroutineは直接パニックになります。しかし、グローバルなrecoverロジックを毎回記述するのは非常にエレガントではないため、カプセル化されたHandleCrash
メソッドを使用できます。
package runtime var ( ReallyCrash = true ) // デフォルトのグローバルパニックハンドラ var PanicHandlers = []func(interface{}){logPanic} // 外部から追加のカスタムパニックハンドラを渡すことができます func HandleCrash(additionalHandlers ...func(interface{})) { if r := recover(); r != nil { for _, fn := range PanicHandlers { fn(r) } for _, fn := range additionalHandlers { fn(r) } if ReallyCrash { panic(r) } } }
これは、内部例外処理と追加ハンドラの外部注入の両方をサポートします。クラッシュさせたくない場合は、必要に応じてロジックを変更できます。
package runtime func Go(fn func()) { go func() { defer HandleCrash() fn() }() }
goroutineを開始するときは、Go
メソッドを使用できます。これにより、パニック処理の追加を忘れることも防ぎます。
WaitGroupのカプセル化
import "sync" type Group struct { wg sync.WaitGroup } func (g *Group) Wait() { g.wg.Wait() } func (g *Group) Start(f func()) { g.wg.Add(1) go func() { defer g.wg.Done() f() }() }
ここで最も重要な部分はStart
メソッドです。これは、Add
とDone
を内部的にカプセル化します。わずか数行のコードですが、waitgroupを使用するたびに、カウンタのインクリメントまたは完了を忘れないようにします。
セマフォによってトリガーされるロジックのカプセル化
type BoundedFrequencyRunner struct { sync.Mutex // アクティブにトリガーされます run chan struct{} // タイマー制限 timer *time.Timer // 実行する実際のロジック fn func() } func NewBoundedFrequencyRunner(fn func()) *BoundedFrequencyRunner { return &BoundedFrequencyRunner{ run: make(chan struct{}, 1), fn: fn, timer: time.NewTimer(0), } } // Run は実行をトリガーします。ここに書き込むことができるシグナルは1つだけで、追加のシグナルはブロックせずに破棄されます。必要に応じてキューのサイズを増やすことができます。 func (b *BoundedFrequencyRunner) Run() { select { case b.run <- struct{}{}: fmt.Println("Signal written successfully") default: fmt.Println("Signal already triggered once, discarding") } } func (b *BoundedFrequencyRunner) Loop() { b.timer.Reset(time.Second * 1) for { select { case <-b.run: fmt.Println("Run signal triggered") b.tryRun() case <-b.timer.C: fmt.Println("Timer triggered execution") b.tryRun() } } } func (b *BoundedFrequencyRunner) tryRun() { b.Lock() defer b.Unlock() // ここにレート制限などのロジックを追加できます b.timer.Reset(time.Second * 1) b.fn() }
Leapcellは、Goプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedisのための次世代サーバーレスプラットフォームです。
マルチ言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも請求もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察のためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高いパフォーマンス
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
X でフォローしてください: @LeapcellHQ