A recent article described some problems when dealing with Java interfaces, coming from a language that allows duck typing.
The bottom line is this. There is an interface:
public interface DynamoImpl {
public PutItemResult putItem(PutItemRequest putItemRequest);
public GetItemResult getItem(GetItemRequest getItemRequest);
}
And there is a class AmazonDynamoDB
. That class has the two above methods, but also a lot more. Now John, the programmer, wants to expose only these two methods of AmazonDynamoDB
to the rest of his program. He wants to use them in his class Database
:
public class Database {
public DynamoImpl db;
// code removed for simplicity
public Database(DynamoImpl db) {
this.db = db;
}
}
Now: in a language that supports duck typing, you could just pass an instance of class AmazonDynamoDB
to the constructor, and call the two methods (because the class has them). Not in Java, though. You need to pass in an instance of a class that literally implements
the DynamoImpl
interface (or extends a class that does so). But AmazonDynamoDB
does not implement that interface.
The easiest way to achieve the same goal in Java is to create a wrapper that delegates to an AmazonDynamoDB
instance:
public class AmazonDynamoDbWrapper implements DynamoImpl{
private AmazonDynamoDB amazonDynamoDB;
public AmazonDynamoDbWrapper(AmazonDynamoDB amazonDynamoDB) {
this.amazonDynamoDB = amazonDynamoDB;
}
@Override
public PutItemResult putItem(PutItemRequest putItemRequest) {
return amazonDynamoDB.putItem(putItemRequest);
}
@Override
public GetItemResult getItem(GetItemRequest getItemRequest) {
return amazonDynamoDB.getItem(getItemRequest);
}
}
Yes, a bit verbose, but pretty straight forward once you understand the pattern. Now you can pass an instance of this class to an instance of Database
.
So much for the production code. What about tests?
The easiest solution is to provide a minimal implementation of the DynamoImpl
interface, like so:
public class StubbedDynamoImpl implements DynamoImpl {
@Override
public PutItemResult putItem(PutItemRequest putItemRequest) {
return new PutItemResult(/* Test data here */);
}
@Override
public GetItemResult getItem(GetItemRequest getItemRequest) {
return new GetItemResult(/* Test data here */);
}
}
Again, you can pass an instance of this class to the Database
instance's constructor, in a test:
public class DatabaseTest {
@Test
public void createsDatabase() {
Database database = new Database(new StubbedDynamoImpl());
// Whatever your test assertions are, here
}
// ...
}
No need for fancy mocking frameworks if all you want to expose is two methods.
This is the easiest solution I could think of. I hope it's useful.
P.S.
One word concerning Java conventions: please don't use the suffix Impl for interfaces. This should be used for concrete implementation classes (if at all), or otherwise you might confuse a lot of people.
Top comments (18)
I think, this only might be a problem when you never worked (weren't socialized) on a statically typed language.
I love the metaphor of interfaces being contracts. In implementing an interface you promise to syntactically adhere to the standards required by the contract - if you do semantically useful things is on another sheet of paper.
Unless you aren't implementing an interface, there is good reason for the compiler to suspect something errorneous and scream about it.
But beware of cargocultig interfaces for every class to write. I've seen many codebases where each and every class had its interface which is utterly nonsensical. Here too helps the contracts metaphpor:
Does it make sense to set up a contract if you are the only participant?
No. And before someon comes but with »but in the future« just let me say »Nah! YAGNI«
P.S.: I was socialized on statically typed languages but at present write only python and JS (plain no TS). I am missing nothing so far. But done my homework in statically typed languages helps me avoiding problems in the dynamically typed world.
Yeah, I had a short period where I created interfaces for everything as well. I am glad it‘s over. :) I find them most useful when communicating with external systems (where external could mean sth like microservice as well).
I am really interested what the root for this antipattern is. I have the suspicion that once people misunderstood "program to interfaces not implementations" and took it literally.
When seeing this pattern I tell programmers that every time they do this a little puppy drowns 😬
I'm curious, do you use dependency injection and unit test heavily?
I've gotten into the pattern of creating interfaces for almost all of my classes so that except for the code that needs to construct the object everything can refer to the interface. This makes mocking out that dependency WAY easier with better test performance to boot.
Agree with this. It's not just about communicating with external systems but testing would be a lot easier if dependencies are passed in and we don't create new instances that depend on other classes
Well, for me, a unit isn’t always equal to a class. In other words: I test several classes together. I know people have strong opinions about isolation- for me, it works just fine, and there are several TDD practioners who advocate this style (among them Martin Fowler and Kent Beck, if I‘m not mistaken).
You get fewer tests, that don’t break easily when the implementation changes. The downside is errors are a bit more difficult to find.
That's a fair approach. I'm certainly not dogmatic about this stuff; as long as you have a good understanding of the pros and cons involved and accept them, like you do here, that's fine. I do think this takes a bit more discipline around understanding what the "unit under test" is, so adoption could be an issue depending your team.
I don't recall reading anything by Fowler on this specific subject and I ❤️ that man. You wouldn't happen to know the book/blog/talk off hand would you? Sometimes searching for these things can prove to be difficult.
I found a pretty lengthy explanation of Robert Martin, a.k.a. Uncle Bob, who also prefers to test several classes at once if necessary: blog.cleancoder.com/uncle-bob/2017...
Thanks for the Sunday reading!
There’s more :) Here’s the interview of Martin Fowler, Kent Beck and DHH about DHHs article “TDD is dead” where they mention that they don’t mock that much. martinfowler.com/articles/is-tdd-d...
Re: your comment about statically typed languages.
The current trend in Education is to start with Python. But as someone involved in University education myself, I wonder if starting with Python makes it harder to then later motivate people to learn this stuff. If you've done Java first, then learning Python is a breeze. But if you've learned Python first, then learning Java is a total drag. (Presumably learning the first language is always a bit problematic for some)
I wonder how we motivate students to bother with statically typed languages (Java and C# are super important in the comercial world) if we start with something so simple as Python.
From my personal experience I made the switch from .Net(later Java) to Python and I worked with a legacy codebase where I saw patterns which were hard to grasp and a compiler would have definitivly complained.
OTOH I would like to see Go more in education. Although I dislike the language for $reasons, I clearly see the upsides of being elegant, fast to learn, general purpose and a nice take on structs and interfaces which mostly makes the student fall into the pit of success.
But I am going too offtopic 😉
I know a lot of people are currently interested in Go. But from an educator's perspective I see little benefit in promoting it at this time. While it has some following in the commercial world its not huge (yet?). Check indeed.com for example and compare to others.
Also, from a linguistic perspective I think some of Go's omissions are important. I think students should be exposed to a full set of OO features. While Go may have its niche in the commercial world, I think a well grounded Python+Java+C developer should be able to pick it up.
In fairness though, the author of the other article is comparing it to another statically typed language (Go).
Interfaces in Go are way more convenient to use compared to Java whilst still being statically checked.
Looks like Amazon skipped the I in SOLID.
The author of the other post wrote DynamoImpl interface in their own project (because the AmazonDynamoDB had interfaces they didn't intend to use) and expected the AmazonDynamoDB to automagically implement DynamoImpl because the signatures match. (This is what Go does, and the author was dissatisfied that Java did not do this.)
If Amazon can have their Java classes implement interfaces that were created after they ship their production artifacts, I'm going to start expecting my prime deliveries a lot faster
My comment wasn't about what the guy in the other article was trying to do. It was about how this interface has dozens methods. It should have either implemented 4-5 other more focused interfaces or, preferably in my opinion, used composition to own those 4-5 interfaces.
If felt with too many of the God classes and I know how they come about. Wether it's a DB or a session is the core of the context of the application so it ends up growing like Katamari.