Annotation suggestion - @Adapter

90 views
Skip to first unread message

Zoltán Csorba

unread,
May 26, 2024, 1:36:48 PMMay 26
to Project Lombok
There are some cases, when you need to write a class that implements a certain interface, but you don't need all of the interface methods, then either you leave the class abstract, or use some Proxy wrapper or let the IDE create all the methods with an empty body. The latter one is not so nice, as there are many empty method blocks, or TODO comments, that do not look good or need some manual effort to clean them up. Also, these methods are not doing anything, so they are not worth the time to cover them with tests. That leads to "boilerplate" code, that obscures the code.

This often happens, when you need to write some test doubles (mocks) or using the nullables design pattern for creating simple implementations that can be injected instead of the "real" dependencies.
In the first case, usually a mocking framework is the right solution, but even if that's available in some cases it's better to implement a simple and straightforward mock class, instead of setting up the expected answers using the mock framework itself.
In case of the nullables, the mocking framework is not a good option, as the nullable implementation is part of the production code (like a dry-run option), so the usage of test framework dependencies is discouraged.
In both cases, the main focus is on those specific methods that are significant for the business logic at hand, and all the other interface methods could be disregarded.
For example, the service injects a DAO component with many general methods provided by the framework (e.g. Spring), but the service only needs the findByNameAndExpirationDate method and nothing else. Then the "nullable" object should implement only this method, but nothing else from the base DAO interface.

My suggestion is a lombok annotation with the following features.
Name:
      @Adapter
Attributes:
  •   of = interface(s)
    Optional attribute to control which interface's methods should be generated.
    As a default behaviour, all the abstract methods of given class should be auto-generated by lombok.
  • throwException = Exception class
    Optional attribute. If not specified, then the default implementation does nothing (void method) or returns a default value according to the method declaration (0, false, null, empty collection, etc).
  • message = Exception message
    Optional attribute as an additional setting for the throws option.
  • suppressThrows = true/false (default: false)
    The generated methods are replicating the same throws declaration as the interface declares them. In case of 'suppressThrows = true' the generated method overrides are omitting these throws declarations. This makes sense in most cases as the default methods won't throw any error.
"Adapter" is a common phrase for classes that provide default implementations for some interfaces and can be used for implementing simple custom adapters by extending them. Maybe the most known example is the EventListener and EventAdapter, where you only need to extend those onX methods that are relevant for a given case. But in most cases the interfaces have no Adapter implementations out of the box.

Additional notes:
The interface may declare default method behaviour, these don't need auto-generated overrides.
Basically, the @Adapter annotation is aiming fix the "unimplemented methods" compilation error, by providing the overriding methods with the necessary default method behaviour.

Code samples.

Let's assume we have this interface declared.

public interface FooInterface {
   void doStuff();
   int countMyObjects(User user) throws UserNotFoundException;
   String getStatus() throws IllegalStatusException;
}

Adapter with default settings:

@Adapter
public class NullableFooInterface implements FooInterface {
  @Override
  public String getStatus() {
    return "TestStatus";
  }
}

That creates the following code:

public class NullableFooInterface implements FooInterface {
  @Override
  public String getStatus() {
    return "TestStatus";
  }
  @Override
  public void doStuff() {
    // empty method
  }
  @Override
  public int countMyObjects(User user) throws UserNotFoundException {
return 0;
  }
}

With custom adapter settings:

@Adapter(of = FooInterface.class, throws = UnsupportedOperationException.class, message = "NOT_IMPLEMENTED", suppressThrows = true)
public class NullableFooInterface implements FooInterface {
  @Override
  public String getStatus() {
    return "TestStatus";
  }
}

That creates the following code:

public class NullableFooInterface implements FooInterface {
  @Override
  public String getStatus() {
    return "TestStatus";
  }
  @Override
  public void doStuff() {
throw new UnsupportedOperationException("NOT_IMPLEMENTED");
  }
  @Override
  public int countMyObjects(User user) {
throw new UnsupportedOperationException("NOT_IMPLEMENTED");
  }
}

What do you think? Could this be useful for other use cases as well?

Zoltan

Marco Servetto

unread,
May 26, 2024, 3:41:06 PMMay 26
to project...@googlegroups.com
Having the auto generated methods throw error should be the default.
Please never 'return zero' or 'null' automatically. It is the source
of plenty of bugs!
> --
> You received this message because you are subscribed to the Google Groups "Project Lombok" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to project-lombo...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/project-lombok/6a7d44af-783d-4e4f-91d1-d9b6e32435den%40googlegroups.com.

Csorba Zoltán

unread,
May 27, 2024, 4:28:45 AMMay 27
to project...@googlegroups.com
Hi Marco, thanks for the reply.
I was also thinking, that throwing an error could be the default, but it depends on the use case.
For the default exception, the UnsupportedOperationException is a reasonable choice in my opinion. It's easy to understand even without an error message. But both would be customizable with the annotation attributes.

For the other option, when the generated methods won't throw an error, but silently doing nothing, then the default return values should be as simple as possible.
In my opinion, the "zero, null and empty" are good options. My reasoning is that these auto-generated methods should avoid additional complexity and ambiguity. The following mapping could be sufficient:
* primitive types: default primitive value
  e.g. 0 for an int, 0L for a long, '0' for a char, false for a boolean, and so on
* String: null
  could be an empty string, but that's not the default in Java
* collections: empty collection
  list, set, map, and so on, including the sorted versions
  the only question is the mutability, i.e. should it be "return new ArrayList<>()" or "return Collections.emptyList()" instead?
  I think, the immutable collections are a better option, for the auto-generated methods. If anything else is needed, then those methods should be manually implemented.
* Optional: Optional.empty()
* arrays: empty array
* any other object type: null
  that seems to be the easiest and safest option for the code generator

Let's say that there is a "silent" attribute of the @Attribute annotation. It's default value is 'false', meaning that the generated methods are throwing an exception. The silent=true means that the generated methods are returning default values as described above and the void methods are silently not doing anything at all.

The original intention with the @Adapter annotation is to eliminate the boilerplate code from the source files. So, it should work like the IDE when you create a new class and instruct the IDE to generate methods for any implemented interface methods. The only difference, that in this case those methods won't clutter the code base.


Mat Jaggard

unread,
May 27, 2024, 4:32:59 AMMay 27
to project...@googlegroups.com
This is not something I would use personally but I have in the past worked with some really frustrating interfaces that have loads of methods that just don't need to be there. Most however, like AWT/Swing, have "Adapter"(sic) classes already provided that let you override only the methods you're interested in. Ultimately this is a failure of interface design but frequently the users of those interfaces don't have any choice so it would get my vote as a "maybe".

On Sun, 26 May 2024 at 20:41, Marco Servetto <marco.s...@gmail.com> wrote:

Csorba Zoltán

unread,
May 28, 2024, 4:31:30 AMMay 28
to project...@googlegroups.com
Thank you, Mat. I appreciate your "maybe" and that it can be useful in some cases.

The AWT/Swing event listener and adapter example might have been a mistake from me as it's misleading. That is not something I am using at the moment.
And this Adapter annotation is not for functional interfaces or small ones with a handful of methods only. I assume those are the well designed interfaces you're thinking of.

Maybe a better example would be the Spring repository interface where you got many methods out of the box. 
Or APIs to public services, where you might get dozens of methods in a single interface declaration, but no adapter is coming with it.
And the list could go on.

Anyhow, I am looking around in the codebase and checking how this could be implemented. It seems quite straightforward, but will see.



Mat Jaggard

unread,
May 28, 2024, 4:45:01 AMMay 28
to project...@googlegroups.com
Got you. I don't think I'd consider Spring Repositories or clients for other services to be badly designed, I just hadn't thought of them as a use case for this. Am I right in thinking that this would be used most commonly in test code? When you want a fake version of an interface to pass into the class under test and you're using enough methods that Mockito gets unwieldy but don't want to have to implement every method because there are a lot more of them that you don't use than you do?

Csorba Zoltán

unread,
May 28, 2024, 4:33:58 PMMay 28
to project...@googlegroups.com
Yes, my main intention for this is using for testing purposes, i.e. test doubles.
I do use mock frameworks. At the moment it's EasyMock, though I like Mockito better 
Last year I heard about the Nullables methodology from James Shore, and successfully used that for multiple Java projects. Partially implemented interfaces are a common use case for that methodology.

But even with EasyMock/Mockito or other frameworks, it's often better implementing some simplified test doubles instead of setting up the mocks in each case to simulate a certain behaviour.


Zoltán Csorba

unread,
Jun 7, 2024, 3:55:16 AMJun 7
to Project Lombok
Well, I was wrong about thinking that implementation would be "straightforward" :)
But it was not so hard since the @Adapter is similar to the @Delegate annotation, just less complex.
So, I used the same approach as the Delegate, i.e. created a PatchAdapter for the eclipse agent and the HandleAdapter for the javac handler.

I'm going to create the PR and waiting for your feedback.

Zoltán Csorba

unread,
Jun 7, 2024, 3:55:30 AMJun 7
to Project Lombok
Well, I've managed to implement the JavacAnnotationHandler for the Adapter annotation. It looks quite good, but now I've realized that the EclipseAnnotationHandler requires a separate implementation too.
So, I need some more learning and playing around to figure out how that works. It would be nice to have a layer of abstraction that could hide the differences so that only one implementation was necessary.

On Tuesday 28 May 2024 at 21:33:58 UTC+1 Zoltán Csorba wrote:
Reply all
Reply to author
Forward
0 new messages