ホームページ  >  記事  >  バックエンド開発  >  Golang でクリーン アーキテクチャが苦戦する理由と、より良く機能するもの

Golang でクリーン アーキテクチャが苦戦する理由と、より良く機能するもの

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2024-11-07 01:59:02493ブラウズ

Why Clean Architecture Struggles in Golang and What Works Better

Golang は、シンプルさを優先した高速で効率的な言語として確固たる評判を築いてきました。これが、Golang がバックエンド サービス、マイクロサービス、インフラストラクチャ ツールに非常によく使用される理由の 1 つです。しかし、Java や C# などの言語から Go に移行する開発者が増えるにつれ、クリーン アーキテクチャの実装に関する疑問が生じます。 Clean Architecture のレイヤーベースのアプリケーション構築アプローチに慣れている人にとっては、同じ原則を Go に適用することが直感的に感じられるでしょう。ただし、これから説明するように、Go でクリーン アーキテクチャを実装しようとすると裏目に出ることがよくあります。代わりに、より単純で柔軟で、Go の「シンプルにする」哲学に沿った、Go の強みに合わせた構造を見ていきます。

クリーンなアーキテクチャが Go に場違いに感じられる理由

ボブおじさん (ロバート C. マーティン) が擁護するクリーン アーキテクチャの目標は、モジュール式でテスト可能で拡張が簡単なソフトウェアを作成することです。これは、コアのビジネス ロジックを外部の懸念事項から隔離して、レイヤー間の懸念事項の分離を強制することによって実現されます。これは Java のような高度なオブジェクト指向言語ではうまく機能しますが、Go では摩擦が生じます。その理由は次のとおりです:

1. Go のミニマリズムは過度の抽象化と戦う

Go では、読みやすさ、シンプルさ、オーバーヘッドの削減が重視されます。 Clean Architecture では、インターフェイス、依存関係の反転、複雑な依存関係の注入、ビジネス ロジックのサービス レイヤーなど、抽象化のレイヤーを重ねて導入します。ただし、これらの追加レイヤーは、Go で実装する場合に不必要な複雑さを追加する傾向があります。

Kubernetes を例に挙げてみましょう。 Kubernetes は Go で構築された大規模なプロジェクトですが、クリーン アーキテクチャの原則に依存していません。代わりに、パッケージとサブシステムに重点を置いたフラットな機能指向の構造を採用しています。これは、Kubernetes GitHub リポジトリで確認できます。ここでは、パッケージが厳密なレイヤーではなく機能別に編成されています。 Kubernetes は、機能に基づいてコードをグループ化することで、複雑な抽象化を行わずに高度なモジュール性を実現します。

囲碁の哲学は実用性とスピードを優先します。この言語の作成者は、過剰なアーキテクチャを避け、単純な実装を好むことを一貫して主張してきました。抽象化が絶対に必要でない場合、それは Go コードに属しません。 Go の作成者は、オーバーエンジニアリングの落とし穴を避けるために継承を行わずに言語を設計し、開発者に設計をクリーンかつ明確に保つよう奨励しました。

2. 依存関係の注入は設計により制限されています

クリーン アーキテクチャは、依存関係の挿入に大きく依存して、さまざまなレイヤーを分離し、モジュールをよりテストしやすくします。 Java などの言語では、Spring などのフレームワークのおかげで、DI はエコシステムの自然な一部となっています。これらのフレームワークは DI を自動的に処理するため、コードを乱雑にすることなく、依存関係を簡単に結び付けることができます。

しかし、Go にはネイティブ DI システムが欠けており、Go 用の DI ライブラリのほとんどは過度に複雑であるか、単調に感じられます。 Go は、コンストラクター関数または関数パラメーターを介した明示的な依存関係の注入に依存しており、依存関係を明確に保ち、​​DI コンテナーに隠された「魔法」を回避します。 Go のアプローチはコードをより明示的にしますが、あまりにも多くのレイヤーを導入すると、依存関係の管理が管理できなくなり、冗長になることも意味します。

たとえば、Kubernetes では、複雑な DI フレームワークや DI コンテナは見られません。代わりに、依存関係はコンストラクターを使用して直接注入されます。この設計により、コードの透過性が維持され、DI フレームワークの落とし穴が回避されます。 Golang は、本当に意味のある場合にのみ DI を使用することを奨励しています。これが、Kubernetes がパターンに従うためだけに不必要なインターフェイスや依存関係を作成することを避ける理由です。

3. レイヤーが多すぎるとテストがより複雑になる

Go のクリーン アーキテクチャに関するもう 1 つの課題は、テストが不必要に複雑になる可能性があることです。たとえば、Java では、Clean Architecture は依存関係のモックを多用する堅牢な単体テストをサポートしています。モックを使用すると、各レイヤーを分離し、個別にテストできます。ただし、Go ではモックの作成が面倒な場合があり、Go コミュニティでは一般に、可能な限り統合テストまたは実際の実装を使用したテストを好みます。

Kubernetes などの実稼働グレードの Go プロジェクトでは、テストは各コンポーネントを分離することではなく、実際のシナリオをカバーする統合テストとエンドツーエンドのテストに重点を置いて処理されます。抽象化レイヤーを削減することで、Kubernetes のような Go プロジェクトは、テストを実際の動作に近づけながら高いテスト カバレッジを実現し、実稼働環境にデプロイする際の信頼性が高まります。

Golang に最適なアーキテクチャ アプローチ

では、クリーン アーキテクチャが Go に適合しないとしたら、何が適合するのでしょうか?その答えは、パッケージを強調し、厳密な階層化よりもモジュール性に焦点を当てた、よりシンプルで機能的な構造にあります。 Go の効果的なアーキテクチャ パターンの 1 つは、ポートとアダプタ として知られる ヘキサゴナル アーキテクチャ に基づいています。このアーキテクチャにより、過度の階層化を行わずにモジュール性と柔軟性が可能になります。

Golang 標準プロジェクト レイアウトは、Go で本番環境に対応したプロジェクトを作成するための優れた出発点です。この構造は、アーキテクチャ層ごとではなく、目的と機能ごとにコードを整理するための基盤を提供します。

Go プロジェクトの構造: 実践的な例

あなたの言う通りです!パッケージ中心のアプローチで Go プロジェクトを構造化すると、機能が階層化されたフォルダー構造ではなくパッケージごとに分割され、Go の設計原則とよりよく一致します。 Go では、レイヤー (コントローラー、サービス、リポジトリなど) ごとにトップレベルのディレクトリを作成するのではなく、それぞれが独自のモデル、サービス、リポジトリをカプセル化する、まとまりのあるパッケージを作成する方が慣用的です。このパッケージベースのアプローチは結合を軽減し、コードをモジュール化した状態に保ちます。これは実稼働グレードの Go アプリケーションに不可欠です。

Go に適した洗練されたパッケージ中心の構造を見てみましょう:

/myapp
   /cmd                   // Entrypoints for different executables (e.g., main.go)
      /myapp-api
         main.go          // Entrypoint for the main application
   /config                // Configuration files and setup
   /internal              // Private/internal packages (not accessible externally)
      /user               // Package focused on user-related functionality
         models.go        // Data models and structs specific to user functionality
         service.go       // Core business logic for user operations
         repository.go    // Database access methods for user data
      /order              // Package for order-related logic
         models.go        // Data models for orders
         service.go       // Core order-related logic
         repository.go    // Database access for orders
   /pkg                   // Shared, reusable packages across the application
      /auth               // Authorization and authentication package
      /logger             // Custom logging utilities
   /api                   // Package with REST or gRPC handlers
      /v1
         user_handler.go  // Handler for user-related endpoints
         order_handler.go // Handler for order-related endpoints
   /utils                 // General-purpose utility functions and helpers
   go.mod                 // Module file

パッケージベースの構造の主要コンポーネント

  1. /cmd

このフォルダーは、アプリケーションのエントリ ポイントの従来の場所です。ここでの各サブフォルダーは、アプリの異なる実行可能ファイルを表します。たとえば、マイクロサービス アーキテクチャでは、各サービスが main.go を持つ独自のディレクトリを持つことができます。ここのコードは最小限で、ブートストラップと依存関係のセットアップのみを担当する必要があります。

  1. /config

環境変数や外部構成のロードなど、構成ファイルとセットアップ ロジックを保存します。このパッケージでは、アプリケーション構成の構造も定義できます。

  1. /内部

ここにアプリケーションのコア ロジックが存在し、機能に基づいてパッケージに分割されます。 Go は、外部モジュールから内部パッケージへのアクセスを制限し、これらのパッケージをアプリケーションに対してプライベートに保ちます。各パッケージ (ユーザー、オーダーなど) は自己完結型であり、独自のモデル、サービス、リポジトリを備えています。これは、過剰な階層化を行わずにカプセル化するという Go の哲学の鍵となります。

  • /internal/user – モデル (データ構造)、サービス (ビジネス ロジック)、リポジトリ (データベース インタラクション) など、ユーザー関連の機能をすべて管理します。これにより、ユーザー関連のロジックが 1 つのパッケージに保持され、保守が容易になります。

  • /internal/order – 同様に、このパッケージは注文関連のコードをカプセル化します。各機能領域には独自のモデル、サービス、リポジトリがあります。

  1. /パッケージ

pkg は、アプリケーション全体で使用されるが、特定のパッケージに固有ではない再利用可能なコンポーネントを保持します。認証用の auth やカスタム ログ用のロガーなど、独立して使用できるライブラリまたはユーティリティがここに保管されます。これらのパッケージが特に役立つ場合は、後で独自のモジュールに抽出することもできます。

  1. /API

API パッケージは、HTTP または gRPC ハンドラーのレイヤーとして機能します。ここでのハンドラーは、受信リクエストを処理し、サービスを呼び出し、応答を返します。 API バージョン (v1 など) ごとにハンドラーをグループ化することは、バージョン管理の良い習慣であり、将来の変更を分離しておくのに役立ちます。

  1. /utils

特定のパッケージに関連付けられていないが、コードベース全体にわたる横断的な目的 (例: 日付解析、文字列操作) を提供する汎用ユーティリティ。これを最小限に抑え、純粋にユーティリティ機能に重点を置くと便利です。

ユーザーパッケージのサンプルコードレイアウト

構造を説明するために、ユーザー パッケージがどのようなものかを詳しく見てみましょう:

モデル.ゴー

/myapp
   /cmd                   // Entrypoints for different executables (e.g., main.go)
      /myapp-api
         main.go          // Entrypoint for the main application
   /config                // Configuration files and setup
   /internal              // Private/internal packages (not accessible externally)
      /user               // Package focused on user-related functionality
         models.go        // Data models and structs specific to user functionality
         service.go       // Core business logic for user operations
         repository.go    // Database access methods for user data
      /order              // Package for order-related logic
         models.go        // Data models for orders
         service.go       // Core order-related logic
         repository.go    // Database access for orders
   /pkg                   // Shared, reusable packages across the application
      /auth               // Authorization and authentication package
      /logger             // Custom logging utilities
   /api                   // Package with REST or gRPC handlers
      /v1
         user_handler.go  // Handler for user-related endpoints
         order_handler.go // Handler for order-related endpoints
   /utils                 // General-purpose utility functions and helpers
   go.mod                 // Module file

サービス.ゴー

// models.go - Defines the data structures related to users

package user

type User struct {
    ID       int
    Name     string
    Email    string
    Password string
}

リポジトリ.go

// service.go - Contains the core business logic for user operations

package user

type UserService struct {
    repo UserRepository
}

// NewUserService creates a new instance of UserService
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) RegisterUser(name, email, password string) error {
    // Business logic for registering a user
    newUser := User{Name: name, Email: email, Password: password}
    return s.repo.Save(newUser)
}

このパッケージベースの構造が Go に最適な理由

この構造は Go のイディオムとよく一致しています:

  1. カプセル化

機能に基づいてパッケージを編成することで、コードは自然にカプセル化され、モジュール化されます。各パッケージは独自のモデル、サービス、およびリポジトリを所有し、コードの一貫性と高度なモジュール性を維持します。これにより、個々のパッケージの移動、理解、テストが容易になります。

  1. 最小限のインターフェース

インターフェイスは、テストと柔軟性にとって最も意味のあるパッケージ境界 (例: UserRepository) でのみ使用されます。このアプローチにより、Go コードの保守が困難になる可能性がある不要なインターフェイスの乱雑さが軽減されます。

  1. 明示的な依存関係の注入

依存関係はコンストラクター関数 (NewUserService など) を介して挿入されます。これにより、依存関係が明示的に保たれ、複雑な依存関係注入フレームワークの必要性が回避され、Go のシンプルさを重視した設計が忠実に保たれます。

  1. /pkg の再利用性

pkg ディレクトリ内の auth や logger などのコンポーネントはパッケージ間で共有できるため、過剰な結合なしで再利用性が促進されます。

  1. 明確な API 構造

ハンドラーを /api の下にグループ化すると、アプリケーションの成長に合わせて API レイヤーを拡張したり、新しいバージョンやハンドラーを追加したりすることが簡単になります。各ハンドラーは、コードをモジュール化してクリーンに保ち、リクエストの処理とサービスとの調整に集中できます。

このパッケージ中心の構造により、それぞれが独自のモデル、サービス、リポジトリを持つドメイン (製品、在庫など) を追加するたびに拡張できます。ドメインによる分離は、Go のコードを編成する慣用的な方法と一致しており、厳格な階層化よりも単純さと明瞭さに忠実です。

意見と実体験

Go を使った私の経験では、クリーン アーキテクチャは多くの場合、重要な価値を追加せずにコードベースを複雑にします。クリーン アーキテクチャは、DI のサポートが多数組み込まれており、深い継承構造の管理が一般的なニーズである Java などの言語で大規模なエンタープライズ グレードのアプリケーションを構築する場合に意味をなす傾向があります。ただし、Go のミニマリズム、シンプルさ第一の考え方、同時実行性とエラー処理への直接的なアプローチは、まったく異なるエコシステムを作成します。

結論: Go の慣用的なアーキテクチャを受け入れる

Java のバックグラウンドを持っている場合は、クリーン アーキテクチャを Go に適用したくなるかもしれません。ただし、Go の強みは、複雑な抽象化を行わないシンプルさ、透明性、およびモジュール性にあります。 Go の理想的なアーキテクチャでは、機能別に整理されたパッケージ、最小限のインターフェイス、明示的な DI、現実的なテスト、柔軟性を高めるアダプターが優先されます。

Go プロジェクトを設計するときは、Kubernetes、Vault、Golang 標準プロジェクト レイアウトなどの実世界の例に注目してください。これらは、アーキテクチャが厳格な構造よりもシンプルさを採用した場合に、Go がいかに強力になるかを示しています。 Go をクリーン アーキテクチャの型に当てはめようとするのではなく、Go 自体と同じくらい単純で効率的なアーキテクチャを採用してください。このようにして、慣用的なだけでなく、理解、保守、拡張が容易なコードベースを構築できます。

以上がGolang でクリーン アーキテクチャが苦戦する理由と、より良く機能するものの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。