DEV Community

Kittipat.po
Kittipat.po

Posted on • Edited on

Understanding the Factory Method Pattern in Go

Factory Method Pattern

The Factory Method Pattern introduces a novel concept, creating objects without having to specify their exact types. This pattern allows developers to encapsulate the process of object creation, abstracting it behind a common interface or base class. Subclasses or implementations of this interface furnish the necessary creation logic, enabling clients to generate objects without delving into intricate implementation details.

Implementing the Factory Method Pattern in Go

Picture yourself orchestrating an e-commerce symphony, where customers from around the globe make purchases, each with their preferred payment method. You're tasked with weaving together a seamless experience for credit cards, digital wallets, and more. Here's where the Factory Method Pattern steps in – it lets you harmoniously integrate different payment processors without causing a coding cacophony.

package main

import (
    "errors"
    "fmt"
)

// PaymentGatewayType defines the type of payment gateway.
type PaymentGatewayType int

const (
    PayPalGateway PaymentGatewayType = iota
    StripeGateway
)

// PaymentGateway represents the common interface for payment gateways.
type PaymentGateway interface {
    ProcessPayment(amount float64) error
}

// PayPalGateway is a concrete payment gateway.
type PayPalGateway struct{}

func (pg *PayPalGateway) ProcessPayment(amount float64) error {
    fmt.Printf("Processing PayPal payment of $%.2f\n", amount)
    // Simulate PayPal payment processing logic.
    return nil
}

// StripeGateway is another concrete payment gateway.
type StripeGateway struct{}

func (sg *StripeGateway) ProcessPayment(amount float64) error {
    fmt.Printf("Processing Stripe payment of $%.2f\n", amount)
    // Simulate Stripe payment processing logic.
    return nil
}

// NewPaymentGateway creates a payment gateway based on the provided type.
func NewPaymentGateway(gwType PaymentGatewayType) (PaymentGateway, error) {
    switch gwType {
    case PayPalGateway:
        return &PayPalGateway{}, nil
    case StripeGateway:
        return &StripeGateway{}, nil
    default:
        return nil, errors.New("unsupported payment gateway type")
    }
}

func main() {
    payPalGateway, _ := NewPaymentGateway(PayPalGateway)
    payPalGateway.ProcessPayment(100.00)

    stripeGateway, _ := NewPaymentGateway(StripeGateway)
    stripeGateway.ProcessPayment(150.50)
}

Enter fullscreen mode Exit fullscreen mode

In this example, we define the PaymentGateway interface as the shared contract for all payment gateways. We implement two concrete payment gateways, PayPalGateway and StripeGateway, each with its respective ProcessPayment method.

The NewPaymentGateway function acts as the factory method, creating payment gateways based on the provided type. It encapsulates the creation logic and returns the appropriate instance, allowing the client to interact with different payment gateways using a unified interface.

The client code demonstrates how to use the Factory Method Pattern to create and process payments through different gateways. By invoking NewPaymentGateway with the desired type, the client can obtain instances of specific payment gateways.

Extending the Factory Method Pattern with Configurations

In the real world, each payment gateway may require specific configuration parameters:

  • PayPalGateway might need a ClientID and ClientSecret.
  • StripeGateway might require an APIKey.
  • Future gateways could have their own unique configurations.

How do you design your factory method to handle these differing configurations while keeping your code clean and maintainable?

Updated Implementation with Configurations

To accommodate different configurations for each payment gateway, we can modify the factory method to accept a configuration parameter of type interface{}. Inside the factory method, we'll use type assertions to determine the concrete type of the configuration and proceed accordingly.

package main

import (
    "errors"
    "fmt"
)

// PaymentGatewayType defines the type of payment gateway.
type PaymentGatewayType int

const (
    PayPalGateway PaymentGatewayType = iota
    StripeGateway
)

// PaymentGateway represents the common interface for payment gateways.
type PaymentGateway interface {
    ProcessPayment(amount float64) error
}

// PayPalGateway is a concrete payment gateway.
type PayPalGateway struct {
    ClientID     string
    ClientSecret string
}

func (pg *PayPalGateway) ProcessPayment(amount float64) error {
    fmt.Printf("Processing PayPal payment of $%.2f with ClientID: %s\n", amount, pg.ClientID)
    // Simulate PayPal payment processing logic.
    return nil
}

// StripeGateway is another concrete payment gateway.
type StripeGateway struct {
    APIKey string
}

func (sg *StripeGateway) ProcessPayment(amount float64) error {
    fmt.Printf("Processing Stripe payment of $%.2f with APIKey: %s\n", amount, sg.APIKey)
    // Simulate Stripe payment processing logic.
    return nil
}

// PayPalConfig is the configuration struct for PayPalGateway.
type PayPalConfig struct {
    ClientID     string
    ClientSecret string
}

// StripeConfig is the configuration struct for StripeGateway.
type StripeConfig struct {
    APIKey string
}

// NewPaymentGateway creates a payment gateway based on the provided type and configuration.
func NewPaymentGateway(gwType PaymentGatewayType, config interface{}) (PaymentGateway, error) {
    switch gwType {
    case PayPalGateway:
        paypalConfig, ok := config.(PayPalConfig)
        if !ok {
            return nil, errors.New("invalid config for PayPalGateway")
        }
        return &PayPalGateway{
            ClientID:     paypalConfig.ClientID,
            ClientSecret: paypalConfig.ClientSecret,
        }, nil
    case StripeGateway:
        stripeConfig, ok := config.(StripeConfig)
        if !ok {
            return nil, errors.New("invalid config for StripeGateway")
        }
        return &StripeGateway{
            APIKey: stripeConfig.APIKey,
        }, nil
    default:
        return nil, errors.New("unsupported payment gateway type")
    }
}

func main() {
    payPalGateway, err := NewPaymentGateway(PayPalGateway, PayPalConfig{
        ClientID:     "paypal-client-id",
        ClientSecret: "paypal-client-secret",
    })
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    payPalGateway.ProcessPayment(100.00)

    stripeGateway, err := NewPaymentGateway(StripeGateway, StripeConfig{
        APIKey: "stripe-api-key",
    })
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    stripeGateway.ProcessPayment(150.50)
}
Enter fullscreen mode Exit fullscreen mode

Alternative Approaches

While the above solution works well, there are other approaches to consider for handling different configurations.

Using Functional Options
Functional options allow for a more flexible configuration by using variadic functions. Here's how you can implement it:

type PaymentGatewayOption func(PaymentGateway) error

func WithClientID(clientID string) PaymentGatewayOption {
    return func(pg PaymentGateway) error {
        if paypal, ok := pg.(*PayPalGateway); ok {
            paypal.ClientID = clientID
            return nil
        }
        return errors.New("invalid option for this gateway")
    }
}

func WithClientSecret(clientSecret string) PaymentGatewayOption {
    return func(pg PaymentGateway) error {
        if paypal, ok := pg.(*PayPalGateway); ok {
            paypal.ClientSecret = clientSecret
            return nil
        }
        return errors.New("invalid option for this gateway")
    }
}

func WithAPIKey(apiKey string) PaymentGatewayOption {
    return func(pg PaymentGateway) error {
        if stripe, ok := pg.(*StripeGateway); ok {
            stripe.APIKey = apiKey
            return nil
        }
        return errors.New("invalid option for this gateway")
    }
}

func NewPaymentGateway(gwType PaymentGatewayType, opts ...PaymentGatewayOption) (PaymentGateway, error) {
    var pg PaymentGateway
    switch gwType {
    case PayPalGateway:
        pg = &PayPalGateway{}
    case StripeGateway:
        pg = &StripeGateway{}
    default:
        return nil, errors.New("unsupported payment gateway type")
    }

    for _, opt := range opts {
        if err := opt(pg); err != nil {
            return nil, err
        }
    }

    return pg, nil
}

func main() {
    payPalGateway, err := NewPaymentGateway(
        PayPalGateway,
        WithClientID("paypal-client-id"),
        WithClientSecret("paypal-client-secret"),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    payPalGateway.ProcessPayment(100.00)

    stripeGateway, err := NewPaymentGateway(
        StripeGateway,
        WithAPIKey("stripe-api-key"),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    stripeGateway.ProcessPayment(150.50)
}
Enter fullscreen mode Exit fullscreen mode

Conclusion 🥂

The Factory Method Pattern empowers flexible object creation, abstracting types from clients. By integrating configurations that vary between different products, you can enhance the pattern to handle real-world scenarios more effectively.

In our example, we've shown how to:

  • Implement the basic Factory Method Pattern to create payment gateways without exposing the creation logic to the client.
  • Extend the pattern to handle different configurations for each gateway by using type assertions and specific configuration structs.
  • Explore alternative approaches like functional options and separate factory functions to achieve the same goal with different trade-offs.

With seamless integration, the Factory Method Pattern fosters cleaner code and adapts to evolving needs. From payment gateways to varied objects, it's a design gem for crafting elegant software solutions. Embrace its versatility and elevate your coding prowess.

Top comments (1)

Collapse
 
ryuheechul profile image
Heechul Ryu

If anyone is new to iota, this article should be helpful, go101.org/article/constants-and-va...