Cdaprod / registry-service

0 stars 0 forks source link

Solution: Interfacing with registry service — Agnostic Facade Interfacing for Builtins Loader & Plugins Loader #2

Open Cdaprod opened 2 weeks ago

Cdaprod commented 2 weeks ago

Going with a lightweight facade will help maintain a clean separation of concerns and keep your codebase flexible and easily maintainable. By using a facade, you provide a simple, unified interface to the more complex underlying subsystem, allowing you to manage both built-in and dynamically loaded plugins efficiently.

Key Benefits of Using a Lightweight Facade

  1. Simplicity and Clarity: The facade simplifies the interface exposed to the main application (main.go), making it easier to understand and work with. This helps in maintaining the code as the number of plugins and complexity grows.

  2. Flexibility: It allows for easy integration and removal of plugins without modifying the core logic in main.go or other core components. You only need to adjust the facade and the respective plugin loaders.

  3. Loose Coupling: By separating the plugin loading logic into the facade and plugin loaders, you maintain loose coupling between the core application logic and the plugin management. This design adheres to the principle of high cohesion and low coupling, which is key in creating maintainable software.

  4. Easy Testing and Maintenance: The facade pattern makes it easier to mock and test the plugin loading process independently from the core application logic. This separation also allows for easier debugging and updating of plugin logic without affecting the main application.

Next Steps

By implementing this design, you ensure that your registry service remains adaptable and scalable, capable of handling both current and future integration needs with minimal changes to the core architecture.


Instructions

Utilizing Go's native plugin system in a Facade pattern to handle both built-ins and dynamic plugins is possible and aligns well with your goal of creating a highly cohesive and loosely coupled system. However, deciding whether to integrate the native plugin loading directly in the Facade or handle it individually within builtins and plugins depends on your design goals and modularity requirements.

Approaches to Using Go Native Plugin in the Facade

  1. Centralized Plugin Management via Facade:

    • Pros:
      • The Facade acts as the single entry point for all plugin loading, providing a unified interface for managing both built-ins and dynamically loaded plugins.
      • This approach simplifies the main.go execution path since all plugin management logic is encapsulated in the Facade.
      • Reduces redundancy by avoiding repeated code in both builtins and plugins.
    • Cons:
      • It could lead to a larger Facade class, which might become harder to maintain if the plugin management logic becomes complex.
      • May reduce the flexibility of handling specialized loading procedures for different types of plugins (e.g., handling different initialization requirements for built-ins vs. dynamic plugins).
  2. Individual Plugin Handling in builtins and plugins:

    • Pros:
      • Keeps the Facade lightweight by delegating specific loading responsibilities to builtins and plugins.
      • This approach provides greater flexibility to handle unique requirements or initialization logic for each plugin type.
      • Enhances maintainability by adhering to the Single Responsibility Principle, where each component (builtins and plugins) only handles its specific task.
    • Cons:
      • Requires some duplication of code between builtins and plugins for common plugin management tasks (like error handling, plugin validation, etc.).
      • Slightly more complex integration in main.go as it requires separate calls to the Facade for loading different plugin types.

Recommended Approach

Given your emphasis on creating a decoupled and abstracted system with minimal changes to the core (main.go) while maintaining loose coupling and strong cohesion, a hybrid approach could be optimal:

Implementation

Facade with Delegation to Individual Loaders

// pkg/facade/facade.go
package facade

import (
    "github.com/Cdaprod/registry-service/internal/service"
    "github.com/Cdaprod/registry-service/pkg/plugins"
    "github.com/Cdaprod/registry-service/pkg/builtins"
)

type Facade struct {
    service      *service.Service
    pluginLoader *plugins.PluginLoader
    builtinLoader *builtins.BuiltinLoader
}

func NewFacade(svc *service.Service) *Facade {
    return &Facade{
        service: svc,
        pluginLoader: plugins.NewPluginLoader(svc),
        builtinLoader: builtins.NewBuiltinLoader(svc),
    }
}

func (f *Facade) LoadAll() error {
    if err := f.builtinLoader.LoadAll(); err != nil {
        return err
    }
    if err := f.pluginLoader.LoadAll(); err != nil {
        return err
    }
    return nil
}

Builtin and Plugin Loaders Handling Go Native Plugins

pkg/builtins/builtin_loader.go:

package builtins

import (
    "plugin"
    "github.com/Cdaprod/registry-service/internal/service"
    "fmt"
    "path/filepath"
    "os"
)

type BuiltinLoader struct {
    service *service.Service
}

func NewBuiltinLoader(svc *service.Service) *BuiltinLoader {
    return &BuiltinLoader{service: svc}
}

func (bl *BuiltinLoader) LoadAll() error {
    return filepath.Walk("path/to/builtins", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if filepath.Ext(path) != ".so" {
            return nil
        }

        p, err := plugin.Open(path)
        if err != nil {
            return fmt.Errorf("failed to open plugin: %v", err)
        }

        symRegister, err := p.Lookup("Register")
        if err != nil {
            return fmt.Errorf("failed to find Register function in %v: %v", path, err)
        }

        registerFunc, ok := symRegister.(func(service *service.Service) error)
        if !ok {
            return fmt.Errorf("invalid Register function signature in plugin: %v", path)
        }

        if err := registerFunc(bl.service); err != nil {
            return fmt.Errorf("failed to register built-in plugin: %v", err)
        }

        return nil
    })
}

pkg/plugins/plugin_loader.go:

package plugins

import (
    "plugin"
    "github.com/Cdaprod/registry-service/internal/service"
    "fmt"
    "path/filepath"
    "os"
)

type PluginLoader struct {
    service *service.Service
}

func NewPluginLoader(svc *service.Service) *PluginLoader {
    return &PluginLoader{service: svc}
}

func (pl *PluginLoader) LoadAll() error {
    return filepath.Walk("path/to/plugins", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if filepath.Ext(path) != ".so" {
            return nil
        }

        p, err := plugin.Open(path)
        if err != nil {
            return fmt.Errorf("failed to open plugin: %v", err)
        }

        symRegister, err := p.Lookup("Register")
        if err != nil {
            return fmt.Errorf("failed to find Register function in %v: %v", path, err)
        }

        registerFunc, ok := symRegister.(func(service *service.Service) error)
        if !ok {
            return fmt.Errorf("invalid Register function signature in plugin: %v", path)
        }

        if err := registerFunc(pl.service); err != nil {
            return fmt.Errorf("failed to register plugin: %v", err)
        }

        return nil
    })
}

Conclusion

By leveraging a combination of the Facade pattern with Go's native plugin system, you can create a modular, flexible, and easily extendable architecture for your registry service. The facade coordinates the loading process, while the individual loaders handle the specific details, ensuring that the entire system remains cohesive and adaptable to future requirements.

Cdaprod commented 2 weeks ago

Touched on this in registry-service with the concrete "Service" type in internal/.

This is part of the pkg/facade, builtins_loader, plugins_loader all being written as parts of facade.go, builtins.go, and plugins.go.

Point to repocate issue @Cdaprod/repocate discussing Concrete RegistryRegistry, which registry service allows for Registerable... so it tracks, its 1 am.

🫳 🎤