3

tl;dr: Should I avoid exposing third-party types in a public interface?


I'm working on a Kotlin-based project that relies heavily on data keyed with two values. It's just something I'm tinkering with at the moment to learn Kotlin, but I've been considering publishing it to a public repository such as Maven Central.

In pure Java I might store this data in a Map<Foo, Map<Bar, Baz>> so that, given a Foo and a Bar, I might access it like so:

Baz someBaz = someData.get(someFoo).get(someBar)

In Kotlin, I can use

val someBaz = someObject[someFoo][someBar]

to the same effect, which is (slightly) cleaner. Instead, I opted to leverage the Table class in Guava* since it stores data in exactly this fashion and is considerably easier to work with.

Given that I may be publishing this project in a public fashion, should I expose (in public methods) references to the Guava Table class, or should I wrap them with an equivalent pure-Java (or Kotlin) API?


For example, should I do this (Option 1):

fun doSomeProcessing(data: Table<Foo, Bar, Baz>): Table<Foo, Bar, Baz>

and simply consume and produce a Table directly, or should I do this (Option 2):

fun doSomeProcessing(data: Map<Foo, Map<Bar, Baz>>): Map<Foo, Map<Bar, Baz>>

and then wrap the Map<Foo, Map<Bar, Baz>> in a Table?

Should I just operate on the Map directly, and forget about using Table* (Option 3)?

My gut says that Option 1 isn't a good idea, because I'm directly exposing a third-party API that users of the project might not (probably won't?) be interested in using.

On the other hand, Option 2 feels like a pointless obfuscation; if I'm really operating on a Table, I should just call it a Table and stop messing around with wrapping it in a recursive Map.

Option 3 just seems... wrong. I'm already using Guava and it exposes a very useful API that has exactly the sort of data structure I want to use, and I don't want to reinvent and reimplement a bunch of Guava code*.


*Note that this isn't the only reason I'm depending on Guava, and I'd have to change a non-trivial amount of code and reimplement a significant amount of functionality to do away with it entirely. So it's an option, but not one I'd be very happy to take.

6
  • 8
    By exposing the 3rd party types, you are permanently welding your library to the 3rd party library (i.e. tight coupling). Commented Dec 7, 2016 at 13:07
  • 3
    That might be a good decision, or an atrocious one. Decide that case by case. Commented Mar 1, 2019 at 13:28
  • 1
    It will sound controversial, but doesn't matter. If you, as designer are fine with exposing implementation details, then it's ok. Ultimately, the API and the dependency on Guava won't change untill you say so and looks like it's not going to happen anytime soon. So let users to decide whether they agree or disagree with you. If coupling is a problem, give support for both, Maps and Tables and let the user choose. Commented Mar 31, 2019 at 21:14
  • @Laiv I can think of a number of situations where a 3rd party library is found to have dangerous security flaws such as remote execution. I don't think it's a good idea to assume you can migrate to something else on your own timeline. Commented Jul 29, 2019 at 17:23
  • 1
    @Laiv Yeah, I thought about this some more. Utilities such as Collection APIs are a special case. It's probably OK as long as you are honest with yourself and your users that you have a hard dependency on the library and that dependency is transitive to the user i.e. "if you depend on my lib, you depend on their lib." Commented Jul 29, 2019 at 18:00

2 Answers 2

1

The API that you provide should be independent from the details of the implementation. Whether you write all the code yourself, or you use libraries... the API should be independent.

In that way you make sure that the user of the application can still use the application without problems, if you decide to make changes in the implementation.

2
  • 2
    Actually, they should be independent … unless they should not. There can be good reasons for both. Commented Mar 1, 2019 at 13:29
  • 1
    You are right, but I concentrated on the context of the question. Commented Mar 1, 2019 at 13:31
0

My first instinct when I skimmed this was that you should not build dependencies on 3rd-party libraries into your API. But on further consideration, it's a little more nuanced than that. But first there are at least two more options:

  1. Build an interface that encapsulates the features of the 3rd-party API and build an adapter/wrapper for Guava. This is a good idea if there's a known subset of functionality of the 3rd-party that is useful with your API.
  2. Build optional modules that allow the user to choose a module to work with. One of these may be a custom interface as in 4.

The big downside to 4 is that if you were to build your own custom interfaces to wrap the Guava capabilities and your users are using Guava, it's kind of silly. 5 mitigates that issue but is the most technically challenging and work-intensive of all the options.

When you are talking about utility packages such as guava, it might be a reasonable solution to bind yourself tightly to it. You should understand and make sure any users of this API understand that by taking a dependency on your API, a user is taking a dependency on Guava. The most likely challenge for the user will be when they want to use a new version of Guava that your API doesn't support.

The most likely challenge for you is that if Guava has a major release with incompatibilities, you will need to make a decision as to whether you want to continue t support the old version of Guava or make a hard break to the new version. The latter is the easiest for you but can be problematic for your users. A good example of how this gets messy is Kafka. They have two parallel version paths: one for the old version of Scala and one for the new version of Scala. I think it's plain to see that if there were another such dependency, the project would come completely off the rails.

Probably the best example where depending on 3rd party libs went broadly wrong is the Log4J fiasco. It was the defacto standard for logging and then a new version of Java incorporated a similar but incompatible set of classes. A lot of projects had a hard dependency on Log4J and others on the JDK libraries. Quickly, logging became a big mess where your often had to manage multiple logging configurations. The solution was (as in #5 above) to create a layer of indirection between the libraries and the details of the logging implementation e.g. SLF4J.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.