In my personal project, I recently decided to encapsulate the Echo server in a struct to improve the organization, maintainability, and extensibility of my code. In this article, I’ll explain the benefits of this approach and provide a practical example.
Why Encapsulate the Echo Server?
Encapsulating the Echo server in a struct offers several advantages:
- Organization and Modularity: Grouping all server-related operations in a single struct makes the code easier to read and maintain. Ease of Configuration: Centralizing server configuration allows for easy application of global settings, such as middlewares and ports.
- Extensibility: Adding new functionalities becomes straightforward. You can add methods for configuring middlewares, routes, and other server-specific features without modifying the main code. Testability: Creating unit and integration tests becomes easier. You can instantiate the server with different configurations and test its functionalities in isolation.
- Dependency Injection: Encapsulating the server allows for organized dependency injection, making it easier to pass configurations, services, and other components needed for the server’s operation.
- Framework Agnosticism: By encapsulating the server logic, you can easily switch to another framework if you find a better one, without incurring high maintenance costs. This abstraction layer decouples your application logic from the specific server implementation.
Practical Example
Here is an example of how I encapsulated the Echo server in my project:
package api
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Server struct {
server *echo.Echo
}
func NewServer() *Server {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
s := &Server{
server: e,
}
s.buildRoutes()
return s
}
func (s *Server) Start(address string) error {
return s.server.Start(address)
}
func (s *Server) buildRoutes() {
s.server.GET("/", func(c echo.Context) error {
return c.String(200, "Hello, World!")
})
// Add other routes here
}
Using the Server in main
To use the encapsulated server in your main function, you can simply create a new instance of the server and call its Start method.
Notice that the main function has no idea which framework is being used to run the server. This highlights the framework agnosticism of the approach.
The server configuration, including middleware setup and route definitions, is centralized within the Server struct, making it easy to manage and modify.
By encapsulating the server logic in a struct, the main function remains clean and focused on high-level application flow:
package main
import (
"discount-club/infra/api"
"log"
)
func main() {
server := api.NewServer()
if err := server.Start(":8000"); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
Example Tests
Creating unit and integration tests for the encapsulated server is straightforward and highlights the benefits of encapsulation. Encapsulating the server logic makes it easier to create isolated tests for different components and functionalities. Grouping related tests in a suite improves the organization and readability of the test code. Here are two example test cases for the encapsulated server:
package api_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/joaofilippe/discount-club/infra/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ServerTestSuite struct {
suite.Suite
server *api.Server
}
func (s *ServerTestSuite) SetupTest() {
s.server = api.NewServer()
}
func (s *ServerTestSuite) TestNewServer() {
assert.NotNil(s.T(), s.server)
assert.NotNil(s.T(), s.server.Start)
}
func (s *ServerTestSuite) TestSingletonServer() {
server1 := api.NewServer()
server2 := api.NewServer()
assert.Equal(s.T(), server1, server2)
}
func TestServerTestSuite(t *testing.T) {
suite.Run(t, new(ServerTestSuite))
}
Conclusion
Encapsulating the Echo server in a struct brings several benefits in terms of organization, maintainability, extensibility, testability, and dependency injection. This approach helps create cleaner, more modular, and easier-to-maintain code. I applied this technique in my personal project, and it has significantly improved the structure and readability of my code.
Top comments (0)