A
A
Artem00712021-06-07 13:21:25
go
Artem0071, 2021-06-07 13:21:25

Is it right to do a service manager in Go?

I've never worked before with languages ​​that prohibit importing a package from a downloaded package (import cycle not allowed)

I have several services:
1) UserService
2) ProjectService

UserService can use ProjectService'a logic. To do this, I pass in userService.New (projectService ProjectService)

But the logic of UserService'a can also be used in ProjectService.

As I came up with:
1) Do not accept exactly the service, but only its interface
2) Make a "manager" of services

What happened:

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

Each service accepts a ServiceManager and can already access other services through it. Also, the database, logger and other things that are needed in all services are transferred to the ServiceManager itself, so access to the database and other things will be from all places

. What is in the 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
}


Usage example:
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)
}


To what extent is this correct?

Answer the question

In order to leave comments, you need to log in

2 answer(s)
E
Evgeny Mamonov, 2021-06-07
@EvgenyMamonov

> In UserService logic ProjectService'a can be used. To do this, I pass in userService.New (projectService ProjectService)
For good, there should be no ProjectService logic in UserService.
It is better not to spread the logic to different places, such code is very difficult to maintain later.
Imagine that in a year a new person will come and make changes to the ProjectService, he may not know / forget to make the necessary changes to the UserService.
If there is absolutely no way without this, then it is better to make the 3rd service, which in the constructor will accept the Users and Project services and will call the necessary methods from each of them.
Ideally, one should rebuild and design packages in such a way that such problems do not arise.
But when you rebuild - the results of the work will be much better.
After Perl, Python, it took me a lot of time to rebuild :))
But now I can say with confidence that it was definitely worth it!
The approach you have with ServiceManager is also used, I even saw ready-made libraries, but I don’t remember what they were called. I tried them, these libraries did not take root for me)
Now I use this approach:
users/user.go
here I describe the Users structure, simple field validation, methods of the GetFullName type structure, etc.
users/repo.go I
describe the interfaces here:
QueryRepo for extracting data
CommandRepo for making changes to user data
repositories are used only within the users package, other packages use the
users/querysvc.go service interfaces
here I describe the QuerySvc interface (only for extracting data related to users),
it already uses the QueryRepo interface,
this package will import other packages
users/commandsvc.go
here I describe the CommandSvc interface (only for making changes to user data), it also uses the QueryRepo interfaces, CommandRepo
this package will be imported by other packages
users/signupsvc.go
this is a user registration service, here I describe the interface SignupSvc
users/repos/mysql/query.go
here the implementation of the QueryRepo interface on MySQL
this package will be imported only in the `app` package (see below )
users/repos/mysql/command.go
here is the implementation of the CommandRepo interface on MySQL
this package will only be imported in the `app` package (see below)
users/services/querysvc.go QuerySvc
interface implementation here
users/services/commandsvc.go
CommandSvc interface implementation here
users/services/signupsvc.go SignupSvc
interface implementation here
core/
here are all the constants that can be used in different application packages (for example, error codes)
this package can include any packages, but it does not import anything within the
app/
project here is the initialization of all repositories, services, configs, etc.
this is where the binding of all packages takes place.
Those. something like

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

projectSvc := projectservices.New(usersQuerySvc)

I really like this approach because there are no hidden dependencies, everything is visible at a glance, who uses what.
And if there are difficulties with the use of such a scheme, for me this is a sure sign that it is necessary to design differently, since difficulties have already arisen, then there will be even more of them in the future))
Well, all packages as parameters accept only interfaces everywhere.
Although ... from the point of view of Dependency Inversion, it would be correct to describe your own interface for each package, and not the way I do :), but I make the interfaces very small, then it turns out very flexible and, it seems to me, after that it’s still inside It is already redundant to describe interfaces of other packages in my case, there is more work, but there is not much sense, so for now I save time on this :)
cmd/serve.go- imports the app and starts the web server for example
If you need any more details - write, I will be happy to help

M
montray, 2021-06-07
@quiex

Of course, the first option will be better (so, usually, everyone does). Inject the interface, keep the definition in a separate package (domain, system, helper etc), and the implementation in service.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question