Чистая архитектура на Go
Не могу разобраться как внедрять зависимости.
Например у меня есть сущность:
type Car struct {
ID int
Model string
Price int
}
func NewCar(model string, price int) *Car {
return &Car{
Model: model,
Price: price,
}
}
Для взаимодействия с БД, я сделал следующий интерфейс:
type Storer interface {
Create(*model.Car) error
GetByID(int) (*model.Car, error)
}
type Store struct {
db *sql.DB
}
func NewStore(db *sql.DB) Storer {
return &Store{
db: db,
}
}
func Connectdb() (*sql.DB, error) {
db, err := sql.Open("mysql", "root:@/go_test")
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
func (s *Store) Create(c *model.Car) error {
s.db.QueryRow("INSERT INTO car (model, price) VALUES(?, ?)", c.Model, c.Price).Scan(&c.Model, &c.Price)
return nil
}
Правильно ли я понимаю, что в конструкторе нужно вернуть именно интерфейс. И потом в структуре где я собираюсь использовать передать этот объект:
type services struct {
store db.Storer
}
type Service interface {
Delivery(int) (float64, error)
CounteryPayment()
}
func NewService(s *db.Storer) Service {
return &services{
store: *s,
}
}
func (s *services) Delivery(id int) (float64, error) {
u, err := s.store.GetByID(id)
if err != nil {
return 0, err
}
delivery := u.Price - 250
return float64(delivery), nil
}
func (s *services) CounteryPayment() {
return
}
И так далее. Передача объекта интерфейса Service в следующую структуру ?
Ответы (1 шт):
У вас есть два компонента: сервис и хранилище. Сервис обеспечивает логику, хранилище - взаимодействие с БД. Компоненты находятся в разных пакетах: package service и package storage. В микросервисной архитектуре зачастую достаточно прямого встраивания одних компонентов в другие.
package storage
type Storage struct{}
package service
import "storage"
type Service struct {
storage *storage.Storage
}
func NewService(storage *storage.Storage) *Service {
return &Service{storage: storage}
}
package main
import (
"storage"
"service"
)
...
var db *sql.DB // опустим как мы получили соединение с базой
stor := storage.NewStorage(db)
svc := service.NewService(stor)
Красиво, изящно, просто. Если вы будите добавлять методы для хранилища или менять их сигнатуры, вам не придется каждый раз переписывать интерфейс.
По импорту видно, что основной компонент (сервис) зависит от хранилища и привязан к конкретной реализации, что не очень хорошо.
Давайте уменьшим связность и будем ближе к "чистой архитектуре". Определим интерфейс Storage прямо в пакете service.
package service
type Storage interface {
Create(*model.Car) error
GetByID(int) (*model.Car, error)
}
type Service struct {
storage Storage
}
func NewService(storage Storage) *Service {
return &Service{storage: storage}
}
Теперь пакеты не зависят друг от друга, а в конструктор сервиса (правильнее фабричную функцию, factory function) можно передавать любой объект, реализующий интерфейс Storage. C точки зрения хранилища вообще ничего не поменялось. Хотя мы можем в пакете storage сделать небольшое дополнение и убедиться, что наше хранилище реализует этот интерфейс:
package storage
import (
"service"
)
...
var _ service.Storage = (*Storage)(nil) // статическая проверка
Обратите внимание, что направление зависимости поменялось. Конкретное хранилище зависит от сервиса, но сам сервис не зависит от реализации хранилища, а зависит только от собственного интерфейса. Это и есть последняя буква из акронима SOLID (принципа инверсии зависимостей от Роберта Мартина).
Что касается фабричных функций, то в общем случае они должны возвращать именно тот тип, который в них и создается:
package storage
...
func NewStorage(db *sql.DB) *Storage {
return &Storage{db: db}
}
Утиная типизация позволит интерпретировать его как нужный интерфейс (в случае хранилища это service.Storage, что собственно мы и видим на примере статической проверки).