feat: implement dependency injection with proper hexagonal architecture

 Features:
- Simple dependency inversion following SOLID principles
- Clean constructor injection without complex DI containers
- Proper hexagonal architecture with driving/driven separation
- Presentation layer moved to cmd/cli for correct application structure

🏗️ Architecture Changes:
- Driving Adapters (Inbound): internal/adapters/driving/cli/
- Driven Adapters (Outbound): internal/adapters/driven/edgeconnect/
- Core Services: Dependency-injected via interface parameters
- main.go relocated from root to cmd/cli/main.go

📦 Application Flow:
1. cmd/cli/main.go - Entry point and dependency wiring
   └── Creates EdgeConnect client based on environment
   └── Instantiates services with injected repositories
   └── Executes CLI with properly wired dependencies

2. internal/adapters/driving/cli/ - User interface layer
   └── Receives user commands and input validation
   └── Delegates to core services via driving ports
   └── Handles presentation logic and output formatting

3. internal/core/services/ - Business logic layer
   └── NewAppService(appRepo, instanceRepo) - Constructor injection
   └── NewAppInstanceService(instanceRepo) - Interface dependencies
   └── NewCloudletService(cloudletRepo) - Clean separation

4. internal/adapters/driven/edgeconnect/ - Infrastructure layer
   └── Implements repository interfaces for external API
   └── Handles HTTP communication and data persistence
   └── Provides concrete implementations of driven ports

🔧 Build & Deployment:
- CLI Binary: make build → bin/edge-connect-cli
- Usage: ./bin/edge-connect-cli --help
- Tests: make test (all passing)
- Clean: make clean (updated paths)

💡 Benefits:
- Simple and maintainable dependency management
- Testable architecture with clear boundaries
- SOLID principles compliance without overengineering
- Proper separation of concerns in hexagonal structure
This commit is contained in:
Stephan Lo 2025-10-08 18:15:26 +02:00
parent 2625a58691
commit 8e2e61d61e
23 changed files with 79 additions and 43 deletions

View file

@ -18,12 +18,12 @@ test-coverage:
# Build the CLI
build:
go build -o bin/edge-connect .
go build -o bin/edge-connect-cli ./cmd/cli
# Clean generated files and build artifacts
clean:
rm -f sdk/client/types_generated.go
rm -f bin/edge-connect
rm -f bin/edge-connect-cli
rm -f coverage.out coverage.html
# Lint the code

40
cmd/cli/main.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"os"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driving/cli"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/services"
)
func main() {
// Präsentationsschicht: Simple dependency wiring - no complex container needed
// 1. Infrastructure Layer: Create EdgeConnect client (concrete implementation)
baseURL := getEnvOrDefault("EDGE_CONNECT_BASE_URL", "https://console.mobiledgex.net")
username := os.Getenv("EDGE_CONNECT_USERNAME")
password := os.Getenv("EDGE_CONNECT_PASSWORD")
var client *edgeconnect.Client
if username != "" && password != "" {
client = edgeconnect.NewClientWithCredentials(baseURL, username, password)
} else {
client = edgeconnect.NewClient(baseURL)
}
// 2. Application Layer: Create services with dependency injection (client implements repository interfaces)
appService := services.NewAppService(client) // client implements AppRepository
instanceService := services.NewAppInstanceService(client) // client implements AppInstanceRepository
cloudletService := services.NewCloudletService(client) // client implements CloudletRepository
// 3. Presentation Layer: Execute CLI driven adapters with injected services (simple parameter passing)
cli.ExecuteWithServices(appService, instanceService, cloudletService)
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

BIN
edge-connect-cli Executable file

Binary file not shown.

View file

@ -52,12 +52,12 @@ Here is the proposed directory structure:
### Adapters
* `internal/adapters/cli`: The CLI adapter. It implements the user interface and calls the `driving` ports of the core.
* `internal/adapters/edgeconnect`: The EdgeXR API adapter. It implements the `driven` port interfaces and communicates with the EdgeXR API.
* `internal/adapters/driving/cli`: The CLI adapter. It implements the user interface and calls the `driving` ports of the core.
* `internal/adapters/driven/edgeconnect`: The EdgeXR API adapter. It implements the `driven` port interfaces and communicates with the EdgeXR API.
### `cmd`
* `cmd/main.go`: The main entry point of the application. It is responsible for wiring everything together: creating the adapters, injecting them into the core services, and starting the CLI.
* `cmd/cli/main.go`: The main entry point of the CLI application. It is responsible for wiring everything together: creating the adapters, injecting them into the core services, and starting the CLI.
## Refactoring Steps
@ -65,9 +65,9 @@ Here is the proposed directory structure:
2. **Define ports:** Define the `driving` and `driven` port interfaces in `internal/core/ports`.
3. **Implement core services:** Implement the core business logic in `internal/core/services`.
4. **Create adapters:**
* Move the existing CLI code from `cmd` to `internal/adapters/cli` and adapt it to call the core services.
* Move the existing `sdk` code to `internal/adapters/edgeconnect` and adapt it to implement the repository interfaces.
5. **Wire everything together:** Update `cmd/main.go` to create the adapters and inject them into the core services.
* Move the existing CLI code from `cmd` to `internal/adapters/driving/cli` and adapt it to call the core services.
* Move the existing `sdk` code to `internal/adapters/driven/edgeconnect` and adapt it to implement the repository interfaces.
5. **Wire everything together:** Update `cmd/cli/main.go` to create the adapters and inject them into the core services.
## Benefits

View file

@ -66,7 +66,7 @@ var createAppCmd = &cobra.Command{
},
}
err := appService.CreateApp(context.Background(), region, app)
err := services.AppService.CreateApp(context.Background(), region, app)
if err != nil {
fmt.Printf("Error creating app: %v\n", err)
os.Exit(1)
@ -85,7 +85,7 @@ var showAppCmd = &cobra.Command{
Version: appVersion,
}
app, err := appService.ShowApp(context.Background(), region, appKey)
app, err := services.AppService.ShowApp(context.Background(), region, appKey)
if err != nil {
// Handle domain-specific errors with appropriate user feedback
if domain.IsNotFoundError(err) {
@ -114,7 +114,7 @@ var listAppsCmd = &cobra.Command{
Version: appVersion,
}
apps, err := appService.ShowApps(context.Background(), region, appKey)
apps, err := services.AppService.ShowApps(context.Background(), region, appKey)
if err != nil {
fmt.Printf("Error listing apps: %v\n", err)
os.Exit(1)
@ -136,7 +136,7 @@ var deleteAppCmd = &cobra.Command{
Version: appVersion,
}
err := appService.DeleteApp(context.Background(), region, appKey)
err := services.AppService.DeleteApp(context.Background(), region, appKey)
if err != nil {
fmt.Printf("Error deleting app: %v\n", err)
os.Exit(1)

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/apply"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"github.com/spf13/cobra"

View file

@ -45,7 +45,7 @@ var createInstanceCmd = &cobra.Command{
},
}
err := instanceService.CreateAppInstance(context.Background(), region, appInst)
err := services.InstanceService.CreateAppInstance(context.Background(), region, appInst)
if err != nil {
fmt.Printf("Error creating app instance: %v\n", err)
os.Exit(1)
@ -67,7 +67,7 @@ var showInstanceCmd = &cobra.Command{
},
}
instance, err := instanceService.ShowAppInstance(context.Background(), region, instanceKey)
instance, err := services.InstanceService.ShowAppInstance(context.Background(), region, instanceKey)
if err != nil {
fmt.Printf("Error showing app instance: %v\n", err)
os.Exit(1)
@ -89,7 +89,7 @@ var listInstancesCmd = &cobra.Command{
},
}
instances, err := instanceService.ShowAppInstances(context.Background(), region, instanceKey)
instances, err := services.InstanceService.ShowAppInstances(context.Background(), region, instanceKey)
if err != nil {
fmt.Printf("Error listing app instances: %v\n", err)
os.Exit(1)
@ -114,7 +114,7 @@ var deleteInstanceCmd = &cobra.Command{
},
}
err := instanceService.DeleteAppInstance(context.Background(), region, instanceKey)
err := services.InstanceService.DeleteAppInstance(context.Background(), region, instanceKey)
if err != nil {
fmt.Printf("Error deleting app instance: %v\n", err)
os.Exit(1)

View file

@ -15,24 +15,15 @@ var (
username string
password string
appService driving.AppService
instanceService driving.AppInstanceService
cloudletService driving.CloudletService
// Services injected via constructor - no global state
services *ServiceContainer
)
// SetAppService injects the application service
func SetAppService(service driving.AppService) {
appService = service
}
// SetInstanceService injects the instance service
func SetInstanceService(service driving.AppInstanceService) {
instanceService = service
}
// SetCloudletService injects the cloudlet service
func SetCloudletService(service driving.CloudletService) {
cloudletService = service
// ServiceContainer holds injected services (simple struct - no complex container)
type ServiceContainer struct {
AppService driving.AppService
InstanceService driving.AppInstanceService
CloudletService driving.CloudletService
}
// rootCmd represents the base command when called without any subcommands
@ -52,6 +43,18 @@ func Execute() {
}
}
// ExecuteWithServices executes CLI with dependency-injected services (simple parameter passing)
func ExecuteWithServices(appSvc driving.AppService, instanceSvc driving.AppInstanceService, cloudletSvc driving.CloudletService) {
// Simple dependency injection - just store services in container
services = &ServiceContainer{
AppService: appSvc,
InstanceService: instanceSvc,
CloudletService: cloudletSvc,
}
Execute()
}
func init() {
cobra.OnInitialize(initConfig)

View file

@ -12,7 +12,7 @@ import (
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

View file

@ -1,7 +0,0 @@
package main
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/cli"
func main() {
cli.Execute()
}

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
)

View file

@ -11,7 +11,7 @@ import (
"os"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
)