Przy okazji jednego ze szkoleń, tym razem głównie opartego o C#, moją uwagę zwrócił interesujący zapis wywołań funkcji. Otóż wywoływana metoda nie była zdefiniowana w klasie, z której ją wywoływano. Nie była tez zdefiniowana w żadnym z rodziców tej klasy. Krótki przegląd przewodnika na stronie Microsoft wyjaśnił tę zagadkę. Okazało się, że są to tzw. metody rozszerzeń (ang. extension methods). Sam mechanizm wydaje mi się całkiem interesujący, stąd od razu pojawiło się u mnie pytanie. Czy w łatwy sposób osiągnę podobne zachowanie w Java, czy Extension Methods okażą się tu też możliwe do zrealizowania?

Extensions Methods w C#
Zacznijmy więc od przykładu podobnego do tego z przewodnika:
using Itbrains.Samples.Documents;
...
string s = "Hello Extension Methods";
int i = s.CountWords();
Wywołana metoda CountWords() w zaznaczonej linii czwartej nie jest oczywiście metodą zdefiniowaną w klasie String. W jej rodzicu również takiej definicji nie ma.
Dodatkowo, warto zwrócić uwagę na to, że klasa String w C# jest klasą sealed, czyli nie podlega dziedziczeniu. Podobnie jak final w Java.
Jak to się jednak dzieje, że wywołanie tej metody działa i kompilator nie zgłasza błędów?
Przyjrzyjmy się zatem miejscu, gdzie ta magia jest ukryta.
namespace Itbrains.Samples.Documents
{
public static class Extensions
{
public static int CountWords(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
Powyżej, w linii 5. widzimy przykład tworzenia metody rozszerzającej w C#. Tak zdefiniowana metoda CountWords(this String str) pozwala na wprowadzenie do typu String nowej funkcjonalności.
W C# skorzystać z tego rozszerzenia będzie można, jeśli w naszym programie w danym miejscu dołączymy naszą przestrzeń nazwy (jak import w Java) poprzez:
using Itbrains.Samples.Documents;
Rozwiązanie jest proste i wydaje się mieć kilka ciekawych zastosowań. Przejdźmy więc do Javy.
Extension Methods w Java
Na wstępie rzeczy oczywiste. Java nie wspiera mechanizmu podobnego do Extension Methods w standardzie.
A jeśli czegoś nie wspiera w standardzie, a jest to coś całkiem fajnego, to z dużym prawdopodobieństwem będziemy do tego mieli jakieś biblioteki.
Lombok na ratunek
Pierwsza myśl i od razu strzał w dziesiątkę, czyli Lombok.
Lombok zawiera magiczną adnotację @ExtensionMethod, która pozwala na tworzenie metod rozszerzających. Opis tej funkcji jest też dość ciekawy:
Annoying API? Fix it yourself: Add new methods to existing types!
@ExtensionMethod
Na warsztat weźmy prostą klasę, która reprezentuje dokument z danymi klienta w Mongo, który trzymam w warstwie infrastruktury.
@Data
@Document(collection = "customers")
public class CustomerDocument {
@Id
private String id;
private String status;
private String name;
private String nip;
private String email;
}
Jest to prosta klasa z danymi. Na potrzeby zapytania REST chciałbym tę klasę przekształcić do postaci mojego DTO z warstwy aplikacyjnej:
@Data
@Builder
public class CustomerDto {
private String id;
private String extendedName;
private String status;
}
Dlatego też dodałem klasę, w której zdefiniowałem sobie moje rozszerzenie. Jak widać, wewnątrz jest to zwykła metoda statyczna.
public class Extensions {
public static CustomerDto asDto(CustomerDocument document) {
return CustomerDto.builder()
.id(document.getId())
.status(document.getStatus())
.extendedName(document.getNip() + " " + document.getName())
.build();
}
}
Warto tutaj podkreślić, że pierwszy argument metody statycznej pełni rolę odpowiednika this przy wywołaniu.
I teraz przechodzimy do obsługi mojego zapytania.
@ExtensionMethod({Extensions.class})
public class GetCustomerHandler {
private CustomerReadRepository repository;
...
public CustomerDto get(String id) {
CustomerDocument document = repository.get(id);
return document.asDto();
}
}
GetCustomerHandler jest handlerem w warstwie infrastruktury. Obsługuje on moje zapytanie o klienta na podstawie jego identyfikatora.
W linii 1. widzimy wykorzystanie adnotacji @ExtensionMethod. To dzięki niej Lombok w tym miejscu zastosuje metodę rozszerzającą. W linii 7 z kolei widzimy wywołanie tej metody.
Osiągnęliśmy zatem założony cel. Wywołanie metody jest eleganckie, a przy tym sama metoda jest oddzielona w kodzie od tej klasy.
Minusy zastosowania Lombok
Zastosowanie Lombok do wprowadzenia w Java Extension Methods ma też kilka drobnych minusów:
- Adnotacja @ExtensionMethod jest cały czas w pakietach eksperymentalnych. W dokumentacji możemy przeczytać, że generalnie w nich pozostanie, ale raczej nie zmieni formy.
- W Intellij Idea nie będzie nam działać auto-completion, a przez to nasze IDE będzie podświetlało wywołanie metody w edytorze na czerwono. Nie każdemu przypadnie to do gustu.
Alternatywne podejścia
Oczywiście, ktoś mógłby powiedzieć, że właściwe można przecież skorzystać z klasy Extensions i tej metody statycznej bez całej magicznej otoczki.
Tak to prawda, ale … Liczy się jednak kontekst. Nie chciałbym zaśmiecać prostej klasy reprezentującej obiekt w strukturze Mongo dodatkowymi metodami w jej kodzie. Ponadto dzięki zastosowaniu adnotacji mam kontrolę, gdzie chcę skorzystać z metod rozszerzających.
Mógłbym również skorzystać z mappera typu MapStruct. Ale podobnie jak akapit wcześniej, zależy mi na kontekście wywołania.
Kiedy stosować
Kilka przypadków:
- kiedy chcemy dodać metody do istniejących klas, m. in. tych oznaczonych final, np. String,
- kiedy chcemy w elegancki sposób ukryć funkcje specyficzne dla danej warstwy w naszym programie.
Alternatywne biblioteki do skorzystania z Extension Methods w Java
Lombok jest jedną z bibliotek, które pozwalają nam na rozszerzanie funkcjonalności języka.
Bardzo ciekawie wygląda w tym przypadku również Manifold. W tym przypadku, zamiast procesora adnotacji, dostajemy plugin do kompilatora javac. Tutaj link do Extension Methods w Manifold.
Na marginesie Manifold jest jednym z mocnych kandydatów do moich kolejnych projektów, także będzie okazja do zbadania tego rozwiązania nieco szerzej.
Na zakończenie
Eleganckie i proste. Czego chcieć więcej?
Sama Java nie wspiera Extension Methods, ale widzimy, że możemy takie mechanizmy wprowadzić do języka dzięki bibliotekom.
Chciałbym dowiedzieć się, co Ty myślisz na ten temat?
A może masz jakieś pytania?
Zostaw komentarz poniżej 😀
Użycie lomboka w celu rozszerzenia metod to bardzo słabe rozwiązanie.
A czy uzasadniłbyś dlaczego tak uważasz?