Design an interface for your component, examples

May 26, 2021

Last time, we discussed the spectrum of design decisions you operate within when designing an interface for your component. That component could be something small like a function, a module, or something way more complex like the whole application.

The conclusion was this. Within the spectrum of design between an interface focused on clients’ use-cases on the one side and a self-centered interface on the other, there is a sweet spot for an individual module. Usually, the favorable solution is closer to the selfish architecture, thus the interface is less influenced by its consumers and more care is placed on its coherent, internal design and planned optimizations. With such an approach, the code is easier to maintain and hugely contributes to the overall system stability.

Examples in your code

The idea of designing exemplary interfaces is crucial on almost any level of code architecture. Let’s start with the most obvious one — variable naming. What is an interface of a variable, you may ask? Well, it’s merely a thing to set and obtain a value, and the operation itself is constrained within the language syntax, but think about the name. You may easily name your variable however you like, but how to do this the best?
When naming your variable, you should briefly describe its data — the purpose of its existence. There is no need to contemplate every possible consumer’s use case. To ensure that your codebase is concise, you should create selfish variable names.

Going further into complexity, it is helpful to consider functions. Those are elements with not only a name but also parameters and a return value. What do you think is the best way to design and name all those things? Indeed the more open your function is to various types of arguments, the more logic is required to normalize them inside the function. Providing different result types will also result in a more complex logic. When you decide to name your function or its parameters not by the virtue of its logic but instead by its consumers’ usages, you will definitely pay the price in refactor time. The more strict your function is concerning the input and output, the better control you have, and the easier it is to maintain the implementation.

Another popular element in your code that should benefit from the selfish design is an integral part of the Object-Oriented programming — a class, usually providing a public API to enable interaction between modules. In this case, an interface expands and includes class methods. The argument to name them with regards to an internal logic seems more natural, thanks to the concept of encapsulation, deeply embedded in the OOP. However, it is worth to mention that even a tiny diversion from this strategy, like allowing clients to pass unconstrained data, or naming a method to consumers’ preference, may result in problems with consistency, maintainability, and testing.

Higher-level examples

This approach scales further from a single module. The following perfect examples are utility libraries and standard libraries of a specific language. In short, a utility library is a set of functions designed to speed up a programmer’s work, providing an implementation for common patterns. When you look at those functions, you will see an idea of the self-centered design. These are well-defined, highly composable, single responsibility functions that do not care about your use-cases but allow you to employ them in many different situations. The terminology is consistent across the library, thus making it easier to reason about.

The identical approach holds when you look at an array of command-line interface programs in your operating system. Those too are designed to do a single thing and operate upon a strict set of arguments. You can combine them into scripts to achieve complex tasks, but the responsibility to create proper data flow lies in the client’s hands. Their strengths are composability and a high number of individual programs designed around a consistent contract of clean interfaces.

There are many more examples of well-designed modules, programs, and libraries. You can find similar principles in most of the interfaces you interact with.

Systems and beyond

The selfish architecture goes even further, and the further it goes, the more complicated are the architectural decisions. There are usually many different conditions to meet in more complex systems, thus it is more challenging to ensure design consistency across the system.

An excellent high-level example is an interface architecture of a REST API. When you design such an interface, you should compose your endpoints based on internal data and relations. You should also restrain yourself from creating client-specific solutions. This way, your endpoints are readily available for future use-cases, thanks to the interface’s client-agnostic scheme.
Of course, there are situations demanding particular optimization or handling of a special case. Still, the rule of thumb should be to start the interface design from the base endpoints and only then consider implementing additions if needed.

When you look at any system available to consumers through some API, you will realize that the approach in question is pretty widespread. It also coexists with specific architectures, like microservices. Those architectures should be loosely coupled and focused on a single task. In addition, their interfaces tend to be minimal and very strict, which hugely benefits collaboration.

Sometimes, there is a requirement for a specific integration between components. That kind of module seems like the only good reason to abandon a strict self-centered architecture for a while. That is because the integration, by definition, should generally care about its clients. Nonetheless, any other use-case tends to benefit from a selfish interface instead.

Conclusion

As you see, there are many examples of interfaces centered around the selfish design that favors solutions closer to their own perspective. They reside on different layers of a system and implement various abstractions. They are usually easier to maintain and develop, easier to test and debug, and frequently easier to scale at the end of the day.

One thing to remember when implementing your module’s interface is to think about its evolution and future maintenance cost. When you plan the interface around its internal responsibility, in the long run, you will experience affordable maintainability and universal application because a module that does one thing well enough is more powerful than it may seem. Good luck!