DEV Community

loading...
Cover image for Why Java Interfaces Are Terrible

Why Java Interfaces Are Terrible

forstmeier profile image John Forstmeier ・2 min read

So maybe they aren't terrible, but they certainly don't behave the way I was hoping that they would, being the newbie to Java that I am.

My problem arose when I was trying to build a unit testable bit of code for an AWS Lambda function which communicates with DynamoDB. What I did was create a class Database which had a constructor that looked like this:

public class Database {
    public DynamoImpl db;

    // code removed for simplicity

    public Database(DynamoImpl db) {
        this.db = db;
    }
}
Enter fullscreen mode Exit fullscreen mode

The DynamoImpl is an interface that looks like this:

public interface DynamoImpl {
  public PutItemResult putItem(PutItemRequest putItemRequest);

  public GetItemResult getItem(GetItemRequest getItemRequest);
}
Enter fullscreen mode Exit fullscreen mode

These two methods are part of the large AmazonDynamoDB interface whic can be used to communicate with your DynamoDB table. My goal with this design was to make it so that I could 1) pass in the production AmazonDynamoDB from my Lambda function (which calls the methods on Database) or 2) pass in a test implementation of the DynamoImpl interface to mock it out in unit tests. This would cut down on having to implement the entire 60 method-log AmazonDynamoDB interface and just make life easier.

Can't be done.

When I try to pass in AmazonDynamoDB it gets rejected.

The constructor Database(AmazonDynamoDB) is undefined

Additionally, casting the AmazonDynamoDB as DynamoImpl before passing it into the constructor seems to work initially, but fails when the code is actually invoked.

To me this is very weird but I do come from Go where stuff like this is straight forward. It feels like it's treating an interface like a strict type and that if you want to want to pass it in as an argument you can only implement the methods it defines, and nothing more. So instead of being able to pass in a reduced interface, it will only take the large one. This feels like such as weird thing to me...but then again, I am coming from a different language perspective.

So ends my rant.

EDIT: Here's a great response piece by Bertil Muth discussing a possible solution to my scenario. Well worth a read.

Discussion (44)

pic
Editor guide
Collapse
dmfay profile image
Dian Fay

If one interface doesn't extend the other then no, you're not going to be able to treat an implementor of A as an implementor of B. That would be duck typing, which describes Go's type system but not Java's.

On the plus side, because Java is extremely strict about what goes where, you can just create dual constructors where one takes your DynamoImpl and the other takes an AmazonDynamoDB.

Don't do that, use a mock framework.

Collapse
forstmeier profile image
John Forstmeier Author

That would be duck typing, which describes Go's type system but not Java's.

This is why I should read more about language fundamentals. : )

And could you explain how the dual constructors would work? I tried that approach but when I added the argument as an attribute on the class, I would get an error (since the attribute was expected either one of the two interfaces).

Collapse
dmfay profile image
Dian Fay

Strictly speaking, you're "supposed" to buckle down and stub out miles of interface method implementations that throw NotImplementedExceptions and this is absolutely a kludge. It's also a little more involved than I'd thought at first (I've been writing primarily Node for years and my Java is rusty):

  • You need a wrapper (formally an "adapter") class which essentially forwards the AmazonDynamoDB methods you care about. Your Database class may be this wrapper already, in which case, great.
  • The wrapper class has two constructors: one accepts a DynamoImpl, the other an AmazonDynamoDB.
  • The wrapper class also has matching DynamoImpl and an AmazonDynamoDB fields. The "forwarding" methods choose whether to invoke dynamoImpl.putItem or amazonDynamoDB.putItem based on which one has been instantiated.

Overall I can't recommend actually doing this purely for the sake of having something easily testable. If you're using an IDE (which is not a bad idea with Java), it can stub out an interface implementation for you. Java is verbose; you kind of just have to live with it.

Thread Thread
forstmeier profile image
John Forstmeier Author

Very helpful; it's definitely a lot of work but it seems like that may be the one plausible solution.

Collapse
cjbrooks12 profile image
Casey Brooks

Java and Go both use the term "interface" for very different things.

Because of its duck-typing in Go, an interface is just a collection of methods. There's really nothing more clever under the surface.

In Java, an interface is a type, in the same way that a class is a type. Realistically, the only difference between a class and an interface is that classes represent state while interfaces represent behavior. That is, interfaces can only expose methods to the world, but cannot define any fields. Classes, because they represent state, can expose fields which hold state, and also methods to modify that state (or do anything else).

Where Java classes and interfaces come together is that a class explicitly conforms to one or more interfaces. When it does this, a class effectively has multiple types. You can assign the class to another variable of that class type, or to any variable of any of its interface types.

The main takeaway is that while Go treats interfaces as facts about what an object can do, Java treats interfaces as facts about what the object is.

The problem with your example is that you're programming to the class, and not to the interface. In your example, you're treating the interface as something that can be swapped out as needed on the Database, but types are static and compile-time checked and cannot simply be swapped out. But objects can be swapped out, as long as multiple objects implement the same interface, you can swap them out however you wish. So instead of

public class Database {
    public DynamoImpl db;

    // code removed for simplicity

    public Database(DynamoImpl db) {
        this.db = db;
    }
}
public interface DynamoImpl {
  public PutItemResult putItem(PutItemRequest putItemRequest);

  public GetItemResult getItem(GetItemRequest getItemRequest);
}

What you really want to do is invert the class and interface, since you want to use a generic Database with multiple implementations.

public interface Database {
    public PutItemResult putItem(PutItemRequest putItemRequest);

    public GetItemResult getItem(GetItemRequest getItemRequest);
}
public class DynamoDatabaseImpl implements Database {
    public AmazonDynamoDB db;

    public Database(AmazonDynamoDB db) {
        this.db = db;
    }

    public PutItemResult putItem(PutItemRequest putItemRequest) {
        return db.putItem(putItemRequest)
    }

    public GetItemResult getItem(GetItemRequest getItemRequest) {
        return db.getItem(getItemRequest)
    }
}
public class TestDatabaseImpl implements Database {
    public MockDatabaseImpl db;

    public Database(MockDatabaseImpl db) {
        this.db = db;
    }

    public PutItemResult putItem(PutItemRequest putItemRequest) {
        return db.putItem(putItemRequest)
    }

    public GetItemResult getItem(GetItemRequest getItemRequest) {
        return db.getItem(getItemRequest)
    }
}
Collapse
miklish profile image
miklish

This is the most natural and simple solution in my opinion. The point being--don't use a class to represent the Database if you want to vary the database implementation.

If you want to only specify the methods the database exposes (its 'contract') but have the ability to change its implementation, then it's clear that Database should be the interface (the contract), and then you can create any number of concrete implementations of the Database as concrete implementation classes...

public class AmazonDB implements Database {...}

public class MockDB implements Database {...}

Now, classes can simply code against the abstract Database interface...

public class DatabaseUser {
  public void useDatabase(Database d) {
    PutItemRequest testPutReq = new PutItemRequest();
    d.putItem(testPutReq);
  }
}

You can then pass in whatever implementation of Database you want to the DatabaseUser class:

DatabaseUser dbUser = new DatabaseUser();

// implementation 1
Database aDb = new AmazonDB();

// mock db implementation
Database mockDb = new MockDB();

// Have DatabaseUser use Amazon
dbUser.useDatabase(aDb);

// now have it use the mock
dbUser.useDatabase(mockDb);
Collapse
goaty92 profile image
goaty92

I can list a hundred things Java that are terrible, but this one certainly isn't. If your Database class expects to work with AmazonDynamoDB interface (that is, one that has 60+ methods in it), then it makes sense to not pass in a reduced interface.

If you just want different implementations for production and mock testing, I believe you can just straight mock the interface AmazonDynamoDB itself. Most mocking frameworks are capable of this.

Collapse
forstmeier profile image
John Forstmeier Author

The reason for the reduced interface was just because I would only need those two methods. Is there a better way to get make this happen in Java? Also, is there a framework you'd recommend? I looked at Mockito but didn't find good examples for what I was trying to do.

Collapse
goaty92 profile image
goaty92

If Database only needs to work with those two method then the Java way to do is to write an implementation class for DynamoImpl which is a wrapper of your production AmazonDynamoDB, but only exposes those two methods publicly.

Mockito is a good framework that will probably have most of the features you'd want. What are you trying to do specificly? In Mockito you can do things like "create a (mock) object that implement this interface, but when method A() is called return some fake data" or stuffs like that.

Thread Thread
forstmeier profile image
John Forstmeier Author

Yes, Database only needs those two methods (right now, if I expand it later, I can add those methods); ideally, I want to be able to hit the "put" and "get" methods within the Database class without actually touching the production DynamoDB (which it wouldn't be able to do from a local run anyway without additional configuration).

Collapse
tux0r profile image
tux0r

Java (...) Terrible

Yup.

Collapse
chiefnoah profile image
Noah Pederson

Java itself isn't really that bad. It has it's issues, but every language does.

Collapse
cjbrooks12 profile image
Casey Brooks

The language may have its flaws, but a huge ecosystem of high-quality libraries more than makes up for it. Especially when you realize that you can eliminate most/all of the language's flaws with Lombok (hacky as it may be) or Kotlin, and still get the benefits of the great JVM ecosystem.

Thread Thread
tux0r profile image
tux0r

And still no working resource management which strikes all the JVM languages out for most things. Sorry, even Perl 6.

Thread Thread
chiefnoah profile image
Noah Pederson

What do you mean by resource management?

Thread Thread
tux0r profile image
tux0r

The JVM is RAM-hungry and you can't really tame it.

Collapse
yorodm profile image
Collapse
skittishsloth profile image
Matthew Cory

One nit I want to pick at is that you named your interface *Impl - don't know about Go, but in Java land you'd normally name your interface something like Database and your implementation of that would be something like DynamoImpl. NBD and to each their own, but I'd reject a pull request for that at the very least.

Collapse
forstmeier profile image
Collapse
aminmansuri profile image
hidden_dude

To understand the Java philosophy its useful to look at Pascal. Pascal is a language where everything is nicely structured for you and strict.

The idea is that we're going to put a lot in the language so that you avoid shooting yourself in the foot. That stands in contrast with dynamically typed languages of the day like Lisp or Smalltalk or BASIC, which were more free for all, but had few compiler checks.

The idea is that you want to burden the compiler as much as you can in finding bugs for you so you don't have to look for obvious type problems later on. It was also written in a time before automatic type inference.

So in this Java world, everything has to be formally declared and formally compatible. The advantage is that when looking at a 1 million lines of code program its easy to know what type of object you're going to get because that info is declared.

Interfaces are a way to make statically typed languages like Java and C++ a bit more flexible by allowing a looser coupling. But its not as loose as duck typing. You still need to preserve the formal type system so that it can determine if you're doing something wrong or not.

So Java is good if you like the compiler cleaning out obvious bugs for you. Dynamic typing is good when you want less protocol and shorter code in the hopes that shorter code means its easier to find/fix bugs.

But if you show me a huge codebase, I think I'd prefer it to be Java over something super dynamic like Javascript or Python. Its just easier to understand what is going on. But maybe I'm old fashioned.

Note: most companies are still old fashioned which is why Java and C# are so popular, but maybe changes in education with the emphasis of Python (a fully dynamic language) will finally shift the tide over time. I don't remember a dynamic language being so popular as a first language in Universities before (Scheme doesn't count because it was mostly Ivy League schools only). Also Javascript is becoming so predominant and I don't recall a dynamically typed language being so popularly used for major development since BASIC in the early 80s (and even that was limited). So this may mark a significant change in the culture of programming.

Collapse
yorodm profile image
Yoandy Rodriguez Martinez

Assuming you come from Go, you'll need to learn some proper OOP before jumping into Java.

Collapse
forstmeier profile image
John Forstmeier Author

That has rapidly become apparent.

Collapse
yorodm profile image
Yoandy Rodriguez Martinez

Don't get me wrong, I used to be one of the "proper OOP" guys, that was almost 15 years ago (Java EE was the new cool thing). Nowadays I just want productivity and stick with Python and Go

Collapse
rhymes profile image
rhymes

haha I could object to the definition of "proper OOP" and I would call it "Java style OOP" but yeah :D

Collapse
aminmansuri profile image
hidden_dude

Proper OO is Smalltalk.. so I'd say Python and Ruby are closer to "proper OO" than Java. (And I'm a Java guy)

Collapse
aminmansuri profile image
hidden_dude

Smalltalk is "proper OO" not Java. I'd say Python and Ruby are closer to "proper OO" than Java. (And I'm a Java guy)

Collapse
chiefnoah profile image
Noah Pederson

What you're describing is called dependency injection, which was first popularized by Java ([citation needed]). What you're doing can work, you just need to change a few things. Your class itself needs to implement the interface, and then call the injected db's methods:

public class Database implements DynamoImpl {

    private DyanmoImpl db;

    public Database(DyanmoImpl db) {
        this.db = db;
    }

    // implemented methods
    public PutItemResult putItem(PutItemRequest putItemRequest) {
        this.db.putItem(putItemRequest);
    }

    public GetItemResult getItem(GetItemRequest getItemRequest) {
        this.db.getItem(getItemRequest);
    }

}
Collapse
forstmeier profile image
John Forstmeier Author

Interesting. Would this then be able to accept an AmazonDynamoDB interface into the constructor? Or would there be a need for to like:

public Database(AmazonDynamoDB db) {
    this.db = db;
}

public Database(DynamoImpl db) {
    this.db = db;
}
Collapse
bertilmuth profile image
Bertil Muth

I wrote an article describing the easiest possible solution I can think of. I hope it helps to understand Java interfaces.

Collapse
forstmeier profile image
John Forstmeier Author

This is AWESOME and I will definitely be reading it!

EDIT: I'm going to include a link to this piece on my article.

Collapse
bzitzow profile image
Brian Zitzow

You have hard coded the implementation types. Code to the interfaces of those types and you can then pass in any concrete type that implements the interface. If that doesn't make sense, I will try to clarify. Good luck!

Collapse
marekmosiewicz profile image
Marek Mosiewicz

The problem is different. You can pass bigInterfaceImpl as smallInterface into db and cast into bigInterface. I think your problem is how to switch between big and small interfaces. When you have new BigSmallInterfaceImpl() in code you can not change it. There is when Spring Framework comes. Spring can be heavly overused and in many cases and it could be just factory pattern be better approach:
Factory.createInstance(MyInterface.class)
In factory you can take based on your environment you decide if you want to create mock, real db or yet something different implementation for given interface.

dmfay profile image
Dian Fay

@forstmeier this is also good advice, look into mock frameworks like Mockito. I don't know if there's anything more current.

Thread Thread
forstmeier profile image
John Forstmeier Author

My thoughts exactly with the dual constructors when I realized I couldn't set the inputs of the constructors to the same attribute within the class; creating "test vs prod" switches raises red flags for me.

Collapse
isaric profile image
Ivan Šarić

Wow. Just wow. Anything else would probably violate community standards.

Collapse
forstmeier profile image
John Forstmeier Author

Which standards do you believe are being violated?

Collapse
isaric profile image
Ivan Šarić

Ok, I see my comment was unclear. I'm saying if I wanted to give an honest response to your clickbaity titled article, I would be violating community standards.

Collapse
honzasterba_53 profile image
Honza Sterba

1) read the language docs

2) read something about API design, the fundamental flaw is that you have an *Impl class in your API, why not have a constructor that takes an Interface? Thats how testable programs are written

3) use Spock for testing (I hope you are taling about automated tests) that way you dont need to implement all the method and just do Mock(DynamoDbInterface) and implement the methods you need for testing

Collapse
honzasterba_53 profile image
Honza Sterba

also Java is a awesome language - once you really understand it, yes it has its downsides, but thats true for every language, if you are just learning Java I would suggest starting with Kotlin instead, much nicer to work with (not for you perhaps now, but in the long run yes)

Collapse
forstmeier profile image
John Forstmeier Author

1) Yep, this is a must.
2) I did have a constructor on the class that was accepting the DynamoImpl interface which only defined two methods. These two methods were identical to two methods in the much larger AmazonDynamoDB interface. The goal here was to be able to only have to provide two fake methods instead of 60 in testing which is where I ran into trouble because it wouldn't accept the larger AmazonDynamoDB interface in the Database constructor.
3) I'll definitely take a look!

Collapse
lluismf profile image
Lluís Josep Martínez

If AmazonDynamoDB is an instance of DynamoImpl it should work. Maybe you don't have a clue about Java?