Refactor mock files and codes
Closed this issue · 1 comments
Is your feature request related to a problem? Please describe.
Siren has mock files generated with mockery. However there are some missing things that need to be added and updated:
- Add
make generate
in Makefile to autogenerate mocks - Add
go generate
mockery annotation in each interface files to add ability to generate mock per interface- This is also to have more granular control to organize the generated mock files into a specific folder
- Reorganize the mocks files to meet
odpf
standard
Some improvements to the code structure also needed
- Highly coupled codes
- Several global variables
- Some variables name are same with package name, it will cause conflict in the future
Describe the solution you'd like
- Add
make generate
in Makefile to autogenerate mocks. - Add
go generate
mockery annotation in each interface files to add ability to generate mock per interface. \- This is also to have more granular control to organize the generated mock files into a specific folder.
- Reorganize the mocks files to meet
odpf
standard.
Generated mocks for dependency interfaces can be inside mocks/ package in the respective package. see [handbook](https://github.com/odpf/handbook).
- There are also possibilities to refactor domain to its own package, similar case like what entropy does
- Refactor the highly coupled codes 1, 2
- Refactor several global variables 1, 2. Instantiation should only be done when initiating server, unless it is a singleton or lazy initialization
There will be 3 PRs for this Raising this with 1 PR instead
- Refactor global variables so every initialization done in the server creation
- Decouple highly coupled package & services
- Refactor files in
domain
package to its respective domain and organize the mock files better
Siren project still doesn't group all domain into its own domain package name, instead it stores all domain in a domain
package. If I remember last time the issue was import cycle. I have taken a look deep dive into the codebase and figure out the are some places where the code is highly coupled and don't have clear separation of concern.
Here are the points that could be improved.
Better passed an instance as dependency rather than initializing an instance inside a constructor (ref)
The code below shown we create NewSlackHelper()
and slack.NewService()
inside service constructor, this makes slackHelper
and slackService is hard to test. Even if we wire the dependency correctly, service does not need to know slackHelper
, its only dependency is slackService
.
// Service handles business logic
type Service struct {
repository store.ReceiverRepository
slackService slackService
slackHelper SlackHelper
}
// NewService returns service struct
func NewService(repository store.ReceiverRepository, httpClient http.Doer, encryptionKey string) (domain.ReceiverService, error) {
slackHelper, err := NewSlackHelper(httpClient, encryptionKey)
if err != nil {
return nil, errors.Wrap(err, "failed to create slack helper")
}
return &Service{
repository: repository,
slackHelper: slackHelper,
slackService: slack.NewService(),
}, nil
}
We can refactor the code to be like this.
type Service struct {
repository store.ReceiverRepository
slackService slackService
}
func NewService(repository store.ReceiverRepository, slackService SlackService) (domain.ReceiverService, error) {
return &Service{
repository: repository,
slackService: slackService,
slackHel
}, nil
}
and slackService
is
func NewService(repository store.ReceiverRepository, slackHelper SlackHelper) (domain.ReceiverService, error) {}
With the refactored code above, it is easier to mock the SlackService
and SlackHelper
and easier to test.
If we do this, we don't need to do this and we don't need to test like this.
All initializations should be done in the highest level (server creation) and pass down the instance to the lower components through dependency (ref)
All services, repositories, clients, etc should be created during server creation step (Except for some cases like singleton and lazy initialization). This will make a single source of truth of component creation and this would increase the predictability of our software.
If an error happened during a component creation, we could fail fast and fix the problem faster. Moreover, we don't have to deal with the scenario where component creation failure happen inside our business logic if we sure that all creation are handled earlier.
Don't mix all logic that interact with external parties inside our domain service (ref)
Here we can see there is cortex
client initialized and being used. Don't do this. Instead, we need to have a single package, for example cortex
(usually inside a pkg/cortex
) that does all interaction to external parties. Later, our domain service should only depend on the pkg/cortex
and this package can be used by any service in our domain. The cortex client can be initialized in the server creation and passed as dependency to the services.
Be more defensive in the code (ref)
There are a lot of scenarios that caused nil to be returned. Accessing nil in golang would cause panic. Panic is something that we should avoid. Gracefully handle the nil would make our code less possible to panic.
For example
clientId := configurations["client_id"].(string)
We need to check whether casting is fine. Even we can check if clientID is empty, we don't need to do external call, just error out.
In go function, if there is no output of the function, just return error only (ref)
This function return (*domain.SlackMessageSendResponse, error)
with
type SlackMessageSendResponse struct {
OK bool `json:"ok"`
}
This is ineffective, we can just make the function return error
only and check the OK inside the function.
Create a function as pure as possible
Non-pure function would make our software less predictable and would give you hard time to debug.
Avoid naming a variable with the same name like a package name in the project (ref)
subscription
is a package name in our project, naming a variable with the same name as a package name might not give issue for now. But later when the refactoring happen, there might be conflict with the package name and give more time to fix.
Avoid this issue by not using package name as variable.
Use blackbock testing to avoid import cycle and mimick the usage
Network is always unreliable, compensate this scenario
If we have a client that makes external call. Be sure to have a retry/circuit breaker/other resiliency logic.
Separating business logic and encryption logic (ref)
I notice in several places, we have some kind a "hook" to transform token plaintext to ciphertext in service layer. Seeing the usage, encryption & decryption are not really the business logic, they were tied to how do we store the data in repository. This is more or less the diagram.
We can make it better by separating the concern of encryption and business logic by introducing a kind of secure service proxy
like this.
Both service and secure service should conform the same interface. Doing so would make our code easier to test since we have the same mocks. There is also separation of concern with business logic and encryption logic.