This could be a large topic and there are many ways of handle interfaces gracefully, and here I’m going to use one simple example to explain this.
Please see below code: I have interface defined in package domain:
I have interface implementation embeded in package grpc:
Package domain:
This package defines an interface named StoreRepository. This interface specifies a set of methods that any concrete type (struct) must implement to satisfy the interface. In this case, StoreRepository requires a method called Find that takes a context and a storeID and returns a *Store and an error.
Package grpc:
This package defines a struct named StoreRepository. The struct StoreRepository in the grpc package is attempting to satisfy the StoreRepository interface defined in the domain package.
var _ domain.StoreRepository = (*StoreRepository)(nil): This line of code creates a static type assertion. It tells the Go compiler to check if the StoreRepository type in the grpc package satisfies the StoreRepository interface in the domain package. It doesn’t create a variable or have any runtime effect; it’s solely for the purpose of static type checking during compilation.
If the StoreRepository type in the grpc package doesn’t implement all the methods required by the StoreRepository interface in the domain package, the Go compiler will produce an error at compile time. This ensures that you can’t mistakenly think you’ve implemented an interface when you haven’t.
So, in essence, this code is ensuring that the StoreRepository struct in the grpc package correctly implements the StoreRepository interface defined in the domain package, providing compile-time safety and assurance that the required methods are implemented.
Cannot Interface and Struct stay in one package?
Types and interfaces can be defined in separate packages for good reasons.
Here’s why they might be in different packages in the context of an event-driven architecture:
Separation of Concerns:
In a well-structured Go application, you often want to separate concerns to keep your codebase organized and maintainable. The domain package typically contains domain-specific types and interfaces, which define the core business logic and data structures. On the other hand, the grpc package may contain implementation details related to the gRPC communication protocol. This separation allows you to cleanly distinguish between domain-specific code and communication code.
Dependency Isolation:
By keeping domain-specific types and interfaces in a separate package (domain in this case), you can ensure that other parts of your application only depend on the domain logic. This minimizes dependencies on external packages or frameworks, making your code more modular and easier to test.
Implementation Flexibility:
Placing the concrete StoreRepository struct in the grpc package gives you the flexibility to implement it using gRPC-specific functionality without polluting the core domain logic. If, in the future, you want to change the communication protocol from gRPC to something else (e.g., HTTP or WebSocket), you can create a new package for that implementation while keeping the domain logic intact.
Testing:
When writing unit tests for the domain logic, you can easily mock the StoreRepository interface in the domain package without needing to import gRPC-related packages or deal with network communication. This isolation simplifies unit testing.
please check above screenshot the mock_store_repository file, this mock implementation can be used in unit testing to isolate the code being tested from its dependencies.
This is how the MockStoreRepository is used for unit testing inside application_test.go file.
Event-Driven Architecture:
In an event-driven architecture, components of your system communicate by emitting and consuming events. These events often represent changes or updates in the system’s state. By separating domain logic from communication protocols (like gRPC), you can keep your domain logic agnostic of the specific event transport mechanism. This allows you to swap out one event transport for another without affecting the core business logic.
In summary
Separating domain-specific types and interfaces from communication-specific implementations is a common design practice in Go to promote clean code, maintainability, and flexibility. This separation doesn’t just apply to event-driven architectures but is a general best practice in software engineering. It ensures that your codebase remains modular, testable, and adaptable to changing requirements.