avatarRamseyjiang

Summary

The provided web content discusses the Abstract design pattern in Golang, detailing its concept, objectives, pros and cons, practical scenarios, implementation steps, and real-world examples with unit tests.

Abstract

The Abstract design pattern is a creational design pattern in Golang that allows for the abstraction of object creation, encapsulating the creation logic into separate factory structs. This pattern enables the flexible and extensible construction of related objects without tying the client code to their concrete structs, thus facilitating the easy replacement of one set of related objects with another. The article outlines the main objectives of the pattern, such as defining interfaces for object creation, encapsulating creation logic, and enabling the easy replacement of object sets. It also weighs the advantages, including code decoupling, reusability, and testability, against potential drawbacks like increased complexity and performance overhead. Practical scenarios for the Abstract pattern's application include payment processing systems, external APIs integration, communication protocol management, and cloud service provider resource handling. The article further provides a detailed guide on implementing the pattern in Golang, complete with code examples for a clothes factory instance and a GUI components factory instance, along with corresponding unit tests to validate the implementations.

Opinions

  • The Abstract pattern is deemed beneficial for systems requiring flexibility and extensibility, particularly in scenarios involving multiple related objects.
  • Introducing interfaces and additional layers of abstraction may increase code complexity, potentially making it more challenging for developers unfamiliar with the pattern.
  • The pattern's ability to enable the easy replacement of one set of related objects with another without modifying client code is highlighted as a significant advantage.
  • While acknowledging the potential performance overhead due to the extra level of abstraction, the article suggests that this is usually negligible.
  • The article emphasizes the importance of careful evaluation before choosing the Abstract design pattern, ensuring it aligns with the system's needs and justifies the added complexity.

Abstract Design Pattern in Golang Two Examples with Unit Tests

In this article, I will explain the Abstract design pattern’s concept, objectives, pros and cons, scenarios and how to implement, and provide two instances and unit tests.

Concept

Abstract pattern is also called abstract factory pattern. The abstract design pattern is a type of creational design pattern that involves defining an abstract base interface that declares common methods for concrete structs. Concrete structs implement this interface, providing their own specific implementations of these methods. The client code works with instances of the interface, allowing for flexibility and extensibility.

Abstract pattern is a new layer of grouping to achieve a bigger and more complex composite object, which is used through its interfaces.

Objectives

The main objectives of the abstract design pattern are to:

  1. Define an interface for creating related objects without specifying their concrete structs.
  2. Encapsulate object creation logic into separate factory structs.
  3. Enable the easy replacement of one set of related objects with another, without modifying client code.

Pros and Cons

Advantages of using the abstract design pattern:

  1. Decouples object creation from the client code.
  2. Improves code reusability and maintainability.
  3. Enables adding new products without changing the client code.
  4. Easier to test, as dependencies can be easily mocked.

Disadvantages of using the abstract design pattern:

  1. Increased complexity: Introducing interfaces and additional layers of abstraction may make the code harder to understand for developers unfamiliar with the pattern.
  2. Potential performance overhead: In some cases, the extra level of abstraction might introduce a performance penalty, although this is usually negligible.

Scenarios

The Abstract Pattern is often used in real-world scenarios where flexibility, extensibility, and maintainability are crucial. Here are a few examples of how the Abstract Pattern can be applied in practice:

1. Payment processing systems

In an e-commerce application, you might need to support various payment gateways (e.g., PayPal, Stripe, Braintree). The Abstract Pattern can be used to define a common interface for processing payments, with each concrete implementation handling a specific payment gateway. This approach makes it easy to add or modify payment gateways without affecting the client code.

2. External APIs

When integrating with multiple external APIs, such as social media platforms (e.g., Facebook, Twitter, LinkedIn), you can use the Abstract Pattern to define a common interface for all API interactions. Each concrete implementation corresponds to a specific platform, allowing the application to easily switch between platforms or support new ones without changing the client code.

3. Communication protocols

An application that supports multiple communication protocols (e.g., HTTP, WebSocket, gRPC) could use the abstract factory pattern to create an interface for sending and receiving messages. Each concrete factory would implement the methods specific to a particular protocol.

4. Cloud service providers

An application that needs to work with multiple cloud service providers (e.g., AWS, Google Cloud, Azure) can use the abstract factory pattern to provide a consistent interface for creating and managing resources, such as virtual machines, storage, or databases. Each concrete factory would correspond to a specific cloud provider, implementing the methods specific to that provider’s APIs.

How to implement

The following are steps on how to implement the abstract pattern in Go.

  1. Define the abstract product interfaces.
  2. Create concrete product implementations.
  3. Define the abstract factory interface.
  4. Create concrete factory implementations.
  5. Use the factory to create related objects without specifying their concrete structs

The following image is how to implement the abstract pattern diagram. Perhaps you think about Golang does not have a class. Correct, Golang does not have classes in the traditional sense like some other object-oriented programming languages such as Java or C++. Instead, Go has types with methods, and it uses interfaces to achieve polymorphism. I use the term “class” as a general term to represent a type that has methods.

First instance

In the following instance, I will implement the clothes instance. The abstract pattern needs a clear code structure, it will be helpful to understand it. In the following instance, there are 6 files in total.

The below code is the sports.go file content. The SportsFactory is an Abstract product interface. The makeShoe() and the makeShirt() are methods that concrete products must implement.

package clothes

import "fmt"

const (
   adidas = "adidas"
   nike   = "nike"
)

type SportsFactory interface {
   makeShoe() IShoe
   makeShirt() IShirt
}

func GetSportsFactory(brand string) (SportsFactory, error) {
   if brand == adidas {
      return &Adidas{}, nil
   }

   if brand == nike {
      return &Nike{}, nil
   }

   return nil, fmt.Errorf("wrong brand type passed")
}

The below code is the adidas.go file content. Adidas is a Concrete product.

package clothes

type Adidas struct {
}

func (a *Adidas) makeShoe() IShoe {
   return &AdidasShoe{
      Shoe: Shoe{
         logo: adidas,
         size: 14,
      },
   }
}

func (a *Adidas) makeShirt() IShirt {
   return &AdidasShirt{
      Shirt: Shirt{
         logo: adidas,
         size: 14,
      },
   }
}

The following code is the nike.go file content. Nike is a Concrete product.

package clothes

type Nike struct {
}

func (n *Nike) makeShoe() IShoe {
   return &NikeShoe{
      Shoe: Shoe{
         logo: nike,
         size: 14,
      },
   }
}

func (n *Nike) makeShirt() IShirt {
   return &NikeShirt{
      Shirt: Shirt{
         logo: nike,
         size: 14,
      },
   }
}

Both Adidas and Nike implement the makeShoe() and the makeShirt() methods.

The following code is the shirt.go file content. IShirt is an abstract factory. Shirt implements all 4 methods in the IShirt. These 4 methods can be used in concrete factories. AdidasShirt and NikeShirt are concrete factories that can use them directly.

package clothes

type IShirt interface {
   setLogo(logo string)
   setSize(size int)
   getLogo() string
   getSize() int
}

type Shirt struct {
   logo string
   size int
}

func (s *Shirt) setLogo(logo string) {
   s.logo = logo
}

func (s *Shirt) getLogo() string {
   return s.logo
}

func (s *Shirt) setSize(size int) {
   s.size = size
}

func (s *Shirt) getSize() int {
   return s.size
}

type AdidasShirt struct {
   Shirt
}

type NikeShirt struct {
   Shirt
}

The below code is the shoe.go file content. It is the same code structure as shirt.go. IShoe is an abstract factory. Shoe implements all 4 methods in the IShoe. These 4 methods can be used in concrete factories. AdidasShoe and NikeShoe are concrete factories that can use them directly.

package clothes

type IShoe interface {
   setLogo(logo string)
   setSize(size int)
   getLogo() string
   getSize() int
}

type Shoe struct {
   logo string
   size int
}

func (s *Shoe) setLogo(logo string) {
   s.logo = logo
}

func (s *Shoe) getLogo() string {
   return s.logo
}

func (s *Shoe) setSize(size int) {
   s.size = size
}

func (s *Shoe) getSize() int {
   return s.size
}

type AdidasShoe struct {
   Shoe
}

type NikeShoe struct {
   Shoe
}

The last following code is the sports_test.go file content.

package clothes

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestAdidasShoe(t *testing.T) {
   adidasFactory, _ := GetSportsFactory(adidas)
   adidasShoe := adidasFactory.makeShoe()
   assert.Equal(t, adidas, adidasShoe.getLogo())
   assert.Equal(t, 14, adidasShoe.getSize())
}

func TestAdidasShirt(t *testing.T) {
   adidasFactory, _ := GetSportsFactory(adidas)
   adidasShirt := adidasFactory.makeShirt()
   assert.Equal(t, adidas, adidasShirt.getLogo())
   assert.Equal(t, 14, adidasShirt.getSize())
}

func TestNikeShoe(t *testing.T) {
   nikeFactory, _ := GetSportsFactory(nike)
   nikeShoe := nikeFactory.makeShoe()
   assert.Equal(t, nike, nikeShoe.getLogo())
   assert.Equal(t, 14, nikeShoe.getSize())
}

func TestNikeShirt(t *testing.T) {
   nikeFactory, _ := GetSportsFactory(nike)
   nikeShirt := nikeFactory.makeShirt()
   assert.Equal(t, nike, nikeShirt.getLogo())
   assert.Equal(t, 14, nikeShirt.getSize())
}

After these codes are finished, let’s run the unit tests. The tests result from the screenshot is below.

Second Instance

In the below instance, I will implement the GUI example.

The following code is the content of the file named gui.go.

package gui

// Step  1: Define the abstract product interfaces

type Button interface {
   Click() string
}

type Checkbox interface {
   Check() string
}

// Step 2: Create concrete product implementations

type WindowsButton struct{}

func (w *WindowsButton) Click() string {
   return "Windows button clicked"
}

type MacOSButton struct{}

func (m *MacOSButton) Click() string {
   return "MacOS button clicked"
}

type WindowsCheckbox struct{}

func (w *WindowsCheckbox) Check() string {
   return "Windows checkbox checked"
}

type MacOSCheckbox struct{}

func (m *MacOSCheckbox) Check() string {
   return "MacOS checkbox checked"
}

// Step 3: Define the abstract factory interface

type FactoryGUI interface {
   CreateButton() Button
   CreateCheckbox() Checkbox
}

// Step 4: Create concrete factory implementations

type WindowsFactory struct{}

func (w *WindowsFactory) CreateButton() Button {
   return &WindowsButton{}
}

func (w *WindowsFactory) CreateCheckbox() Checkbox {
   return &WindowsCheckbox{}
}

type MacOSFactory struct{}

func (m *MacOSFactory) CreateButton() Button {
   return &MacOSButton{}
}

func (m *MacOSFactory) CreateCheckbox() Checkbox {
   return &MacOSCheckbox{}
}

The below code is the content of the file named gui_test.go.

package gui

import (
   "runtime"
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestMacGUI(t *testing.T) {
   var factory FactoryGUI

   // Choose the concrete factory based on the platform or configuration.
   if runtime.GOOS == "darwin" {
      factory = &MacOSFactory{}
   } else {
      factory = &WindowsFactory{}
   }

   button := factory.CreateButton()
   checkbox := factory.CreateCheckbox()

   assert.Equal(t, button.Click(), "MacOS button clicked")
   assert.Equal(t, checkbox.Check(), "MacOS checkbox checked")
}

The test result is the below screenshot. As I am using the Mac Pro, I didn’t test the Windows GUI.

Conclusion

In conclusion, the abstract design pattern is a powerful tool for simplifying the design of complex systems and provides a flexible and modular architecture that makes it easier to add new features or functionality to a system. However, it can be more complex than other design patterns and may introduce some performance overhead. As with any design pattern, it is important to carefully evaluate the pros and cons of using the abstract design pattern and choose the pattern that best fits your system's needs.

Bonus

When you use abstract pattern, you should remember two essential things:

1. The most essential thing is an abstract product interface matches at least one concrete product. 2. The last thing is an abstract factory matches at least one concrete factory.

Go back to Creational Design Patterns click here.

To View Structural Design Patterns in Golang, please click here.

To View Behavioural Design Patterns in Golang, please click here.

To View Concurrency Design Patterns in Golang, please click here.

Design Patterns
Programming
Software Engineering
Golang
Technology
Recommended from ReadMedium