What I Learned at Work this Week: The Practical Value of an Interface

Mike Diaz
4 min readMar 30, 2024
Photo by Pedro Figueras: https://www.pexels.com/photo/two-pigeon-perched-on-white-track-light-681447/

I’ve been busy at work. My Python script and I have joined a new engineering team, and I stress the word team here. In my previous experience, I was allowed to be a subject matter expert in my little Python world, rarely having to venture out into someone else’s code. But now we’re talking about being part of an on-call rotation, handling support tickets, building new features…all for a complex series of products…which are written in Java.

After one week, the main thing I can say is that my teammates have been very patient with me. For example, my first ticket is to write a service that periodically deletes rows from a DB to preserve space. When I was asking about writing tests (obviously I don’t know how to write tests), I was asked “do you know what an interface is?”

Yes, yes I do.

“Can you see why one would be useful here?”

Well, no…I cannot. Despite learning about interfaces way back when I was a recent bootcamp grad, I don’t think I have ever looked at a situation and said “oh, yes, this calls for an interface.” That is, until last week.

My class

I mentioned that we wanted to delete rows from a table, so I wrote something like this:

public class DiazPostgresCleanupRepository {
private final Jdbi jdbi;

public DiazPostgresCleanupRepository (Jdbi postgresJdbi) {
this.jdbi = postgresJdbi;
}

public long deleteRowBatch() {
return jdbi.withHandle(handle -> handle.createUpdate(
"""
WITH deleted AS (
DELETE FROM permission_requests
WHERE id IN (
SELECT id FROM permission_requests
WHERE created < current_date - 21
LIMIT 10000) RETURNING id
) SELECT COUNT(*) FROM deleted;
""")
.execute())
.longValue();
}
}

A little bonus here is that I can point out a couple of other things I learned at work this week:

  • First, this method returns a long instead of a Long. I was taught not to use the boxed primitive (Long) because, among other things, it’s possible for its value to be null , which obviously is not a long. Here’s a dev.to post by Kyle Carter that goes a little more into it.
  • I wanted the method to return a value because I could later use that value to determine how frequently the job should run. We don’t know how many rows in permission_requests are older than 21 days, but we want to delete them all. If the method returns 10,000, we want to run it again soon. If it returns 0, we should give it a break.
    To make that work, I turned the DELETE into an auxiliary statement with WITH. By RETURNING id (or any value), the auxiliary statement executes its function and becomes a table that contains one row for each id that was deleted. Then just running a SELECT COUNT(*) on that will return the number of deleted rows. I had to add longValue() to the very end because execute returned the count as a String.

Getting back to the class at hand, I should be able to easily implement this through a Service class and call it a day. But how am I supposed to write a test when my repository makes a SQL request?

My interface

If we create an interface first, then we can implement it differently depending on the context. Just in case you need a refresher, an interface is “a group of related methods with empty bodies” (Java docs). It’s like a template, or a skeleton for a class. Interfaces cannot be instantiated, but they can be implemented by classes, which must define the methods the interface has outlined. In this case, my interface looked like this:

public interface DiazPostgresCleanupRepository {

long deleteRowBatch();
}

All we assume is that the class that implements this is going to have a deleteRowBatch method, which returns a long. We don’t need to know how it derives that long yet. We also don’t need a constructor, even though we know the class is going to require a jdbi object when built.

The only thing we have to change about the class is:

public class DiazPostgresCleanupRepositoryImpl implements DiazPostgresCleanupRepository {
private final Jdbi jdbi;
...

We don’t want our class to have the same name as our interface, so we give it some unique characteristic. And we add implements so that the interface rules are enforced.

In the production code, we’re going to keep the logic that runs the DELETE, but if it’s impractical to run DELETEs during our tests, we can create a different class in the test directory, which doesn’t include SQL. Maybe something like this:

public class TestDiazPostgresCleanupRepository implements DiazPostgresCleanupRepository {

public long deleteRowBatch() {
return 10L;
}
}

If we define our Service such that it expects an instance of DiazPostgresCleanupRepository to be available, we can pass this in just for the tests and not have to worry about creating DBs. All thanks to the utlity of interfaces!

What are we testing?

You might be thinking “removing the only logic in your Repository means you’re not testing anything!” and, depending on the circumstance, you might be right. The idea here is really useful for testing something on a higher level than the Repository itself, like that the Service can successfully start up, or that it can be triggered to re-run if deleteRowBatch returns a value greater than zero.

In my case, I was actually pointed in the direction of an Integration Test pattern that can build, populate, and tear down DBs within a test so that I can actually test my Repository logic after all. I still kept the interface, though, because I think it’s cleaner and generally follows other patterns I’ve seen within the codebase. Plus, if I ever do have to simplify my tests, I’ll have the option!

Sources

--

--