Application Focused API Design

Nov 3, 2016


“I think it’s ok to do heinous stuff to test an API if it makes it more usable by others.” - Nate Finch

Prelude

If you are new to Go, it might help to read these posts first before continuing on with this post.

https://www.goinggo.net/2014/05/methods-interfaces-and-embedded-types.html
https://www.goinggo.net/2015/09/composition-with-go.html
https://www.goinggo.net/2016/10/reducing-type-hierarchies.html
https://www.goinggo.net/2016/10/avoid-interface-pollution.html

Introduction

Packages exists to help provide support for specific problems that are commonly found in the different applications we are building. A package API should be intuitive and simple to use so application developers can focus on their concerns and hopefully develop their applications faster. Tests are an artifact of development and exist to make sure the code we are writing has integrity before it is published. Tests are not a part of the application. They do not get built with the application and none of that code runs when the application is running.

When designing a package API that will be used by an application developer, we have been taught to focus on writing testable API’s first. With a focus on not only what the API needs in terms of testing, but also what the application developer needs in terms of testing. This idea of writing API’s with a focus on what the application developer needs for their tests is something that I don’t agree with for Go. I believe package developers should focus on how the application developer needs to use the API in their applications, not their tests. Application tests are solely a concern for and the responsibility of the application developer, not the package developer.

Note: Who am I to say as a package developer, what your application needs in terms of testing? This is not the purpose of my API. I can’t focus on what you may or may not need to do with your tests. I want to focus the API on providing you the simplest and most intuitive way to solve the problems for your application. Focusing on your need to test, assuming what you need in terms of testing, is beyond the scope of the package API I’m writing and in my opinion a slippery slope.

Let’s look at an example that shows how focusing on what the application developer needs for their applications, and not their tests, can help to keep package API’s simpler, minimized and more intuitive.

Code Example

One day I am asked to write a new package that provides an API to the internal queuing system that all applications are required to use. The API has to provide basic publish and subscribe support. Since this is the only queuing system that is allowed to be used, this new pubsub package can be implemented using only concrete types. Nothing can change since we will not be required to support another queuing system, therefore interfaces are not required.

With this in mind, I write the following package:

Listing 1:

01 // Package pubsub simulates a package that provides
02 // publication/subscription type services.
03 package pubsub
04
05 // PubSub provides access to a queue system.
06 type PubSub struct {
07     /* impl */
08 }
09
10 // New creates a pubsub value for use.
11 func New(/* impl */) *PubSub {
12     ps := PubSub{
13        /* impl */
14     }
15
16     /* impl */
17     return &ps
18 }
19
20 // Publish sends the data for the specified key.
21 func (ps *PubSub) Publish(key string, v interface{}) error {
22     /* impl */ 
23 }
24
25 // Subscribe requests the data for the specified key.
26 func (ps *PubSub) Subscribe(key string) error {
27     /* impl */ 
28 }

If we look at the code for the pubsub package in listing 1 we see a concrete type named PubSub declared on line 06 with two methods. One method is named Publish declared on line 21 and the other method is named Subscribe declared on line 26. On line 11 we have a factory function named New that creates and initializes a PubSub value for use.

There is no need for an interface because the application developer who will use the package does not need to provide any implementation details. Further, we do not require supporting different queuing systems internally inside the package. I write tests that hit the actual system so I know 100% that the package is working. Then I publish the package and tell the team it is ready for use.

Not too long after the team starts using the package, a team member approaches me with a problem. They are trying to write tests for their application and they don’t have access to the internal queuing system when their tests run. They ask me to provide an interface so they can mock access to the internal queuing system for their tests. I very quickly tell them NO and simply state that the pubsub package does not need an interface so I won’t be providing one. However, if they need an interface for their testing, nothing is stopping them from declaring one for themselves.

Listing 2:

// publisher is an interface to allow this package to mock the
// pubsub package support.
type publisher interface {
    Publish(key string, v interface{}) error
    Subscribe(key string) error
}

Listing 2 shows the declaration of the publisher interface I tell the application developer to declare. The interface declares the set of methods associated with my pubsub package. This means that the concrete type PubSub in my package implements this new publisher interface.

With this interface declared by the application, the developer can now write their application and tests to use this interface. My PubSub value can now be decoupled from their app and they have the ability to mock this behavior in their tests.

Listing 3:

// mock is a concrete type to help support the mocking of the
// pubsub package.
type mock struct{}

// Publish implements the publisher interface for the mock.
func (m *mock) Publish(key string, v interface{}) error {
    /* impl */
}

// Subscribe implements the publisher interface for the mock.
func (m *mock) Subscribe(key string) error {
    /* impl */
}

Listing 3 shows a mocking implementation of the publisher interface with the declaration of the concrete type mock. This concrete type can be used for testing since it provides its own implementation that does not need to talk with the physical system.

Conclusion

I disagree with designing a package API that focuses on the application developer’s need to write tests. Since the compiler will identify interface compliance through convention and not configuration, the application developer has the ability to decouple the things they need decoupled and to apply this throughout their applications and tests. I would like to see package developers take an application focused approach to API design as a first priority. I believe this will keep package API’s simpler, minimized and more intuitive for application developers.

Thanks

Here are some friends from the community I would like to thank for taking the time to review the post and provide feedback.

Antonio Troina, Kaveh Shahbazian


Go Training

We have taught Go to thousands of developers all around the world since 2014. There is no other company that has been doing it longer and our material has proven to help jump start developers 6 to 12 months ahead of their knowledge of Go. We know what knowledge developers need in order to be productive and efficient when writing software in Go.

Our Go, Web and Data Science classes are perfect for both experienced and beginning engineers. We start every class from the beginning and get very detailed about the internals, mechanics, specification, guidelines, best practices and design philosophies. We cover a lot about "if performance matters" with a focus on mechanical sympathy, data oriented design, decoupling and writing production software.

Learn More

To learn about Corporate training events, options and special pricing please contact:

William Kennedy
ArdanLabs (www.ardanlabs.com)
bill@ardanlabs.com