現代囲Goの理論

tl;博士の魔法は悪いです。 グローバル状態は魔法です→パッケージ内のグローバル変数は不良です。 init()関数は必要ありません。







Goの最も重要で最高の特性は、本質的には魔法に反していることです。 いくつかの例外は別として、Goコードを読むだけで、定義、依存関係、または実行時の動作に曖昧さがなくなります。 これにより、Goは比較的読みやすくなり、保守が容易になります。これは、産業用プログラミングで最も重要な機能です







しかし、まだ魔法が漏れる可能性のある場所がいくつかあります。 残念ながら一般的な方法の1つは、グローバルステートを使用することです。 グローバルパッケージスペースで定義されたオブジェクトは、外部オブザーバーから隠された状態または動作を保存できます。 これらのグローバルオブジェクトに依存するコードには、プログラムのメンタルモデルを理解して構築する読者の能力を妨げる不快な副作用が生じる可能性があります。







関数(メソッドを含む)は、本質的にGoが抽象化を構築するための唯一のメカニズムです。 次の関数定義を見てみましょう。







func NewObject(n int) (*Object, error)
      
      





慣例により、NewXXXの形式の関数は型コンストラクターであると想定しています。 この期待値は、関数がオブジェクトへのポインターとエラーを返すことが確認されたときに確認されます。 このことから、コンストラクターが機能しない可能性があり、この場合、理由を説明するエラーが返されます。 また、関数が入力として単一の整数パラメータを受け入れることもわかります。これは、返されるオブジェクトの何らかの側面またはプロパティを制御すると考えられます。 おそらく、違反するとエラーにつながる有効なn値がいくつかあります。 ただし、関数は他のパラメーターを受け入れないため、(おそらく)メモリ割り当てを除いて、関数には副作用はないと考えられます。







関数の署名を読むだけで、これらすべての結論を引き出し、関数のメンタルモデルを構築できます。 このプロセスは、メイン関数の最初の行から何度も繰り返し繰り返され、プログラムを読んで理解する方法です。







次に、関数の本体を見てみましょう。







 func NewObject(n int) (*Object, error) { row := dbconn.QueryRow("SELECT ... FROM ... WHERE ...") var id string if err := row.Scan(&id); err != nil { logger.Log("during row scan: %v", err) id = "default" } resource, err := pool.Request(n) if err != nil { return nil, err } return &Object{ id: id, res: resource, }, nil }
      
      





この関数は、sqlパッケージのグローバルオブジェクト-database / sql.Connを使用して、不明瞭なデータベースに要求を行います。 次に、パッケージのグローバルロガーが、理解できない形式で、理解できない形式で文字列を書き込みます。 リソースを要求するグローバルクエリプールオブジェクト。 これらすべての操作には、関数シグネチャを読み取るときに完全に見えない副作用があります。 それを読んでいるプログラマーは、関数の本体を読み取り、グローバル変数の定義を調べることを除いて、これらのアクションを予測する方法はありません。







この署名オプションを試してみましょう。







 func NewObject(db *sql.DB, pool *resource.Pool, n int, logger log.Logger) (*Object, error)
      
      





これらのすべての依存関係をパラメーターとして上げるだけで、プログラマーは関数の依存関係と動作を正確にモデル化することができました。 これで、機能を実行するために関数が必要とするものを正確に把握し、必要なすべてを提供できます。







このパッケージ用のパブリックAPIを開発している場合、さらに先へ進むことができます。







 // RowQueryer models part of a database/sql.DB. type RowQueryer interface { QueryRow(string, ...interface{}) *sql.Row } // Requestor models the requesting side of a resource.Pool. type Requestor interface { Request(n int) (*resource.Value, error) } func NewObject(q RowQueryer, r Requestor, logger log.Logger) (*Object, error) { // ... }
      
      





必要なメソッドのみを含むインターフェイスとして特定の各オブジェクトをシミュレートすることにより、呼び出しコードで実装を簡単に切り替えることができました。 これにより、パッケージ間の接続性が低下し、テストで特定の依存関係を簡単にモックできます。 特定のグローバル変数を使用してコードの元のバージョンをテストするには、退屈でエラーが発生しやすいコンポーネントの置換が必要です。







すべてのコンストラクターと関数が明示的に依存関係をとる場合、グローバル変数はまったく必要ありません。 その見返りとして、メイン関数ですべてのデータベース接続、ロガー、リソースプールを作成し、コードの将来の読者がコンポーネントグラフ非常に明確に理解できるようにしました。 そして、すべての依存関係を非常に明確に伝えることができ、理解に有害なグローバル変数の魔法を取り除きます。 また、グローバル変数がない場合、init関数は不要になります。init関数の唯一の機能は、パッケージのグローバル状態を作成または変更することです。 その後、正当な疑いのあるint関数を使用するすべてのケースを見ることができます。実際、このコードは何をするのでしょうか? メイン関数にない場合、なぜですか?







そして、これは単に可能であるだけでなく、非常にシンプルであり、実際、非常に爽快で、実質的にグローバルなステータスがないGoプログラムを書くことです。 私の経験から、この方法でのプログラミングは、関数定義を減らすためにグローバル変数を使用するよりも遅くも退屈でもありません。 まったく逆です。関数シグネチャがその動作を確実かつ完全に記述する場合、大規模なコードベースのコードをはるかに効率的に議論、リファクタリング、および保守できます。 Goキットは最初からこのスタイルで書かれており、その恩恵を受けるだけでした。







-







そしてこの時点で、現代の囲Goの理論を定式化することができます。 Dave Cheney の言葉に基づいて 、私は次の規則を提案します。









もちろん、例外があります。 しかし、これらの規則に従って、他の慣行は自然に現れます。








All Articles