golang, архитектура приложения
У меня есть две сущности. Пользователи и заказы. Я реализовал следующим образом.
type UserRepository interface {
GetUserByID(int) (*models.User, error)
Create(*models.User) error
UpdateBalance(int, int, int) error
}
type OrderRepository interface {
GetOrderByID(int) (*models.Order, error)
CreateOrder(*models.Order) error
}
type Store interface {
User() UserRepository
Order() OrderRepository
}
а структура Store выглядит следующим образом:
type Store struct {
db *sql.DB
userRepo *UserRepository
orderRepo *OrderRepository
}
Теперь логика приложения. Я не могу оформить заказ если баланс у пользователя меньше определенной величины. Это меня толкает создать уже service структуру таким образом:
type Service struct {
store store.Store
}
func NewService(store store.Store) ServicesInterface {
return &Service{
store: store,
}
}
И в нем уже описать всю логику в том числе создание заказа. Получается, что в Store interface я должен добавить Service interface, чтобы все выглядело логично... Но я думаю, что все это никак не соответствует нормальной архитектуре. Как правильно в данном случае построить ?
Ответы (1 шт):
Довольно сложно, сказать как правильно безДа, в данном случае вы действительно можете рассмотреть вариант добавления Service интерфейса в Store интерфейс. Это позволит собрать все функциональности, связанные с бизнес-логикой, в одном месте.
Однако, вам также стоит рассмотреть вариант создания отдельной структуры Service с соответствующим интерфейсом, которая будет использовать Store интерфейс для доступа к данным и реализации бизнес-логики. Такой подход позволит лучше разделить ответственности и сделает вашу структуру более гибкой.
Примерно так:
type Service interface {
CreateOrder(userID int, amount int) error
}
type ServiceStruct struct {
store Store
}
func NewService(store Store) Service {
return &ServiceStruct{store: store}
}
func (s *ServiceStruct) CreateOrder(userID int, amount int) error {
user, err := s.store.User().GetUserByID(userID)
if err != nil {
return err
}
if user.Balance < amount {
return fmt.Errorf("insufficient balance")
}
err = s.store.User().UpdateBalance(userID, -amount, user.Version)
if err != nil {
return err
}
order := &models.Order{
UserID: userID,
Amount: amount,
}
return s.store.Order().CreateOrder(order)
}
Этот подход также позволит лучше тестировать реализацию бизнес-логики, так как вы сможете передавать заглушенные (mock) реализации Store интерфейса в Service структуру.
Например:
type mockStore struct {
userRepo *mockUserRepo
orderRepo *mockOrderRepo
}
func (m *mockStore) User() UserRepository {
return m.userRepo
}
func (m *mockStore) Order() OrderRepository {
return m.orderRepo
}
func TestCreateOrder(t *testing.T) {
store := &mockStore{
userRepo: &mockUserRepo{
users: map[int]*models.User{
1: {ID: 1, Balance: 100},
},
},
orderRepo: &mockOrderRepo{},
}
service := NewService(store)
err := service.CreateOrder(1, 50)
assert.NoError(t, err)
user, err := store.User().GetUserByID(1)
assert.NoError(t, err)
assert.Equal(t, 50, user.Balance)
}
Вы можете также рассмотреть вариант создания отдельной структуры Application, которая будет содержать в себе Store и Service структуры и реализовывать соответствующий интерфейс. Это позволит еще лучше разделить ответственности и упростить тестирование.