Open
Description
Problem:
The current module.go file directly instantiates concrete implementations of repositories, services, and external providers. This tight coupling makes it difficult to write isolated unit tests for all required scenarios for the module's components (controllers, handlers, services) because tests often end up relying on real databases or external APIs.
The issue also with implicit dependencies. It's not immediately clear what a component needs to function. This makes it harder to understand how different parts of the system interact.
// module.go
func (m *Module) Register(app application.Application) error {
// ...
clientRepo := persistence.NewClientRepository(passportRepo)
chatsService := services.NewChatService(chatRepo, clientRepo, twilioProvider, app.EventPublisher())
app.RegisterServices(
chatsService,
services.NewClientService(clientRepo, chatsService, app.EventPublisher()),
// ...
)
// ...
}
Proposed Solution:
Refactor the modules to use interface-based dependency injection. This involves:
- Defining Interfaces: Create interfaces for all dependencies (repositories, services, external providers like Twilio) within the module.
- Injecting Interfaces: Modify the module.go file to inject dependencies into controllers, handlers, and services via these interfaces.
- Concrete Implementations: Ensure that concrete implementations of the interfaces are provided during module registration.
Benefits:
Improved Testability:
- Mocking: Interfaces allow you to easily create mock implementations of dependencies during testing. This isolates the unit under test and prevents tests from relying on external resources (databases, external APIs).
- Test Doubles: You can create test doubles (mocks, stubs, fakes) that simulate the behavior of real dependencies, allowing you to test different scenarios and edge cases.
- Test different cases: You can test different cases by passing different mocks.
- Faster test runs: faster iterations because of you don't need always rely on external resources
Decoupling:
- Loose Coupling: Interfaces decouple components from concrete implementations. Changes to a concrete implementation don't necessarily require changes to the components that depend on it, as long as the interface remains the same.
- Flexibility: You can easily swap out different implementations of a dependency without modifying the code that uses it.
Maintainability:
- Clear Contracts: Interfaces define clear contracts for how components interact, making the codebase easier to understand and maintain.
- Reduced Complexity: Decoupling reduces the overall complexity of the system by making it easier to reason about individual components.
Code generation:
- Mockery: You can use mockery to generate mocks for your interfaces.
// module.go
package crm
import (
"context"
"github.com/iota-uz/iota-sdk/modules/crm/domain/aggregates/client"
"github.com/iota-uz/iota-sdk/modules/crm/domain/aggregates/chat"
"github.com/iota-uz/iota-sdk/pkg/eventbus"
"github.com/twilio/twilio-go"
)
//go:generate mockery --dir . --output ./mocks
type ClientRepository interface {
GetByID(ctx context.Context, id uint) (client.Client, error)
Create(ctx context.Context, data client.Client) (client.Client, error)
// ... other methods ...
}
type ChatRepository interface {
GetByClientIDOrCreate(ctx context.Context, clientID uint) (chat.Chat, error)
// ... other methods ...
}
type TwilioProvider interface {
// ... methods for sending messages ...
}
type ChatService interface {
GetByClientIDOrCreate(ctx context.Context, clientID uint) (chat.Chat, error)
// ... other methods ...
}
type ClientService interface {
GetByID(ctx context.Context, id uint) (client.Client, error)
Create(ctx context.Context, data *client.CreateDTO) (client.Client, error)
// ... other methods ...
}
func (m *Module) Register(app application.Application) error {
// ...
var clientRepo ClientRepository = persistence.NewClientRepository(passportRepo)
var chatRepo ChatRepository = persistence.NewChatRepository()
var twilioProvider TwilioProvider = cpassproviders.NewTwilioProvider(
twilio.ClientParams{
Username: conf.Twilio.AccountSID,
Password: conf.Twilio.AuthToken,
},
conf.Twilio.WebhookURL,
)
var chatsService ChatService = services.NewChatService(chatRepo, clientRepo, twilioProvider, app.EventPublisher())
var clientService ClientService = services.NewClientService(clientRepo, chatsService, app.EventPublisher())
app.RegisterServices(
chatsService,
clientService,
// ...
)
// ...
}
Metadata
Metadata
Assignees
Labels
No labels