Search code examples
godecoupling

How to decouple this transitive dependency


I'm trying to figure out how to remove a transitive dependency from my service.

Let's call my service ServiceA.

ServiceA depends on LibraryB. LibraryB depends on LibraryC. Therefor ServiceA transitively depends on LibraryC. Let me explain how...

In this case LibraryC happens to be the ozzo-validation library. In this library there is a type named Errors that is defined as a map[string]error. You can see it at https://github.com/go-ozzo/ozzo-validation/blob/v3.6.0/error.go but here it is for reference:

package validation

type Errors map[string]error

// Implement the error interface
func (es Errors) Error() string {
    // Implementation omitted for brevity
}

Note the type Errors implements the error interface.

As I already wrote LibraryB depends on LibraryC, ozzo-validation. LibraryB's use of ozzo-validation is this:

package web

// Error responds to a request with an error object and the specified status
func Error(w http.ResponseWriter, err error, status int) {
    // Implementation omitted for brevity
    errors, ok := err.(validation.Errors)
    if ok {
        for key, err1 := range errors {
            // Implementation omitted for brevity
        }
        // Implementation omitted for brevity
    }
    // Implementation omitted for brevity
}

That's the entire usage. LibraryB imports ozzo-validation so that LibraryB can do a type assertion, errors, ok := err.(validation.Errors), and then range over the map, for key, err1 := range errors.

My service, ServiceA, has no idea that LibraryB has a dependency on ozzo-validation. ServiceA also wants to use ozzo-validation, but needs to use a newer version because the newer version has more features and some important bug fixes. This newer version is v4.3.0. ServiceA calls some methods in ozzo-library that return a validation.Errors instance and passes that instance to LibraryB's web.Error function as the err error parameter.

This is where the fun starts. Because ServiceA is passing in a v4.3.0 validation.Errors instance and LibraryB is type asserting against v3.6.0 validation.Errors the type assertion fails even though the type definitions are exactly the same in both v3.6.0 and v4.3.0.

How can I fix this problem?

I do have access to LibraryB's source code and I can change it. I could easily upgrade LibraryB to use v4.3.0 of ozzo-validation, but that would perpetuate this transitive coupling. I'd much rather remove this transitive coupling completely.

I've tried changing the type assertion in LibraryB to

errors, ok := err.(map[string]error)

because ultimately that's exactly what the instance is, map[string]error but the compiler doesn't like that because map[string]error doesn't implement the error interface.

Is there some way I can make my own object that implements error and is also range-able so that I could wrap the v4.3.0 `validation.Errors in some kind of interface or something that will break this transitive coupling?

What can I do to break this tight, transitive coupling?


Solution

  • If backwards compatibility of LibraryB is a concern, just upgrading ozzo-validation in LibraryB to v4 is not an option. Because if there is a ServiceD that uses LibraryC@v3 and LibraryB, such an upgrade would break ServiceD.

    Fortunately, with help of Go Modules, LibraryB can import both v3 and v4 and do type assertions against both versions.

    package web
    
    import (
     validationv3 "github.com/go-ozzo/ozzo-validation/v3"
     validationv4 "github.com/go-ozzo/ozzo-validation/v4"
    )
    
    // Error responds to a request with an error object and the specified status
    func Error(w http.ResponseWriter, err error, status int) {
        // Implementation omitted for brevity
        errorsv3, ok := err.(validationv3.Errors)
        if ok {
            for key, err1 := range errorsv3 {
                // Implementation omitted for brevity
            }
            // Implementation omitted for brevity
        }
        // Implementation omitted for brevity
    
    
        errorsv4, ok := err.(validationv4.Errors)
        if ok {
            for key, err1 := range errorsv4 {
                // Implementation omitted for brevity
            }
            // Implementation omitted for brevity
        }
        // Implementation omitted for brevity
    
    }