Adapter Design Pattern in Go!
5 May, 2024
1
1
0
Contributors
Note: Throughout this blog post, we use Go idiomatic terminology to describe concepts and code examples. This ensures consistency with Go's conventions and helps maintain readability for Go developers.
What is the Adapter Design Pattern?
- The Adapter design pattern in Go is a structural pattern that allows incompatible interfaces to work together seamlessly
- Acting as a bridge, the Adapter enables these interfaces to work together without requiring modifications to their original code
- Adapter joins these unrelated interfaces
- This pattern is also commonly referred to as the Wrapper pattern
Why use the Adapter Design Pattern in Go?
- It enables seamless addition of new features onto existing code without altering its core functionality
- Benefits:
- Integration of Third-Party Libraries: Go often interfaces with external libraries or systems, which may have different interfaces than what your application expects. Adapters help integrate these external components without rewriting or modifying their code.
- Code Reusability: Adapters promote code reusability by allowing existing components to be used in new contexts. Instead of creating new implementations, you can adapt existing ones to fit your needs.
- Maintainability: Adapters encapsulate the complexity of adapting interfaces, making your codebase easier to maintain and understand. They also facilitate future changes by isolating modifications to the adapter code.
Key Participants in Adapter Design Pattern Implementation
- Target: This defines the interface that the client code expects to interact with. It's the desired interface that the client wants to use.
- Adaptee: This represents the existing functionality or struct that needs to be adapted to fit the target interface. It's the component we want to use but can't directly because of interface incompatibility.
- Adapter: This is the intermediary component that adapts the Adaptee to fit the Target interface. It implements the Target interface and internally uses the Adaptee to perform the required operations.
- Client: This is the component that interacts with the Target interface. It uses struct values that conform to the Target interface, unaware of whether they are using the adapter or the adaptee behind the scenes.
- Concrete Prototype: This typically refers to a specific implementation of a component or functionality that already conforms to the target interface, so there's no need for an adapter to bridge the gap between interfaces. It can be directly used by the client without any modifications.
How: Real World Example!
Lets consider how the adapter pattern could be implemented for a real world example International Travel Adapter
Scenario: You're going on a trip around the world and need to charge your electronic devices. Different countries have different types of power outlets (existing setup), and your devices' plugs don't fit them all.
Adapter Analogy: You use a universal travel adapter (adapter) that comes with multiple plug types and can adapt to fit different power outlets worldwide, allowing you to charge your devices wherever you go.
Lets define the participants based on above example:
- Target:
- Definition: The interface that represents the expected functionality for charging electronic devices
- Example:
UniversalTravelAdapter
interface with a method likePlugInfo()
- Adaptee:
- Definition: The existing functionality or component representing power outlets from different countries, each with its unique plug type, requiring adaptation to achieve compatibility with the Target interface for universal charging.
- Example: Various types of power outlets with different plug shapes found in different countries, such as European, American, British, etc. These power outlets represent the existing functionality that needs to be adapted to fit a universal charging interface.
- Adapter:
- Definition: The universal travel adapter that adapts the existing power outlets to fit the Target interface, enabling charging compatibility for electronic devices worldwide.
- Example: A UniversalTravelAdapter struct with methods to plug in different plug types, adapting to diverse power outlet interfaces globally, thus enabling universal charging for electronic devices.
- Client:
- Definition: The user or traveller who needs to charge their electronic devices during the trip.
- Example: You, the traveller, who utilizes the ChargeDevice method of the UniversalTravelAdapter struct to charge your electronic devices seamlessly during your global travels.
- Concrete Prototype:
- Definition: A concrete prototype could be a power outlet type that already matches the expected interface for charging. Since it aligns with the target interface, it doesn't require adaptation and can be used directly by the client code.
- Example: Power outlet commonly found in certain regions or countries.
Lets first create the program of UniversalTravelAdapter that works with Type A - Canada, United States, Japan, and Mexico
power outlet which can be considered as a common one. So this would be the concrete prototype that is working as expected with any adaptations.
package main
import "fmt"
// The Adapter design pattern in Go is a structural pattern that allows incompatible interfaces to work together seamlessly
// Plug type by countries for reference
// Type A - Canada, United States, Japan, and Mexico
// Type B - Canada, United States, and Mexico
// Type C - widely used throughout Asia, Europe, and South America
// etc...
// Traget interface
type universalTravelAdapter interface {
plugInfo()
}
// Concrete prototype implementation
type typeA struct {
plugTypeCountries string
}
func (plug *typeA) plugInfo() {
fmt.Printf("Plug type is availble in countries: %s\n", plug.plugTypeCountries)
}
// Client
type client struct{}
func (c *client) plugInfo(u universalTravelAdapter) {
u.plugInfo()
}
// Main
func main() {
fmt.Println("Adapter-Design-Pattern")
// Using the power outlet directly without using client for verifying
powerOutlet := &typeA{plugTypeCountries: "Canada, United States, Japan, and Mexico"}
powerOutlet.plugInfo()
// Using power outlet via client
client := &client{}
client.plugInfo(powerOutlet)
}
Output:
Adapter-Design-Pattern
Plug type is availble in countries: Canada, United States, Japan, and Mexico
Plug type is availble in countries: Canada, United States, Japan, and Mexico
Now lets adapt the UniversalTravelAdapter to charge via Type B
power outlet. For this we could either rewrite everything so that we have both Type A
and Type B
supported or else we can introduce an adapter for Type B
in between using the adapter design pattern, and keep the existing code and functionality as it is. So let see how the code changes so that we can include Type B
extended functionality.
package main
import "fmt"
// The Adapter design pattern in Go is a structural pattern that allows incompatible interfaces to work together seamlessly
// Plug type by countries for reference
// Type A - Canada, United States, Japan, and Mexico
// Type B - Canada, United States, and Mexico
// Type C - widely used throughout Asia, Europe, and South America
// etc...
// Target interface
type universalTravelAdapter interface {
plugInfo()
}
// Concrete prototype implementation
type typeA struct {
plugTypeCountries string
}
func (plug *typeA) plugInfo() {
fmt.Printf("Plug type is availble in countries: %s\n", plug.plugTypeCountries)
}
// Client
type client struct{}
func (c *client) plugInfo(u universalTravelAdapter) {
u.plugInfo()
}
// Adaptee which does not follow Target interface
type typeB struct {
plugTypeCountries string
voltage float64
}
func (plug *typeB) plugInfoTypeB() {
fmt.Printf("Plug type is availble in countries: %s\n", plug.plugTypeCountries)
}
func (plug *typeB) getVolatge() float64 {
return plug.voltage
}
// Adpater
type universalTravelAdapterAdapter struct {
plug typeB
}
func (u *universalTravelAdapterAdapter) plugInfo() {
u.plug.plugInfoTypeB()
}
// Main
func main() {
fmt.Println("Adapter-Design-Pattern")
// Using the power outlet directly without using client for verifying
powerOutlet := &typeA{plugTypeCountries: "Canada, United States, Japan, and Mexico"}
powerOutlet.plugInfo()
// Using power outlet via client
client := &client{}
client.plugInfo(powerOutlet)
// Use plug type 2, extended requirement
typeBpowerOutlet := typeB{plugTypeCountries: "Canada, United States, and Mexico"}
// Using type 2 typeBpowerOutlet directly would throw error, since plugInfo is not implemented by typeB!
// client.plugInfo(typeBpowerOutlet)
// So we can use the Adapter here
adapter := &universalTravelAdapterAdapter{plug: typeBpowerOutlet}
client.plugInfo(adapter)
}
Output:
Adapter-Design-Pattern
Plug type is availble in countries: Canada, United States, Japan, and Mexico
Plug type is availble in countries: Canada, United States, Japan, and Mexico
Plug type is availble in countries: Canada, United States, and Mexico
Conclusion:
As we wrap up our discussion on the Adapter design pattern in Go, it becomes clear that this pattern serves as a vital link between mismatched interfaces, simplifying integration without altering original code. Adopting the Adapter pattern not only enhances code reusability, maintainability, and extensibility but also provides a seamless solution for handling interface differences in our software projects.
Links:
For code example, please refer my Github repository: Design-Patterns-In-Go