設計パターンのトピックについては、大量の記事が書かれ、多くの本が出版されています。 ただし、パターンを使用すると、既成の実績のあるソリューションを使用できるため、コードの品質を改善し、技術的な負債を減らすことでプロジェクト開発の時間を短縮できるため、このトピックは関連しなくなります。
デザインパターンの出現以来、それらの効果的な使用の新しい例があります。 そしてこれは素晴らしい。 しかし、軟膏にはハエがありました。各言語には独自の特性があります。 そしてgolang-そしてさらにそうです(古典的なOOPモデルさえありません)。 したがって、個々のプログラミング言語に関連して、パターンにはさまざまなバリエーションがあります。 この記事では、golangに関連するデザインパターンのトピックに触れたいと思います。
デコレータ
デコレータテンプレートを使用すると、同じクラスの他のオブジェクトの動作に影響を与えることなく、オブジェクトに追加の動作を(静的または動的に)接続できます。 テンプレートは、特定の問題を解決するためにクラス間で機能を共有できるため、単一責任原則の遵守によく使用されます。
よく知られているDECORATORパターンは、多くのプログラミング言語で広く使用されています。 したがって、golangでは、すべてのミドルウェアはその基礎に基づいて構築されます。 たとえば、クエリプロファイリングは次のようになります。
func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) }
この場合、デコレータインターフェイスが唯一の機能です。 原則として、これを探す必要があります。 ただし、より広いインターフェイスを持つデコレータが役立つ場合があります。 たとえば、データベース(パッケージデータベース/ sql)へのアクセスを検討します。 データベースクエリの同じプロファイリングを行う必要があるとします。 この場合、次のものが必要です。
- ポインターを介してデータベースと直接対話する代わりに、インターフェイスを介して対話に移動する必要があります(動作を実装から分離するため)。
- SQLデータベースクエリを実行する各メソッドのラッパーを作成します。
その結果、データベースへのすべてのクエリのプロファイルを作成できるデコレータを取得します。 このアプローチの利点は否定できません。
- メインデータベースアクセスコンポーネントのコードの清潔さを維持します。
- 各デコレータは単一の要件を実装します。 これにより、実装が容易になります。
- デコレータの構成により、ニーズに簡単に適応できる拡張可能なモデルが得られます。
- プロファイラーの単純なシャットダウンにより、実稼働モードでパフォーマンスオーバーヘッドがゼロになります。
したがって、たとえば、次のタイプのデコレータを実装できます。
- ハートビート データベースへの接続を維持するためにデータベースにpingを実行します。
- プロファイラー。 リクエストボディとその実行時間の両方の出力。
- スニファー。 データベースメトリックのコレクション。
- クローン デバッグのために元のデータベースを複製します。
原則として、リッチデコレータを実装する場合、すべてのメソッドの実装は必要ありません。実装されていないメソッドを内部オブジェクトに委任すれば十分です。
データベースのDMLクエリを追跡する(INSERT / UPDATE / DELETEクエリを追跡する)ために高度なロガーを実装する必要があるとします。 この場合、データベースインターフェイス全体を実装する必要はありません。Execメソッドのみをオーバーラップさせます。
type MyDatabase interface{ Query(...) (sql.Rows, error) QueryRow(...) error Exec(query string, args ...interface) error Ping() error } type MyExecutor struct { MyDatabase } func (e *MyExecutor) Exec(query string, args ...interface) error { ... }
したがって、golang言語でリッチなデコレータを作成することも特に難しいことではないことがわかります。
テンプレートメソッド
テンプレートメソッド(Eng。テンプレートメソッド)-アルゴリズムの基礎を定義し、相続人が全体として構造を変更せずにアルゴリズムのいくつかのステップを再定義できるようにする動作設計パターン。
golang言語はOOPパラダイムをサポートしているため、このテンプレートを純粋な形式で実装することはできません。 ただし、適切な関数を使用してコンストラクターを即興で作成することを妨げるものはありません。
次の署名を使用してテンプレートメソッドを定義する必要があるとします。
func Method(s string) error
宣言するときは、機能型のフィールドを使用するだけで十分です。 作業の便宜上、不足しているパラメーターで呼び出しを補完するラッパー関数を使用して、対応するコンストラクター関数である特定のインスタンスを作成できます。
type MyStruct struct { MethodImpl func (me *MyStruct, s string) error } // Wrapper for template method func (ms *MyStruct) Method(s string) error { return ms.MethodImpl(ms, s) } // First constructor func NewStruct1() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 1 ... }, } } // Second constructor func NewStruct2() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 2 ... }, } } func main() { // Create object instance o := NewStruct2() // Call the template method err := o.Method("hello") ... }
例からわかるように、パターンを使用するセマンティクスは、従来のOOPとほとんど変わりません。
アダプター
アダプター設計パターンにより、既存のクラスのインターフェースを別のインターフェースとして使用できます。 このテンプレートは、ソースコードを変更せずに一部のクラスが他のクラスと連携するようにするためによく使用されます。
一般に、個々の機能とインターフェース全体の両方がアダプターとして機能します。 インターフェイスですべてが多かれ少なかれ明確で予測可能であれば、個々の機能の観点から微妙な点があります。
内部APIを持つサービスを作成しているとします:
type MyService interface { Create(ctx context.Context, order int) (id int, err error) }
パブリックAPIに別のインターフェイスを提供する必要がある場合(gRPCを使用する場合など)、インターフェイスの変換を処理するアダプター関数を使用できます。 この目的のためにクロージャーを使用すると非常に便利です。
type Endpoint func(ctx context.Context, request interface{}) (interface{}, error) type CreateRequest struct { Order int } type CreateResponse struct { ID int, Err error } func makeCreateEndpoint(s MyService) Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { // Decode request req := request.(CreateRequest) // Call service method id, err := s.Create(ctx, req.Order) // Encode response return CreateResponse{ID: id, Err: err}, nil } }
makeCreateEndpoint関数には、3つの標準ステップがあります。
- デコード値
- 実装されたサービスの内部APIからメソッドを呼び出す
- 値のエンコード
gokitパッケージのすべてのエンドポイントは、この原則に基づいて構築されています。
訪問者
「Visitor」テンプレートは、アルゴリズムを、それが動作するオブジェクトの構造から分離する方法です。 分離の結果、既存のオブジェクト構造を変更せずに、新しい操作を追加できます。 これは、オープン/クローズド原則に準拠する1つの方法です。
幾何学的形状の例でよく知られている訪問者パターンを検討してください。
type Geometry interface { Visit(GeometryVisitor) (interface{}, error) } type GeometryVisitor interface { VisitPoint(p *Point) (interface{}, error) VisitLine(l *Line) (interface{}, error) VisitCircle(c *Circle) (interface{}, error) } type Point struct{ X, Y float32 } func (point *Point) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitPoint(point) } type Line struct{ X1, Y1 float32 X2, Y2 float32 } func (line *Line) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitLine(line) } type Circle struct{ X, Y, R float32 } func (circle *Circle) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitCircle(circle) }
与えられた点から指定された形状までの距離を計算するための戦略を書きたいとします。
type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) { // Evaluate distance from point(X, Y) to point p } func (s *DistanceStrategy) VisitLine(l *Line) (interface{}, error) { // Evaluate distance from point(X, Y) to line l } func (s *DistanceStrategy) VisitCircle(c *Circle) (interface{}, error) { // Evaluate distance from point(X, Y) to circle c } func main() { s := &DistanceStrategy{X: 1, Y: 2} p := &Point{X: 3, Y: 4} res, err := p.Visit(s) if err != nil { panic(err) } fmt.Printf("Distance is %g", res.(float32)) }
同様に、必要な他の戦略を実装できます。
- 垂直範囲
- オブジェクトの水平範囲
- 最小スパニングスクエア(MBR)の構築
- 必要な他のプリミティブ。
さらに、以前に定義された数字(点、線、円など)は、これらの戦略について何も知りません。 彼らの唯一の知識は、GeometryVisitorインターフェースに限定されています。 これにより、それらを個別のパッケージに分離できます。
かつて、地図作成プロジェクトに取り組んでいたとき、2つの任意の地理的オブジェクト間の距離を決定する関数を作成するタスクがありました。 ソリューションは非常に異なっていましたが、それらはすべて十分に効率的でエレガントではありませんでした。 Visitorパターンをどうにかして考えると、ターゲットメソッドを選択するのに役立ち、ご存知のようにタスクを簡素化する別の再帰ステップにいくらか似ていることに気付きました。 これにより、Double Visitorを使用するようになりました。 そのようなアプローチがインターネット上でまったく言及されていないことを発見したとき、私の驚きを想像してください。
type geometryStrategy struct{ G Geometry } func (s *geometryStrategy) VisitPoint(p *Point) (interface{}, error) { return sGVisit(&pointStrategy{Point: p}) } func (d *geometryStrategy) VisitLine(l *Line) (interface{}, error) { return sGVisit(&lineStrategy{Line: l}) } func (d *geometryStrategy) VisitCircle(c *Circle) (interface{}, error) { return sGVisit(&circleStrategy{Circle: c}) } type pointStrategy struct{ *Point } func (point *pointStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between point and p } func (point *pointStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between point and l } func (point *pointStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between point and c } type lineStrategy struct { *Line } func (line *lineStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between line and p } func (line *lineStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between line and l } func (line *lineStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between line and c } type circleStrategy struct { *Circle } func (circle *circleStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between circle and p } func (circle *circleStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between circle and l } func (circle *circleStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between circle and c } func Distance(a, b Geometry) (float32, error) { return a.Visit(&geometryStrategy{G: b}) }
したがって、2レベルの選択メカニズムを構築しました。このメカニズムは、その作業の結果として、2つのプリミティブ間の距離を計算するための適切なメソッドを呼び出します。 これらのメソッドのみを記述でき、目標は達成されました。 これは、エレガントで非決定的な問題をいくつかの基本関数に還元する方法です。
おわりに
golangには古典的なOOPがないという事実にもかかわらず、この言語は、言語の強みを生かした独自のパターンの方言を生成します。 これらのパターンは、拒否から普遍的な受け入れまで標準的な方法で進み、時間とともにベストプラクティスになります。
尊敬されるhabrozhitelemがパターンについて何か考えを持っているなら、恥ずかしがらずにこれについてあなたの考えを表現してください。