SoatDev IT Consulting
SoatDev IT Consulting
  • About us
  • Expertise
  • Services
  • How it works
  • Contact Us
  • News
  • October 10, 2023
  • Rss Fetcher
Image by author

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

  1. Tool used for the Class diagrams
  2. Learn more about patterns with this book, “Dive into Design Patterns”
  3. 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.

Previous Post
Next Post

Recent Posts

  • Tesla sends driverless Model Y from factory to customer to promote its robotaxi tech
  • Next-gen procurement platform Levelpath nabs $55M
  • Fintech Bolt progresses its turnaround by landing Klarna as a partner
  • Instagram now lets you share Spotify songs with sound to Stories
  • At TechCrunch All Stage: VC red flags, founder signals, and pre-seed traps — Charles Hudson will tell you what investors really see

Categories

  • Industry News
  • Programming
  • RSS Fetched Articles
  • Uncategorized

Archives

  • June 2025
  • May 2025
  • April 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • July 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023

Tap into the power of Microservices, MVC Architecture, Cloud, Containers, UML, and Scrum methodologies to bolster your project planning, execution, and application development processes.

Solutions

  • IT Consultation
  • Agile Transformation
  • Software Development
  • DevOps & CI/CD

Regions Covered

  • Montreal
  • New York
  • Paris
  • Mauritius
  • Abidjan
  • Dakar

Subscribe to Newsletter

Join our monthly newsletter subscribers to get the latest news and insights.

© Copyright 2023. All Rights Reserved by Soatdev IT Consulting Inc.