Implementing multiple HTTP Clients with Decorator Pattern in Go

Implementing multiple HTTP Clients with Decorator Pattern in Go

December 3, 2023

Matryoshka Dolls in various colours and sizes

The decorator pattern is a structural design pattern that allows augmenting an object’s behaviour without altering its core structure. In Go, this pattern is especially powerful when crafting various HTTP clients with specific functionalities while maintaining a clean and adaptable codebase.

In Go, since there is no direct support for classes and inheritance as in some other languages like Java or C#, the decorator pattern is typically implemented using interfaces and embedding. This article will explore how to implement the decorator pattern in Go and how to leverage it to create multiple HTTP clients with specific functionalities.

Implementing Decorators for HTTP Clients

To illustrate this, let us imagine an e-commerce scenario that encompasses interactions with several microservices:

  • Product Catalog Service: facilitating product information retrieval.
  • Payments Service: managing payment processes, statuses, and cancellations.
  • Orders Service: handling order placement, status inquiries, and shipping data.
classDiagram
    class HTTPClient {
        << Interface >>
        + Do(req: Request): Response
    }
    class BaseHTTPClient {
        + Do(req: Request): Response
    }
    class ProductCatalogClient {
        - Client: BaseHTTPClient
        + Do(req: Request): Response
        + GetProductList(): Product
        + GetProductVariations(productID: string): Product
        + GetProduct(productID: string): Product
        HTTPClient <|.. ProductCatalogClient : Decorates
    }
    class PaymentServiceClient {
        - Client: BaseHTTPClient
        + Do(req: Request): Response
        + ProcessPayment(paymentData: Any): PaymentResponse
        HTTPClient <|.. PaymentServiceClient : Decorates
    }
    class OrderServiceClient {
        - Client: BaseHTTPClient
        + Do(req: Request): Response
        + PlaceOrder(orderData: Any): OrderResponse
        + GetOrderStatus(orderID: string): OrderResponse
        + CancelOrder(orderID: string): OrderResponse
        HTTPClient <|.. OrderServiceClient : Decorates
    }

    HTTPClient <|.. BaseHTTPClient : Implements
    BaseHTTPClient <|.. ProductCatalogClient : Decorates
    BaseHTTPClient <|.. PaymentServiceClient : Decorates
    BaseHTTPClient <|.. OrderServiceClient : Decorates

To implement these functionalities, we construct a base HTTP Client and employ the decorator pattern to craft specialized clients for each service:

// Base HTTP client interface
type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
}

// Base HTTP client implementation
type BaseHTTPClient struct{}

func (c *BaseHTTPClient) Do(req *http.Request) (*http.Response, error) {
    client := &http.Client{}
    
    return client.Do(req)
}

Specialized Clients

Product Catalog Client

The ProductCatalogClient is a decorator that provides access to the product catalog service, which offers methods to retrieve product information.

// ProductCatalogClient is a decorator to access product catalog
type ProductCatalogClient struct {
    Client HTTPClient
}

// Implementing additional logic specific to accessing the product catalog
func (p *ProductCatalogClient) Do(req *http.Request) (*http.Response, error) {
    // Add necessary headers, modify the request, etc.
    return p.Client.Do(req)
}

// GetProductList retrieves a list of products
func (p *ProductCatalogClient) GetProductList() (interface{}, error) {
    // ...
}
 // GetProductVariations returns all product variations based on the product ID
func (p *ProductCatalogClient) GetProductVariations(productID string) (interface{}, error) {
    // ...
}
// GetProduct get a product by ID
func (p *ProductCatalogClient) GetProduct(productID string) (interface{}, error) {
    // ...
}

Payment Service Client

The PaymentServiceClient is a decorator responsible for processing payments.

// PaymentServiceClient is a decorator to process payments
type PaymentServiceClient struct {
    Client HTTPClient
}

// Do implements additional logic specific to payment processing
func (p *PaymentServiceClient) Do(req *http.Request) (*http.Response, error) {
    // Add authentication headers, handle payment data, etc.
    return p.Client.Do(req)
}

// ProcessPayment initiates payment processing
func (p *PaymentServiceClient) ProcessPayment(paymentData interface{}) (interface{}, error) {
    // ...
}

Order Placement Client

The OrderServiceClient is a decorator responsible for placing orders and managing orders with methods to place, check status, and cancel orders.

// OrderServiceClient is a decorator to place and manage orders
type OrderServiceClient struct {
    Client HTTPClient
}

// Implementing additional logic specific to order placement
func (o *OrderServiceClient) Do(req *http.Request) (*http.Response, error) {
    // Handle order data, add necessary headers, etc.
    return o.Client.Do(req)
}

// PlaceOrder places an order based on the data provided
func (o *OrderServiceClient) PlaceOrder(orderData OrderDetails) (interface{}, error) {
    // ...
}

// GetOrderStatus gets the order status by orderID
func (o *OrderServiceClient) GetOrderStatus(orderID string) (interface{}, error) {
    // ...
}
// CancelOrder cancels the order by ID
func (o *OrderServiceClient) CancelOrder(orderID string) (interface{}, error) {
    // ...
}

Leveraging the Decorator Pattern

The decorator implements the same interface as the base object. This adherence to a common interface allows seamless interchangeability, ensuring that clients interacting with the base object can also work with the decorated objects without needing to know the specific implementation details. This allows for the addition of extra behaviours or functionalities in decorators without altering the base object’s interface, and by adding new methods we are not breaking the pattern as long as the base interface remains intact and all decorators, including the extended ones, adhere to that base interface.

In our example, we can create the specialized HTTP clients for each service by decorating the base HTTP client:

func main() {
    baseClient := &BaseHTTPClient{}

    // Decorating base client for payment processing
    paymentClient := &PaymentServiceClient{Client: baseClient}

    // Decorating base client for order placement
    orderClient := &OrderServiceClient{Client: baseClient}

    // Decorating base client for product catalog access
    productCatalogClient := &ProductCatalogClient{Client: baseClient}

    // Now you can use each specialized clients for their respective methods
    // Examples: 
    // paymentResponse, err := paymentClient.ProcessPayment(paymentData)
    // orderResponse, err := orderClient.PlaceOrder(orderData)
    // productList, err := productCatalogClient.GetProductList()
}

Each specific client (paymentClient, orderClient, productCatalogClient) now offers methods directly corresponding to the functionalities of their respective service within the e-commerce system. The decorator pattern enables a modular and flexible approach, allowing us to easily add, modify, or remove functionalities without altering the base HTTP client structure.