Design Pattern Series - Factory Pattern (Golang)
What is the Factory Pattern?
The Factory Pattern is a design pattern belonging to the Creational Patterns group. It provides a way to create objects without directly specifying their concrete class. Instead, the Factory Pattern uses a factory method or a factory struct to determine which type of object to create based on input parameters or conditions.
Since Go does not support inheritance like traditional object-oriented languages, the Factory Pattern is typically implemented using functions or structs to create objects, leveraging Go's interface feature to ensure flexibility and abstraction.
What is the Purpose of the Factory Pattern?
The Factory Pattern serves the following purposes:
- Abstracts Object Creation: It hides the logic of creating objects, making the code easier to maintain and extend.
- Increases Flexibility: Allows adding new object types without modifying existing code (following the Open/Closed Principle).
- Centralized Control: Keeps object creation logic in one place, making it easier to modify or optimize.
- Reduces Dependencies: Client code interacts only with interfaces or abstract types, without needing to know the details of specific classes.
Use Cases
The Factory Pattern is commonly used in these scenarios:
- When you need to create objects of different types that share a common interface.
- When object creation logic is complex, such as initializing with default values or specific configurations.
- When you want to separate object creation logic from the logic that uses the objects.
Practical Examples
- Creating Database Connections: Generating different database connections (e.g., MySQL, PostgreSQL, SQLite) based on configuration.
- Handling Payment Types: Managing various payment methods (e.g., CreditCard, PayPal) in an e-commerce system.
Example Implementation of the Factory Pattern in Go
To illustrate, we will implement a simple example: a payment system that supports two types of payments, CreditCard and PayPal. Each payment type will have a common method called Pay. We will use the Factory Pattern to create payment objects based on the requested payment type.
Step 1: Define the Interface for Payment Types
First, we define a PaymentMethod interface to ensure all payment types implement the Pay method.
type PaymentMethod interface {
Pay(amount float64) string
}
This interface requires all payment types to implement the Pay method, which takes an amount and returns a string describing the transaction.
Step 2: Implement Concrete Structs
Next, we create two concrete structs, CreditCardPayment and PayPalPayment, each implementing the PaymentMethod interface.
CreditCardPayment:
type CreditCardPayment struct{}
func (c *CreditCardPayment) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using Credit Card", amount)
}
PayPalPayment:
type PayPalPayment struct{}
func (p *PayPalPayment) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using PayPal", amount)
}
Step 3: Create the Factory to Instantiate Objects
Now, we create a factory function to determine which payment object to create based on an input parameter (e.g., payment type).
func PaymentMethodFactory(paymentType string) (PaymentMethod, error) {
switch paymentType {
case "creditcard":
return &CreditCardPayment{}, nil
case "paypal":
return &PayPalPayment{}, nil
default:
return nil, fmt.Errorf("unsupported payment method: %s", paymentType)
}
}
The PaymentMethodFactory function takes a paymentType string and returns an object that satisfies the PaymentMethod interface. If the payment type is unsupported, it returns an error.
Step 4: Use the Factory Pattern in Client Code
Finally, we use the factory in the main code to create payment objects and process payments.
func main() {
// Create a CreditCardPayment object
creditCard, err := PaymentMethodFactory("creditcard")
if err != nil {
log.Fatal(err)
}
fmt.Println(creditCard.Pay(100.50))
// Create a PayPalPayment object
payPal, err := PaymentMethodFactory("paypal")
if err != nil {
log.Fatal(err)
}
fmt.Println(payPal.Pay(75.25))
// Try an invalid payment type
_, err = PaymentMethodFactory("bitcoin")
if err != nil {
fmt.Println(err)
}
}
Explanation of Each Step
- Step 1: The PaymentMethod interface ensures abstraction, allowing client code to work with any payment type without knowing its implementation details.
- Step 2: The CreditCardPayment and PayPalPayment structs provide concrete implementations of the Pay method.
- Step 3: The PaymentMethodFactory function centralizes object creation logic, so client code doesn’t need to know how objects are created.
- Step 4: The client code uses the factory to create objects and call the Pay method. Errors (e.g., invalid payment types) are handled clearly.
Complete Code
Below is the full Go code, combining all the parts into a single file.
package main
import (
"fmt"
"log"
)
// Define the PaymentMethod interface
type PaymentMethod interface {
Pay(amount float64) string
}
// Implement CreditCardPayment
type CreditCardPayment struct{}
func (c *CreditCardPayment) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using Credit Card", amount)
}
// Implement PayPalPayment
type PayPalPayment struct{}
func (p *PayPalPayment) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using PayPal", amount)
}
// Factory function to create PaymentMethod
func PaymentMethodFactory(paymentType string) (PaymentMethod, error) {
switch paymentType {
case "creditcard":
return &CreditCardPayment{}, nil
case "paypal":
return &PayPalPayment{}, nil
default:
return nil, fmt.Errorf("unsupported payment method: %s", paymentType)
}
}
func main() {
// Create a CreditCardPayment object
creditCard, err := PaymentMethodFactory("creditcard")
if err != nil {
log.Fatal(err)
}
fmt.Println(creditCard.Pay(100.50))
// Create a PayPalPayment object
payPal, err := PaymentMethodFactory("paypal")
if err != nil {
log.Fatal(err)
}
fmt.Println(payPal.Pay(75.25))
// Try an invalid payment type
_, err = PaymentMethodFactory("bitcoin")
if err != nil {
fmt.Println(err)
}
}
Program Output
When you run the program, the output will look something like this:
Paid 100.50 using Credit Card
Paid 75.25 using PayPal
unsupported payment method: bitcoin
Conclusion
The Factory Pattern is a powerful tool for managing object creation in Go, especially when you need to support multiple object types that share a common interface. By using the Factory Pattern, you can make your code more extensible, maintainable, and aligned with object-oriented design principles.
To extend this example, you could:
- Add new payment types (e.g., BankTransfer, Crypto).
- Incorporate more complex initialization logic in the factory.
- Use additional configurations or parameters to customize the created objects.
Hopefully, this article helps you better understand the Factory Pattern and how to apply it in Go!
Next post in the series Abstract Factory Pattern