Задать вопрос
@Artem0071
Безработный mr. Junior

Правильно ли делать сервис менеджер в Go?

Никогда до этого не работал с языками, в которых запрещен импорт пакета из загружаемого пакета (import cycle not allowed)

У меня есть несколько сервисов:
1) UserService
2) ProjectService

В UserService может использоваться логика ProjectService'a. Для этого я передаю в userService.New(projectService ProjectService)

Но и в ProjectService может использоваться логика UserService'a.

Как я придумал:
1) Не принимать именно сервис, а только его интерфейс
2) Сделать "менеджер" сервисов

Что вышло:

app
- interfaces
-- UserServiceInterface
-- ProjectServiceInterface
- services
-- ServiceManager
-- userService
--- service
-- projectService
--- service

Каждый сервис принимает в себя ServiceManager'a и уже может обращаться через него к другим сервисам. Так же, в сам ServiceManager передается БД, логгер и прочее, что нужно во всех сервисах, поэтому доступ к БД и прочему будет из всех мест

Что в ServiceManager:
type ServiceManager struct {
        logger Logger
        database Database
        ...

	projectService ProjectServiceInterface
	userService UserServiceInterface
}

func (sm *ServiceManager) ProjectService() ProjectServiceInterface {
	if sm.projectService == nil {
		sm.projectService = projectService.New(sm)
	}
	return sm.projectService
}

func (sm *ServiceManager) UserService() UserServiceInterface {
	if sm.userService == nil {
		sm.userService = userService.New(sm)
	}
	return sm.userService
}


Пример использования:
type userService struct {
 sm ServiceManager
}

func New(sm ServiceManager) UserServiceInterface
{
 return &userService{sm: sm}
}

func (s *UserService) DoSmth (arg1, arg2) {
 arg3 = arg1 * arg2 // просто для примера
 s.sm.ProjectService().DoSmthElse(arg3)
}


На сколько это вообще правильно?
  • Вопрос задан
  • 164 просмотра
Подписаться 1 Средний Комментировать
Пригласить эксперта
Ответы на вопрос 2
EvgenyMamonov
@EvgenyMamonov Куратор тега Go
Senior software developer, system architect
> В UserService может использоваться логика ProjectService'a. Для этого я передаю в userService.New(projectService ProjectService)

По хорошему в UserService не должно быть логики ProjectService.
Логику лучше не разносить по разным местам, такой код очень трудно потом поддерживать.
Представьте, что через год придёт новый человек и внесёт правки в ProjectService, он может не знать/забыть внести нужные правки в UserService.

Если без этого совсем никак, тогда лучше сделать 3й сервис, который в конструкторе примет сервисы Users и Project и будет вызывать нужные методы из каждого из них.

В идеале нужно перестроиться и проектировать пакеты так, чтобы таких проблем не возникало.
Но когда вы перестроитесь - результаты работы будут намного качественнее.
Мне после Perl, Python понадобилось не мало времени чтобы перестроиться :))
Но сейчас могу с уверенностью сказать оно того точно стоило!

Подход как у вас с ServiceManager тоже используется, даже видел библиотеки готовые, но уже не помню как назывались. Я попробовал их, у меня не прижились эти библиотеки )

Сейчас я использую такой подход:
users/user.go
тут описываю структуру Users, простая валидация полей, методы у структуры типа GetFullName и т.д.

users/repo.go
тут описываю интерфейсы:
QueryRepo для извлечения данных
CommandRepo для внесения изменения в данные пользователей
репозитории используются только в рамках пакета users, другие пакеты используют интерфейсы сервисов

users/querysvc.go
тут описываю интерфейс QuerySvc (только для извлечения данных связанных с пользователей),
он уже использует интерфейс QueryRepo
этот пакет будут импортировать другие пакеты

users/commandsvc.go
тут описываю интерфейс CommandSvc (только для внесения изменений в данные пользователей), он тоже использует интерфейсы QueryRepo, CommandRepo
этот пакет будут импортировать другие пакеты

users/signupsvc.go
это сервис регистрации пользователей, тут описываю интерфейс SignupSvc

users/repos/mysql/query.go
тут реализация интерфейса QueryRepo на MySQL
этот пакет будет импортироваться только в пакете `app` (см. ниже)

users/repos/mysql/command.go
тут реализация интерфейса CommandRepo на MySQL
этот пакет будет импортироваться только в пакете `app` (см. ниже)

users/services/querysvc.go
тут реализация интерфейса QuerySvc

users/services/commandsvc.go
тут реализация интерфейса CommandSvc

users/services/signupsvc.go
тут реализация интерфейса SignupSvc

core/
тут все константы, которые могут быть использованы в разных пакетах приложения (например коды ошибок)
этот пакет могут включать любые пакеты, но он ничего не импортирует в рамках проекта

app/
тут иницализация всех репозиториев, сервисов, конфигов и т.д.
здесь же происходит связывание всех пакетов.
Т.е. что-то вроде
usersQuerySvc := usersservices.NewQuerySvc(usersRepo, ...)
usersSignupSvc := usersservices.NewSignupSvc(usersQuerySvc, usersCommandSvc)
// например сервис для админов (с проверкой полномочий), методы которого уже можно использовать в endpoint'ax
usersAdminSvc := usersservices.NewAdminSvc(usersQuerySvc, usersCommandSvc)

projectSvc := projectservices.New(usersQuerySvc)

Мне такой подход очень нравится тем, что нет скрытых зависимостей, всё видно как на ладони, кто и что использует.
А если возникают сложности с применением такой схемы - для меня это верный признак того, что нужно спроектировать иначе, раз сложности уже возникли, значит в будущем их будет еще больше ))

Ну и все пакеты как параметры принимают везде только интерфейсы.
Хотя... с точки зрения Dependency Inversion, правильно бы было описывать под каждый пакет свой интерфейс, а не так, как я :), но я делаю интерфейсы очень маленькими, тогда получается очень гибко и, как мне кажется, после этого еще внутри других пакетов описывать интерфейсы уже избыточно в моём случае, работы больше, а толку не так много, по этому пока экономлю на этом время :)

cmd/serve.go - импортирует app и запускает веб сервер например

Если нужны будут еще какие то подробности - пишите, буду рад помочь
Ответ написан
Комментировать
@quiex
Конечно, же первый вариант будет лучше(так, обычно, все и делают). Инжектите интерфейс, определение храните в отдельном пакете (domain, system, helper etc), а имплементацию в service.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы