2

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?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
HairOfTheDog
  • 2,489
  • 2
  • 29
  • 35
  • 1
    validation v3 and validation v4 are **different**, unrelated packages. This might Seen strange but thats how modules work and is the mechanism which allows to use both versions. So validation@v3.Errors is a different type than validation@v4.Errors and of course you cannot type assert different types. – Volker Feb 12 '21 at 05:17
  • But: validation@v3.Errors and the v4 version do have the same underlying type and this can be converted _after_ the appropriate type assertion. You **cannot** break this coupling. If ServiceA calls into ServiceB and B requires a validation@v3.Errors than A has to pass a validation@v3.Errors and there is no cheating around this hard dependency. – Volker Feb 12 '21 at 05:25
  • 1
    ServiceA must import both packages validation/v3 and validation/v4, convert one Errors type to the other and pass the v3 version to ServiceB. Once you understand that different major versions of a package are different packages you‘ll see that the transitive coupling is there and cannot be magically tricked away. – Volker Feb 12 '21 at 05:37

1 Answers1

1

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

}
vearutop
  • 3,924
  • 24
  • 41