Jauhar's Blog


I Prefix and Impl Suffix

2022/09/22

I often find people writing code with a pair of interface and implementation. This happens usually when working on code with dependency-injection pattern. There is an interface for every implementation. Sometimes, it is hard to find a good name for the interface and implementation type since they describe the same concept but have different roles. One of them is the interface whereas the other one is the implementation. For example, suppose you have a tax-calculator logic in your codebase, what do you call the interface and the implementation. Both of them are a tax calculator, but sometimes we can’t use the same name since our programming language needs to differentiate them. People have different conventions to solve this problem, sometimes they use I prefix to describe the interface. So, they will end up having ITaxCalculator and TaxCalculator type in their codebase. Other people use Impl suffix for the implementation type. So, they will end up having TaxCalculator and TaxCalculatorImpl.

However, I don’t really like this approach. This pattern is a little weird to read. When you have an I prefix on your interface, it’s kind of redundant. The I prefix stands for interface, which does nothing to the naming. We already know the type is an interface from the type definition. At least for most programming languages such as Go, Rust, and Java. You don’t need to have an I prefix to tell that it’s an interface. The same thing applies to the Impl suffix, you don’t need this suffix to tell that this is an implementation. And apart from that, if you change your method definition, you need to change both the implementation and the interface, which means your code is not as loosely coupled as you think it is.

The advantage of this pattern is that by having an interface for everything, you always have the ability to replace the implementation with another object. This can be very useful for testing. Sometimes, when writing a unit test, you want to mock some of your objects or want to make a fake implementation of it. If you are using dynamically typed programming languages like Python, you don’t have a problem. But, if you use statically typed programming languages such as Java and Go, you can’t just substitute it easily. Hence, you need an interface to make it pluggable.

Writing testable code is very important. But, people tend to discriminate test code and see it as a second-class citizens. People tend to think that the test code is not real code, it shouldn’t be visible from the actual code, and doesn’t have to be maintainable. People tend to not think about testing during the design phase. Instead of thinking about how to test the code, they think about the implementation and figure out the test later, after the implementation is already done. When writing the implementation, people rarely think, “How am I going to test this?”. As a result, they try to make an interface for everything, just in case they need to mock it for testing later. I don’t like this practice. It makes the test code look ugly and unmaintainable. As a result, we can’t get strong tests and good test coverage.

I personally don’t like to mock my code too much. I like to mock my code just when I don’t have any other choice such as when the code is too complex to run and slow down the test, or when working on network or I/O because the behavior of the code is hard to reproduce and can be unstable. But, most of the time, mocking makes the test unnecessarily complex. The next time a logic in your code changes, you need to change all the mock behavior you also have. Otherwise, your test will be irrelevant to the new requirement. Instead of writing mocks, try to isolate the code that is hard to test from what is easier to test. Usually, everything that doesn’t touch any I/O like network, file or system calls is easier to test because they are usually deterministic, or at least predictable in terms of the behavior and the performance. If you are doing web-based applications, doing this also has another advantage: it is clearer what I/O operations are performed by your app. Because you already isolated the I/O-related stuff and CPU-related stuff, you can see all the I/O you do in your app clearly.

Writing software is a journey. Having a lot of pairs of implementations and interfaces is not ideal. We often don’t need the interface since there is only one implementation of the interface. And sometimes, it is a sign that we don’t think enough about how we are going to test it. But, it doesn’t mean we shouldn’t do it. In the beginning, I think it’s OK to do it since we don’t know what the better way is to do it, yet. But along the way, we should keep improving our codebase. Down the road, we will see that this component should be split into two components, these two components should be merged together.

All software engineers write ugly code, but a good software engineer constantly improves their code.