Czytelność argumentów w konstruktorze a builder

0

Często przy tworzeniu obiektów nie widać dokładnie, co oznaczają argumenty jego konstruktora, np. new Product(1, 100, "book"). Jak sobie z tym najlepiej radzić? Jeśli użyjemy buildera, to już konstruktor nie powie nam, które argumenty są wymagane i tracimy sprawdzanie na poziomie kompilacji. Fluent builder jest czasochłonny w implementacji. Możemy wprowadzić jakieś value objecty/wrappery typu new Produkt(new Id(), new Quantity()) itd, ale wprowadza to sporo nowych klas i wygląda kiepsko. O setterach nawet nie będe wspominać. Przydałyby się named parameters, ale ich w Javie nie ma.

0

Odwieczny problem javy - brak named parametrów i wszechobecna nullowalność, więc pozostaje żyć z konstruktorami.

1

Możemy wprowadzić jakieś value objecty/wrappery typu new Produkt(new Id(), new Quantity()) itd, ale wprowadza to sporo nowych klas i wygląda kiepsko

W zasadzie to czemu wyglada kiepsko?
BTW mamy tu jakiegoś ewangeliste VO?

1

Pamiętam skądś ten dość upierdliwy w implementacji pattern, gdzie metoda buildera zwraca inny builder, i nie da się pominąć kroku https://stackoverflow.com/questions/1638722/how-to-improve-the-builder-pattern
Ale chyba wtedy musisz wykonać wszystkie kroki budowania w tej samej kolejności, co trochę psuje sens użycia tego wzorca. Pewnie dałoby się zrobić implementację z linka z dowolną kolejnością, ale ilość klas builderów byłaby gigantyczna i raczej nie jest to tego warte.

1

W intellij możesz włączyć pokazywanie nazw parametrów Settings > Editor > Inlay Hints, inne IDE też pewnie coś takiego mają.
Jak chcesz żeby było widać to też poza IDE, np podczas code review to możesz użyć np typed identifiers i opakowywać prymitywy w obiekty

0
obscurity napisał(a):

W intellij możesz włączyć pokazywanie nazw parametrów Settings > Editor > Inlay Hints, inne IDE też pewnie coś takiego mają.
Jak chcesz żeby było widać to też poza IDE, np podczas code review to możesz użyć np typed identifiers i opakowywać prymitywy w obiekty

Nie zawsze kod się czyta w IDE. poza tym nie wiem czemu, ale nie pokazuje niektórych argumentów
image

2

No to tak jak napisałem - typed ids i wrappery. Ewentualnie możesz jeszcze przekazywać obiekt typu "Parameters"

// prosty chwilowy data object z publicznymi fieldami do przechowywania parametrów (typowany "property bag")
// nie ma żadnego sensu w tym przypadku używać getterów / setterów choć wielu pewnie się przyczepiłoby na code review
FilmProps film = new FilmProps();
film.title = "Test film 1";
film.category = FilmCategory.COMEDY;
film.releaseYear = CURRENT_YEAR;
film.duration = Duration.ofMinutes(100);
return new FilmCreateDto(film);

Akurat chyba nie ma to sensu w tym przypadku bo równie dobrze można w ten sposób od razu FilmCreateDto i po prostu zrobić wszystkie własności mutowalnymi i używać setterów.

To też nie wymusi ustawienia wymaganych własności podobnie jak zwykły builder. Jest jeszcze "step builder" ale to dla mnie przesada i mnóstwo kodu żeby minimalnie polepszyć czytelność.

Chyba nie ma innej rady. Jeszcze możesz zmienić język, java jest z tego co mi się wydaje na prostotę i mały próg wejścia, nie ma w niej praktycznie żadnego cukru składniowego ani niczego co uprzyjemnia pracę lub polepsza wygląd kodu jeśli tylko da się to już zrobić w inny sposób. Podobnie jak python w zamyśle miała być prosta żeby przyciągnąć jak najwięcej ludzi i zmniejszyć ich koszt. W połowie wyszło

2

Wśród dyskutowanych ulepszeń: zejść z magicznych liczb np na rzecz enumów (zależnie od sytuacji)
Zamiana 11,12 na new Point(11,12)

Co do dyskutowanych builderów: racjonalnie mały builder - tak. Mega builder krawaty wiąże usuwa ciąże traci to ekonomię.
To się właśnie kończy "w połowie wyszło".

2
obscurity napisał(a):

No to tak jak napisałem - typed ids i wrappery. Ewentualnie możesz jeszcze przekazywać obiekt typu "Parameters"

// prosty chwilowy data object z publicznymi fieldami do przechowywania parametrów (typowany "property bag")
// nie ma żadnego sensu w tym przypadku używać getterów / setterów choć wielu pewnie się przyczepiłoby na code review
FilmProps film = new FilmProps();
film.title = "Test film 1";
film.category = FilmCategory.COMEDY;
film.releaseYear = CURRENT_YEAR;
film.duration = Duration.ofMinutes(100);
return new FilmCreateDto(film);

Akurat chyba nie ma to sensu w tym przypadku bo równie dobrze można w ten sposób od razu FilmCreateDto i po prostu zrobić wszystkie własności mutowalnymi i używać setterów.

Ciekawy pomysł. Chyba nie próbowałem. Ale są jednak zalety w porównaniu do setterów:

  • W tym rozwiązaniu stan jednak zamrażasz w momencie wywołania konstruktora. A potem jest już immutable
  • W konstuktorze możesz przeprowadzić walidację. Nie potrzebujesz sztucznego "postkonstruktora" żeby to zrobić
0
Nofenak napisał(a):

poza tym nie wiem czemu, ale nie pokazuje niektórych argumentów
image

Nazwy pól IDE pokazuje gdy przekaże się literał (goły string / enum / liczbę), a nie pokazuje, gdy przekazujemy zmienną.

0

Jest bardzo dużo opcji:

  1. Dodać builder (mimo że może być nie potrzebny)
  2. Skonfigurować IDE tak żeby podpowiadało argumenty
  3. Przesiąść się na inny język, taki który ma named arugments albo initialization list
  4. Dodać meta-język jak np lombok który generuje fluent builder
  5. rozdzielić parametry tak żeby były obiektami, których typy powiedzą jak to się dzieje. Nie musi to być VO, bo do tych obiektów można wsadzić logikę - to byłoby najbardziej obiektowe podejście. "Gołe" VO nie są obiektowe.
  6. Dodać explanatory variables
    int quantity = 4;
    return new Person(quantity);
    

Ja pewnie wybrałbym 5.

1

Do tworzenia parametrów konstruktora możesz wykorzystać rekordy:

public class Book {

    public final String title;
    
    public final String author;
    
    public final int pages;

    public Book(Author author, Title title, Pages pages){
        this.pages = pages.pages;
        this.title = title.title;
        this.author = author.author;
    }

    public record Title(String title){}

    public record Author(String author){}

    public record Pages(int pages){}
}
...
    public static void main(String[] args) {
        Book book = new Book(
                new Book.Author("Joshua Bloch"),
                new Book.Title("Effective Java"),
                new Book.Pages(408)
        );
    }
0

@cs:

Czytam kod i rozumiem ... ciekawe, ale nie wiem czym bym się takiego podjął ...
Dużo kodu, dużo klas na inspektorach i w JVM ... hmmm się wydaje mało się opłaca
... może ... o ile gdzieś te recordy by pracowały np w filtrach ...

1

A jeszcze jest najprostsze rozwiązanie o którym nie wiem czemu nikt nie wspomniał, a które w dodatku jest szeroko stosowane i widoczne choćby w starych codebasach Microsoftu zanim C# dostał named arguments (13 lat temu :> ); przedstawiam państwu najzwyklejsze w świecie - komentarze

return new FilmCreateDto(
   "Test film 1", /* title */
  FilmCategory.COMEDY,
  CURRENT_YEAR, /* release date */
  Duration.ofMinutes(100)
);

Uważam że to najrozsądniejsze rozwiązanie bo nie trzeba pisać dodatkowych bzdurnych klas, jedynie informacja może zostać zduplikowana dla tych którzy mają włączone w IDE hinty

0
cs napisał(a):

Do tworzenia parametrów konstruktora możesz wykorzystać rekordy:

public class Book {

    public final String title;
    
    public final String author;
    
    public final int pages;

    public Book(Author author, Title title, Pages pages){
        this.pages = pages.pages;
        this.title = title.title;
        this.author = author.author;
    }

    public record Title(String title){}

    public record Author(String author){}

    public record Pages(int pages){}
}
...
    public static void main(String[] args) {
        Book book = new Book(
                new Book.Author("Joshua Bloch"),
                new Book.Title("Effective Java"),
                new Book.Pages(408)
        );
    }

Ciekawy pomysł z tymi recordami

0

Co do hint'ów, to sterowanie podpowiedziami jest w ustawieniach edytora IntelliJ:

screenshot-20230721225401.png

W efekcie mamy podpowiedzi dla parametrów w postaci zmiennych (tylko, gdy zmienne mają inną nazwę od parametrów):

screenshot-20230721225807.png

5

@Nofenak: w pierwszym poście rzuciłeś trzy mity. Po pierwsze builder nie wyklucza „pełnoparametrowego” konstruktora ani walidacji w metodzie build. Po drugie, Fluent builder nie jest czasochłonny w implementacji. Po trzecie, Value Object jest dobrym wzorcem, który pozwala na dodanie walidacji. Ilość klas nie jest problemem, bo kompensujesz to szybkością pracy i czytelnością kodu. Jeżeli twoje VO są tworzone na jedno kopyto, to nie musisz nawet zbytnio się zastanawiać co tam się w środku dzieje. Po prostu zamiast:

record Person(int age, String name, String email){}

class Main{

	void main(){

		new Person(10, "Jaś", "jas@las");

	}
}

napiszesz:

record Age(int age){
	static Age age(int age){
		return new Age(age);
	}
}
record Name(String name){
	static Name name(String name){
		return new Name(name);
	}
}
record Email(String email){
	static Email email(String email){
		return new Email(email);
	}
}

record Person(Age age, Name name, Email email){}

class Main{

	void main(){

		new Person(age(10), name("Jaś"), email("jas@las"));

	}
}

I w tym kodzie można dodawać w nieskończoność walidacje, źródła danych, czy kombinować ze składaniem obiektu z różnych serwisów. Nawet kombinacyjne buildery nie są aż tak trudne, o ile wiesz, co chcesz uzyskać.

Linki:
https://koziolekweb.pl/2011/11/06/ekstremalna-obiektowosc-w-praktyce-%e2%80%93-czesc-3-opakowuj-wszystkie-prymitywy-i-stringi//
https://koziolekweb.pl/2012/01/14/ekstremalna-obiektowosc-w-praktyce-czesc-8-opakowywanie-kolekcji-w-klasy-specyficzne-dla-kontekstu-wykorzystania/
https://koziolekweb.pl/2023/06/02/wzorce-projektowe-inaczej-budowniczy/

0

@Koziołek: czyli w zasadzie, Java zatoczyła pełne koło. Onegdaj się robiło gettery/settery, które jak wiadomo, zaciemniały kod i obiekty były mutowalne. Czasem są niezbędne - choćby w JPA, mniejsza o to. Powstał Lombok, który w zasadzie też zaciemniał kod, bo to był annotation driven development, ok. Od Javy 7 się przyzwyczaiłem, że to jest standard. W końcu w Javie 14 czy 15, powstały recordy, które miały ułatwić sprawę. I teraz pokazujesz, że w zasadzie ten cały trud jest na marne, bo każdego recordu, wg. wzorca VO, trzeba stworzyć osobne recordy, które zaciemniają kod, znowu... mam mindfuck

0

@Pogodny Żniwiarz: gettery i settery w javie nigdy nie miały sensu. Ja w takich przypadkach normalnie używam pół publicznych.

0

@Riddle: coo? xD
pewnie, najlepiej wszystko zrobić global static :D przecież cały koncept OOP się przez cos takiego sypie, bo klasy są wtedy niepotrzebne

0
  1. Jeśli masz jeden DTOs gdzie zakładasz że możesz wypełnić nie wszystkie pola to może to nie powinen być 1 DTOs?
  2. Jeśli chcesz zezwolić na nullowalność i chcesz dla danego wariantu zezwolić na konkretne nulle to możesz:
    • dostarczyć metody statyczne odpowiadająće konkretnemu zestawowi pól i rzucać wyjątek gdy któregoś parametru nie podasz
    • albo dostarczyć buildery i w metodzie build() to samo jw
2
Pogodny Żniwiarz napisał(a):

@Riddle: coo? xD
pewnie, najlepiej wszystko zrobić global static :D przecież cały koncept OOP się przez cos takiego sypie, bo klasy są wtedy niepotrzebne

Jeśli chcesz implementować interfejs, który ma np Counter { getCount(); } to wiadomo że możesz dodać taką funkcję. Ale jeśli getter i setter jedyną rolę jaką pełni to dostęp do prywatnego pola, to czemu to pole miałoby nie być publiczne?

Właśnie przez takie przekonanie że "to jest złe", rodzą się takie absurdy jakie są opisane wyżej.

1

@Riddle:
całe moje zażalenie to, żeby mieć coś sensownego w Javie napisane, to jest w zasadzie dopisanie kolejnego boilerplate code. Nie chodzi mi o to, że rozwiązanie które podał @Koziołek jest bez sensu - właśnie jest bardzo konkretne i sensowne.

Natomiast to co Ty napisałeś, jest zupełnie bez sensu. Sama konstrukcja record w Javie miała załatwić kilka spraw, choćby pozbyć się mutowalności, czyli setterów i poprawić hermetyzację. Bo gettery i tak są potrzebne. To co napisałeś, to w zasadzie nawet nie jest przepis na POJO, tylko podręcznikowe "Nie rób w ten sposób"

0
Pogodny Żniwiarz napisał(a):

Natomiast to co Ty napisałeś, jest zupełnie bez sensu. Sama konstrukcja record w Javie miała załatwić kilka spraw, choćby pozbyć się mutowalności, czyli setterów i poprawić hermetyzację. Bo gettery i tak są potrzebne. To co napisałeś, to w zasadzie nawet nie jest przepis na POJO, tylko podręcznikowe "Nie rób w ten sposób"

Mutowalność możesz ogarnąć dodając final w takiej klasie.

Recordy w javie (wiele innych) są prezentowane jako coś nowego i odejście od starej konwencji, ale to jest old new. Odnośnie pół w recordach, to to samo można osiągnąć w starej javie, tylko wydane w trochę inny sposób. Jasne, że recordy są kapkę lepiej wydane, ale nie są czymś drastycznie innym niż klasa z publicznymi polami final.

1

w klasie tak, w recordzie nie. Taka jest różnica. Jakbym pisał w Javie 5, to może bym się skłonił do Twojego rozwiązania, choć wolałbym zachować ogólnie przyjęty standard i robił pola prywatne z getterami

0
Pogodny Żniwiarz napisał(a):

w klasie tak, w recordzie nie. Taka jest różnica. Jakbym pisał w Javie 5, to może bym się skłonił do Twojego rozwiązania, choć wolałbym zachować ogólnie przyjęty standard i robił pola prywatne z getterami

Po to żeby potem pisać:...

Pogodny Żniwiarz napisał(a):

Onegdaj się robiło gettery/settery, które jak wiadomo, zaciemniały kod i obiekty były mutowalne.

? :>

Serce problemu. Dodawanie setterów i getterów do pól prywatnych bez sensu, tylko po to żeby były i bez specjalnego powodu ku nim.

0

no to pomyliły Ci się kolejności postów :)

poza tym, getterów/setterów się nie pozbędziesz jak chcesz używać JPA/Hibernate, bo się nie da, ORM płacze :) i choćby w tym wypadku trzeba zaklepać POJOsa.
a jak chcesz korzystać z recordów, to jest coś takiego jak JavaEbean. I nawet w dokumentacji napisali, że pod spodem biblioteka tworzy z recordów POJOsy. :)

4
Pogodny Żniwiarz napisał(a):

@Koziołek: czyli w zasadzie, Java zatoczyła pełne koło. Onegdaj się robiło gettery/settery, które jak wiadomo, zaciemniały kod i obiekty były mutowalne. Czasem są niezbędne - choćby w JPA, mniejsza o to. Powstał Lombok, który w zasadzie też zaciemniał kod, bo to był annotation driven development, ok. Od Javy 7 się przyzwyczaiłem, że to jest standard. W końcu w Javie 14 czy 15, powstały recordy, które miały ułatwić sprawę. I teraz pokazujesz, że w zasadzie ten cały trud jest na marne, bo każdego recordu, wg. wzorca VO, trzeba stworzyć osobne recordy, które zaciemniają kod, znowu... mam mindfuck

No nie :) VO to nie jest taki prymitywny stworek, gdzie masz setter i getter. Po pierwsze w tym moim kodzie, to jest trochę nakombinowane. Można prościej:

record Age(int age) {}

record Name(String name) {}

record Email(String email) {}

record Person(Age age, Name name, Email email) {}

class Main {

	void main() {

		new Person(new Age(10), new Name("Jaś"), new Email("jas@las"));

	}
}

Metody statyczne służyły tylko do zrobienia z nich pseudo named params z wykorzystaniem statycznych importów. Po drugie uciąłem walidację. Po trzecie, wzorzec VO rzeczywiście produkuje dużo dodatkowych klas, ale dzięki temu masz „dziwnych” testów, ewentualnie są one lepiej zorganizowane. Tutaj porównaj kod do JSa, gdzie argument typu numerycznego testuje się też z innymi typami, żeby mieć pewność, że nie wysadziłeś czego. VO w javie, to przeniesienie testów na poziom kompilacji.

Problem z getterami i setterami polegał na tym, że dawno temu była sobie specyfikacja Java Beans, która służyła jako baza dla wielu innych specyfikacji JEE. Efekt był taki, że takie oto wymaganie:

Properties are always accessed via method calls on their owning object. For readable properties
there will be a getter method to read the property value. For writable properties there will be a
setter method to allow the property value to be updated.

Zaczęło rozpleniać się wszędzie i bez pomysłu. Więcej o tym tutaj.

Jak VO daje ci silne typowanie, tak rekordy pozwalają na minimalizację kodu. Można sobie napisać:

record Age(@Min(0) @Max(120) int age) {}

record Name(@Pattern(regexp = "[A-Z]{1}[a-z]*") @NotNull String name) {}

record Email(@jakarta.validation.constraints.Email @NotNull String email) {}

record Person(@Valid @NotNull Age age, @Valid @NotNull Name name, @Valid Email email) {}

I masz już całą walidację. Czy to „zaciemnia kod”? Nie bardziej niż wykorzystanie typów prostych czy Stringa, gdzie musisz wykonać sprawdzenie ręcznie, lub przez adnotacje.

Cała ta zabawa dąży do uzyskania kodu, który będzie łatwy w użyciu, gdy zaczynasz bawić się DDD, czy architekturą heksagonalną.

0

@Koziołek: ok, dałbym kciuka, ale jeszcze nie mogę :)

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.