Have you ever found yourself solving a problem in a certain way without realizing there’s a well-established design pattern that perfectly fits the bill? That’s precisely what happened to me. I was knee-deep in my code, trying to communicate my business logic seamlessly with the database and other external services like payment gateways and queue systems. Little did I know that the solution I had stumbled upon was the Adapter pattern.
My ‘aha’ moment came while watching a NestJS tutorial. The instructor demonstrated a similar problem and introduced a solution by creating an adapter. What piqued my interest was how they named the adapter class with “Adapter” added at the end. It was like a light bulb moment.
I realized that what I had been doing all along was an established design pattern called the Adapter pattern. Intrigued, I delved into this pattern, and it was the elegant solution I needed to communicate with various external services.
What is the Adapter Pattern?
Before we dive into the practical aspects, let’s take a moment to understand the Adapter pattern in theory, illustrated with some graphics.
The Adapter pattern is a structural design pattern that allows two incompatible interfaces to work together. It acts as a bridge between two different interfaces, making them compatible without changing their source code. This pattern is particularly useful when you have existing classes or systems you want to reuse that don’t fit your current needs.
In the diagram above, you can see how the Adapter pattern works. The Clientinteracts with the ClientInterface. The Adapterclass serves as the bridge between the Client and the Service, translating requests from the Client into a format that the Service can understand and vice versa.
Coding Example with Golang
Now, let’s put theory into practice. I’ll walk you through a coding example in Golang, demonstrating how I use the Adapter pattern in my projects.
Here, you can see a class diagram of my usual way of implementing this pattern without knowing.
Simulating user data
To begin, let’s simulate user data using a simple struct:
package main
type Model struct {
ID uint
Name string
}
type Models []Model
Business logic with the Adapter pattern
This section’ll explore how the Adapter pattern can be applied to our code. Our business logic, represented by the Usecase struct, interacts with the data storage through the DatabasePort interface.
package main
// DatabasePort interface represents the methods our business logic will use.
type DatabasePort interface {
Create(m Model) error
Update(m Model) error
Delete(ID uint) error
GetAll() (Models, error)
GetByID(ID uint) (Model, error)
}
// Usecase struct encapsulates the business logic.
type Usecase struct {
database DatabasePort
}
// NewUsecase constructor of the Usecase
func NewUsecase(database DatabasePort) Usecase {
return Usecase{database: database}
}
// Implementing business logic methods.
func (u Usecase) Create(m Model) error {
return u.database.Create(m)
}
func (u Usecase) Update(m Model) error {
return u.database.Update(m)
}
func (u Usecase) Delete(ID uint) error {
return u.database.Delete(ID)
}
func (u Usecase) GetAll() (Models, error) {
return u.database.GetAll()
}
func (u Usecase) GetByID(ID uint) (Model, error) {
return u.database.GetByID(ID)
}
Implementing the Adapter
Our Adapter, PostgresAdapter, translates database operations into the DatabasePort interface. It serves as the bridge between our business logic and the actual database operations.
package main
import "database/sql"
// PostgresAdapter is an implementation of the DatabasePort interface.
type PostgresAdapter struct {
// db is the instance of the service the Usecase can't directly talk to
db *sql.DB
}
// NewPostgresAdapter initializes a new PostgresAdapter instance.
func NewPostgresAdapter(db *sql.DB) PostgresAdapter {
return PostgresAdapter{db: db}
}
// Implement the methods of the DatabasePort interface.
func (p PostgresAdapter) Create(m Model) error {
stmt, err := p.db.Prepare(`INSERT INTO users (name) VALUES ($1)`)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(m.Name)
return err
}
func (p PostgresAdapter) Update(m Model) error {
stmt, err := p.db.Prepare(`UPDATE users SET name = $1, updated_at = now() WHERE id = $2`)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(m.Name, m.ID)
return err
}
func (p PostgresAdapter) Delete(ID uint) error {
stmt, err := p.db.Prepare(`DELETE FROM users WHERE id = $1`)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(ID)
return err
}
func (p PostgresAdapter) GetAll() (Models, error) {
stmt, err := p.db.Prepare(`SELECT id, name FROM users`)
if err != nil {
return nil, err
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
return nil, err
}
defer rows.Close()
var ms Models
for rows.Next() {
var m Model
if err := rows.Scan(&m.ID, &m.Name); err != nil {
return nil, err
}
ms = append(ms, m)
}
return ms, nil
}
func (p PostgresAdapter) GetByID(ID uint) (Model, error) {
stmt, err := p.db.Prepare(`SELECT id, name FROM users WHERE id = $1`)
if err != nil {
return Model{}, err
}
defer stmt.Close()
rows, err := stmt.Query(ID)
if err != nil {
return Model{}, err
}
defer rows.Close()
var m Model
if err := rows.Scan(&m.ID, &m.Name); err != nil {
return Model{}, err
}
return m, nil
}
Bringing it all together
In this section, we establish the database connection, create the adapter, and use it in our business logic.
package main
import (
"database/sql"
"log"
)
func main() {
// Simulate the database connection.
sqlDB, err := sql.Open("postgres", "postgres://...")
if err != nil {
log.Fatal(err)
}
// Create a PostgresAdapter instance, injecting the database connection.
adapter := NewPostgresAdapter(sqlDB)
// Create the Usecase instance, injecting the adapter.
client := NewUsecase(adapter)
// Use the Usecase methods.
client.Create(Model{Name: "Hernan"})
client.Update(Model{Name: "Hernan Reyes"})
}
With this setup, we’ve demonstrated how the Adapter pattern can seamlessly connect our business logic to external services like a database. It’s worth noting that this pattern isn’t limited to just databases; I’ve applied it to various other services, such as payment gateways.
In the case of a payment gateway integration, the communication process might involve using the HTTP protocol. So, in our adapter, we’d handle requests using this protocol to interact with the chosen payment gateway, adapting its interface to our application’s needs.
Pros and Cons
Pros
Single Responsibility Principle (SRP)
The Adapter pattern excels at adhering to the Single Responsibility Principle. It ensures that each class or component has a single reason to change. By creating adapters to interface with external systems, you isolate the complexity of adapting to their interfaces. This separation of concerns makes your codebase cleaner, easier to understand, and more maintainable.
Open/Closed Principle (OCP)
The Open/Closed Principle, a fundamental concept in software design, emphasizes the importance of extending functionality without modifying existing code. The Adapter pattern aligns well with this principle. You can create a new adapter class without altering the core business logic when you need to introduce a new external system or change an existing one. This keeps your codebase open for extension while closed for modification.
Cons
Abstraction overhead
One of the downsides of the Adapter pattern is the introduction of additional abstraction layers. Adapters act as intermediaries between your code and external systems, which can add complexity and increase the number of classes and interfaces in your application. While this abstraction is beneficial for isolating changes, it may make the codebase appear more complex than it needs to be, especially for simple systems.
Conclusion
Discovering the Adapter pattern was a game-changer for me. It helped me streamline communication between my business logic and various external services, making my code more modular and maintainable. The Adapter pattern’s ability to bridge the gap between incompatible interfaces is a powerful tool in a developer’s toolkit. So, the next time you find yourself solving a problem unconventionally, take a step back and see if the Adapter pattern might be the elegant solution you didn’t know you were using.
Incorporating design patterns into your coding arsenal can save you time, make your code more robust, and ultimately make you a more effective developer. So, keep exploring and learning because you never know what other design patterns might be hiding in your code.
Resources
- Tool used for the Class diagrams
- Learn more about patterns with this book, “Dive into Design Patterns”
- Figma used for the featured image
The Adapter Pattern’s Sneaky Role in My Projects was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.