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)
}
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 aClientID
andClientSecret
. -
StripeGateway
might require anAPIKey
. - 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)
}
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)
}
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)
If anyone is new to iota, this article should be helpful, go101.org/article/constants-and-va...