Importance of using interface while construct program and example of using go
Table of Contents
- Why Should We Design Applications Based on Interfaces?
- How Interface-Based Design Improves the Development Process
- Challenges of Interface-Based Design
- How Go Supports Interface-Based Design
- Example: Designing an Ethereum Transaction Sender Interface
- Advantages of Interface-Based Design
- Conclusion
Why Should We Design Applications Based on Interfaces?
Modern software applications are composed of multiple modules that interact with each other. To ensure scalability, maintainability, and flexibility, interface-based design is widely recommended.
Let’s take Netflix as an example. Netflix is not just a video streaming service; it includes user management, subscription payments, recommendations, advertising, and many other services. Each team within Netflix works on different components, often following their own schedules and priorities. Without a well-defined interface-based approach, collaboration and integration would become extremely complex.
The best way to address this challenge is to design and develop applications based on interfaces.
How Interface-Based Design Improves the Development Process
Let’s assume we’re developing an app that enables users to purchase subscriptions.
Scenario
- Team A is responsible for user management, and Team B is developing the payment system.
- Team A needs to implement subscription purchases, which depend on Team B’s payment gateway (PG) integration.
- Team B is busy with other tasks and cannot immediately implement the PG integration. However, Team A cannot wait indefinitely for them to complete it.
- Team B provides an interface that defines the PG integration structure. Even though the actual implementation is not ready, the interface specifies what the final implementation will look like.
- Team A develops a mock implementation based on the provided interface and proceeds with subscription feature development.
- Once Team B completes the real PG integration, Team A replaces the mock implementation with the actual implementation.
By adopting interface-based design, modules remain decoupled and can be developed independently without waiting for other teams.
Challenges of Interface-Based Design
Despite its benefits, interface-based design is challenging when business logic is not well-defined.
- Business requirements must be well-analyzed before development to determine what interfaces will be needed.
- Some Agile methodologies misinterpret speed as skipping planning. Fast development should not mean skipping clear business requirements and interface design.
- If Team A starts development without a clear understanding of what Team B will deliver, frequent changes to the interface can result in wasted effort.
- A well-defined interface can reduce development time, not increase it.
To prevent unnecessary changes, a clear interface design should be agreed upon before development begins.
How Go Supports Interface-Based Design
Go encourages interface-based design and provides a simple yet powerful way to implement it.
Key Differences from Java and Other Languages
- No need for explicit interface declarations in implementing structs.
- No separate interface files are required.
- If a struct implements all the methods of an interface, it automatically satisfies that interface.
Best Practices for Defining Interfaces in Go
- Use meaningful names ending in -er to describe the action performed.
- Example:
- Printer: Prints output
- Writer: Writes to a file
Example: Designing an Ethereum Transaction Sender Interface
Let’s define an interface for sending signed Ethereum transactions.
type Sender interface {
SendTransaction(ctx context.Context, from common.Address, to *common.Address, value *big.Int, data []byte) error
}
Transaction Sending Approaches
Ethereum transactions can be sent in two ways:
- Synchronous (Sync): Waits until the transaction is mined before returning a response.
- Asynchronous (Async): Sends the transaction without waiting for confirmation.
Since both approaches use the same parameters, we can define a single Sender
interface.
Using Dependency Injection for Flexibility
We create a TxManager
struct that depends on a Sender
interface rather than a specific implementation.
type TxManager struct {
sender Sender
}
// Injects an implementation of Sender into TxManager
func NewTxManager(sender Sender) *TxManager {
return &TxManager{
sender: sender,
}
}
Different Implementations of the Sender Interface
We now create two different implementations:
- SyncSender: Implements synchronous transactions.
- AsyncSender: Implements asynchronous transactions.
- Both implementations satisfy the
Sender
interface because they define the required method.
// SyncSender
func (t *SyncSender) NewSyncSender() *SyncSender {}
func (t *SyncSender) SendTransaction(ctx context.Context, from common.Address, to *common.Address, value *big.Int, data []byte) error
// AsyncSender
func (t *AsyncSender) NewAsyncSender() *AsyncSender {}
func (t *AsyncSender) SendTransaction(ctx context.Context, from common.Address, to *common.Address, value *big.Int, data []byte) error
Injecting the Implementations
We can now inject either SyncSender
or AsyncSender
into the TxManager dynamically.
func main() {
syncSender := NewSyncSender()
asyncSender := NewAsyncSender()
// Use synchronous transaction processing
txm := NewTxManager(syncSender)
// Use asynchronous transaction processing
txm = NewTxManager(asyncSender)
}
Advantages of Interface-Based Design
Decouples Modules
- Teams can develop independently without waiting for other teams.
- Reduces dependencies between different components.
Enables Dependency Injection
- Implementations can be easily replaced or modified without changing business logic.
- Useful for mock testing and swapping different implementations.
Enhances Maintainability & Scalability
- Clear separation of concerns makes code easier to maintain.
- New features can be added without modifying existing components.
Encourages Reusability
- The same interface can have multiple implementations, making it reusable across different scenarios.
Conclusion
Why should we use interface-based design:
- Modularization – Develop and maintain different parts of a system independently.
- Flexibility – Swap implementations without changing business logic.
- Scalability – Extend and modify software without breaking existing functionality.
- Testing – Use mock implementations for unit tests.