Skip to content

Instantly share code, notes, and snippets.

@rtbe
Last active December 17, 2020 15:50
Show Gist options
  • Save rtbe/3705b5b3b9dcd0fb34a276d09a5cd93c to your computer and use it in GitHub Desktop.
Save rtbe/3705b5b3b9dcd0fb34a276d09a5cd93c to your computer and use it in GitHub Desktop.
Паттерн репозиторий

Паттерн-репозиторий (repository pattern)

WHY?

Смешивание бизнес логики и логики хранения данных в приложении приводит к тому, что код становится запутанным (spaghetti code), в него тяжело вносить изменения, так как они могут иметь непредсказуемый характер. Код такого приложения можно описать словами "ригидный" и "хрупкий" (code that intolerant to changes) - изменение одного участка кода зачастую приводит к необходимости вносить изменения в другую часть, что может быть не всегда очевидно изначально. Как результат такой код значительно усложняет и замедляет разработку приложения и приводит к тому, что разработчику тяжело дать точную оценку времени необходимого на реализацию задачи.

HOW?

Абстрагирование слоя отвечающего за работу с базой данных в приложении с помощью использования специального интерфейса. Который определит, то как с ним будет взаимодействовать слой описывающий бизнес логику приложения при этом не вдаваясь в детали реализации, связанные с тем как это будет реализовано в конкретной базе данных.

В данном приложении нам необходимо обеспечить возможность осуществлять выполнение базовых операциий с пользователем: - Получить пользователя из базы данных. - Создать пользователя в базе данных - Удалить пользователя из базы данных. - Получить всех пользователей в базе данных.

package user

type Repository interface {
	Get(string) (*entity.User, error)
	Create(*entity.User) (*entity.User, error)
	Delete(string) error
	List() ([]entity.User, error)
}

Данный интерфейс определяется в пакете user, что помогает с его идентификацией.

Пример реализации данного интерфейса с использованием PostgreSQL

package repo

import (
	"errors"
	"fmt"

	"example.com/clean-rest-api/entity"
	"github.com/jmoiron/sqlx"
)

//UserPGRepo is an abstraction layer that manages user entities inside PostgreSQL DB
type UserPGRepo struct {
	db *sqlx.DB
}

//NewUserPGRepo creates new PostgreSQL repository for User entity
func NewUserPGRepo(db *sqlx.DB) *UserPGRepo {
	return &UserPGRepo{
		db: db,
	}
}

//Get gets user from PostgreSQL DB
func (r *UserPGRepo) Get(id string) (*entity.User, error) {
	var u entity.User
	err := r.db.Get(&u, `SELECT FIRST_NAME,LAST_NAME,EMAIL,DATE_CREATED,DATE_UPDATED FROM USERS WHERE USER_ID=$1`, id)
	return &u, err
}

//Create creates new user in PostgreSQL DB
func (r *UserPGRepo) Create(u *entity.User) (*entity.User, error) {
	_, err := r.db.NamedExec(`
		INSERT INTO USERS (USER_ID, FIRST_NAME, LAST_NAME, PASSWORD_HASH, EMAIL, DATE_CREATED, DATE_UPDATED) 
		VALUES(:userID,:firstName,:lastName,:password,:email,:dateCreated,:dateUpdated)`,
		map[string]interface{}{
			"userID":      u.UserID,
			"firstName":   u.FirstName,
			"lastName":    u.LastName,
			"password":    u.PasswordHash,
			"email":       u.Email,
			"role":        u.Role,
			"dateCreated": u.DateCreated,
			"dateUpdated": u.DateCreated,
		})
	if err != nil {
		return u, err
	}
	return u, nil
}

//Delete deletes user from PostgreSQL DB
func (r *UserPGRepo) Delete(id string) error {
	res, err := r.db.Exec(`DELETE FROM USERS WHERE USER_ID=$1`, id)
	if err != nil {
		return err
	}
	if i, _ := res.RowsAffected(); i == 0 {
		return fmt.Errorf("User with id:%v was not found", id)
	}
	return nil
}

//List lists all users from PostgreSQL DB
func (r *UserPGRepo) List() ([]entity.User, error) {
	users := make([]entity.User, 0)

	rows, err := r.db.Queryx(`
		SELECT FIRST_NAME, LAST_NAME, EMAIL, DATE_CREATED, DATE_UPDATED FROM USERS
	`)
	if err != nil {
		return nil, err
	}

	for rows.Next() {
		var user entity.User
		err := rows.StructScan(&user)
		if err != nil {
			return nil, err
		}
		users = append(users, user)
	}

	if len(users) == 0 {
		return nil, errors.New("There are no users")
	}

	return users, nil
}

Тот же самый функционал реализованный с помощью простого хранения в map.

package repo

import (
	"errors"
	"fmt"
	"sync"

	"example.com/clean-rest-api/entity"
)

//UserInMemRepo is an abstraction layer that manages user entities inside basic in-memory store (map+RWMutex)
type UserInMemRepo struct {
	store map[string]entity.User
	sync.RWMutex
}

//NewUserInMemRepo creates new in-memory repository for User entity
func NewUserInMemRepo() *UserInMemRepo {
	m := make(map[string]entity.User)
	return &UserInMemRepo{
		store: m,
	}
}

//Get gets user from in-memory store
func (r *UserInMemRepo) Get(id string) (*entity.User, error) {
	u, err := r.store[id]
	if !err {
		return nil, errors.New("There are no user with this id")
	}
	return &u, nil
}

//Create creates new user in in-memory store
func (r *UserInMemRepo) Create(u *entity.User) (*entity.User, error) {
	r.Lock()
	defer r.Unlock()

	r.store[u.UserID] = *u
	return u, nil
}

//Delete deletes user from in-memory store
func (r *UserInMemRepo) Delete(id string) error {
	r.Lock()
	defer r.Unlock()

	_, err := r.store[id]
	if !err {
		return fmt.Errorf("User with id:%v was not found", id)
	}
	delete(r.store, id)
	return nil
}

//List lists all users from in-memory store
func (r *UserInMemRepo) List() ([]entity.User, error) {
	users := make([]entity.User, 0)
	for _, user := range r.store {
		users = append(users, user)
	}
	if len(users) == 0 {
		return nil, errors.New("There are no users")
	}
	return users, nil
}

Таким образом слой-пользователь работающий с хранением данных в приложении не зависит от деталей реализации хранения данных.

    package main

	//In-memory userRepo implementation
	//userRepo := repo.NewUserInMemRepo()

	userRepo := repo.NewUserPGRepo(postgresDB)

	userService := user.NewUserService(userRepo)

Сигнатура функции NewUserService представлена ниже

user.NewUserService(r user.Repository) *user.Service

Поскольку функция NewUserService, отвечающая за реализацию бизнес логики не видит отличий между двумя реализациями хранения данных(используя PostgreSQL или используя map), мы можем легко заменить одну реализацию другой.

Relation to SOLID principles

Таким образом, данный архитектурный паттерн обеспечивает исполнение приниципов SOLID:

  1. Single responsibility principle.

"A class should have one, and only one, reason to change." –Robert C Martin

Паттерн-репозиторий реализует принцип единственной с помощью выделения отдельных слоев в архитектуре приложения отвечающих за бизнес логику и хранение данных. Таким образом, если мы хотим внести изменения в то как хранятся данные в приложении нам не надо вносить соответствующие изменения в слой отвечающий за бизнес логику приложения и наоборот. Другими словами происходит разделение ответственностей между слоями приложения (separation of concerns), что приводит к их развязыванию (decoupling).

  1. Open/closed principle.

"Software entities should be open for extension, but closed for modification." –Bertrand Meyer, Object-Oriented Software Construction

Паттерн-репозиторий соответствует принципу открытости для добавления/закрытости для модификации. Теперь мы всегда можем добавить новую реализацию БД, просто удовлетворив требованиям интерфейса. Так запись в различные базы данных (разных видов - postgreSQL/MongoDB или mySQL/Redis) для слоя использующего интерфейс репозитория будет выглядеть абсолютно одинакого. Данный слой просто будет использовать метод write у интерфейсного типа (который будет отличаться у каждой конкретной имплементации данного интерфейса). Как результат разработчик может расширить/внести изменения в то как хранятся данные в приложении не меняя уже описанную бизнес логику приложения.

  1. Liskov Substitution Principle.

"Coined by Barbara Liskov, the Liskov substitution principle states, roughly, that two types are substitutable if they exhibit behaviour such that the caller is unable to tell the difference." -Dave Cheney, SOLID Go Design

Паттерн-репозиторий реализует данный принцип. Так для слоя использующего интерфейс репозитория, не важен конкретный тип интерфейса. Важно лишь что конкретный тип удовлетворяет договоренностям описанным в интерфейсе репозитория, о которых знает и которые использует слой-пользователь. Как результат мы можем заменить одну реализацию базы данных другой (даже поменять её вид), и слой-пользователь не заметит разницы (Есть пример с инмемори БД и постгре описанный в мейн)

  1. Interface segregation principle.

"Clients should not be forced to depend on methods they do not use." –Robert C. Martin

Паттерн-репозиторий соответствует данному принципу. Так интерфейс репозитория абстрагирующий работу с базой данных включает в себя только те методы, которые необходимы для выполнения этой функции. Слой-пользователь интерфеса репозитория не зависит от методов, которые он не будет использовать для хранения данных.

  1. Dependency inversion.

"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." –Robert C. Martin

Паттерн-репозиторий разворачивает зависимости между слоями приложения таким образом, что высокоуровневые бизнес правила не зависят от низкоуровневых деталей работы с базами данных, оба слоя зависят от абстракции - интерфейса репозитория. Но конкретная реализация базы данных, описывающая детали, для того чтобы работать, должна соответствовать интерфейсу репозитория. Получается, что низкоуровневый слой знает и зависит от более высокоуровневой абстракции описанной с помощью интерфейса. Другими словами теперь слой описывающий бизнес-логику приложения (высокоуровневый) и слой реализующий конкретный функционал конкретной базы данных (низкоуровневый) не зависят друг от друга, а зависят от абстракции (интерфейса репозитория). Но при этом сама абстракция не зависит от низкоуровнего слоя - низкоуровневый слой зависит от абстракции, т.к он обязан ей соответствовать.

Result

Обеспечение независимости (decoupling) бизнес логики приложения от низкоуровневых деталей работы связанных с хранением данных в приложении. Что приводит как к повышению понятности/гибкости/предсказуемости кода, что упрощает и ускоряет процесс внесения изменений. Так и обеспечивает независимость приложения от конкретных решений связанных с реализацией хранения данных - позволяет в процессе разработки отложить решение о использовании конкретных технологий для хранения данных и сосредоточиться на соответствии бизнес логике (Domain-driven design).

"The important decisions that a Software Architect makes are the ones that allow you to NOT make the decisions about the database, and the webserver, and the frameworks." -Robert C. Martin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment