The Repository Pattern is a commonly used pattern in software architecture that abstracts the data access logic for an application. It promotes a cleaner separation of concerns, making the application more maintainable, scalable, and testable.
When applied to Go (or "Golang"), the Repository Pattern provides a straightforward way to abstract the access to your data source, which could be a database, a web service, or even a file system. Here's a brief overview of how you can implement it:
1. Define your domain model:
// models/user.go
package models
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
2. Define the Repository interface:
This interface will outline the methods that your repository must implement.
// repository/user_repository.go
package repository
import "your_package_path/models"
type UserRepository interface {
GetAll() ([]models.User, error)
GetByID(id int) (models.User, error)
Save(user models.User) error
Delete(id int) error
}
3. Implement the Repository:
For this example, let's assume we're using a simple in-memory store.
// repository/memory/user_repository_impl.go
package memory
import (
"your_package_path/models"
"your_package_path/repository"
)
type UserRepositoryMemory struct {
users []models.User
}
func NewUserRepositoryMemory() repository.UserRepository {
return &UserRepositoryMemory{
users: []models.User{},
}
}
func (r *UserRepositoryMemory) GetAll() ([]models.User, error) {
return r.users, nil
}
func (r *UserRepositoryMemory) GetByID(id int) (models.User, error) {
for _, user := range r.users {
if user.ID == id {
return user, nil
}
}
return models.User{}, errors.New("User not found")
}
func (r *UserRepositoryMemory) Save(user models.User) error {
r.users = append(r.users, user)
return nil
}
func (r *UserRepositoryMemory) Delete(id int) error {
for index, user := range r.users {
if user.ID == id {
r.users = append(r.users[:index], r.users[index+1:]...)
return nil
}
}
return errors.New("User not found")
}
4. Use the Repository in your services or application logic:
package main
import (
"your_package_path/models"
"your_package_path/repository"
"your_package_path/repository/memory"
)
func main() {
// Using the in-memory implementation
repo := memory.NewUserRepositoryMemory()
user := models.User{
ID: 1,
Name: "John Doe",
}
repo.Save(user)
users, _ := repo.GetAll()
for _, user := range users {
fmt.Println(user.Name)
}
}
With this pattern in place, you can easily swap out your in-memory implementation for, say, a PostgreSQL or a MongoDB implementation without changing your main application logic.
Top comments (4)
Could You show us about how to implements this pattern changing between InMemory and InDatabase without instance directly the UserRepositoryMemory? Thanks and great post
It's not quite a clean implementation as the imports and code in 4 would have to change. You'd need to make it a bit more generic and alias the import so you could do
repo := repository.NewRepository()
. The idea is you can make lots of 3's which implement different backends.This smells a lot like MVC without the V :)
You're right; the solution I provided does still tie the main application code to the specific repository implementations, albeit indirectly. Your idea of further abstracting the repository instantiation is a valid one and will provide even greater flexibility.
We can do something like this.
File
repository/factory.go
Then,
Usage
One common way to switch between different data store implementations without directly instantiating the specific repositories is to use Dependency Injection (DI) or to use factory functions.
Here's how you can implement this:
1. Implement the Database Repository
Let's first add a mock UserRepositoryDatabase to demonstrate:
Create a file
repository/database/user_repository_impl.go
2. Create a Factory Function
This factory will decide which repository to return based on some condition (e.g., a configuration setting).
Create a file
/repository/factory.go
3. Use the Factory in your main application:
This way, you can easily switch between the memory and database implementations just by changing a configuration setting:
By doing this, your main application is abstracted from the actual repository implementation. You can now easily switch between different data storage mechanisms (in-memory, databases, etc.) with minimal changes to the core application logic.