Wątek przeniesiony 2021-11-09 18:51 z PHP przez somekind.

Czy korzystacie z jakichś generatorów kodu (poza frameworkowymi)?

1

Obecnie większość frameworków typu Symfony, Laravel czy Magento posiada różne generatory kodu (np. maker dla Symfony) . Czasami są to rozwiązania wbudowane we framework czasami tworzone przez społeczność (np. plugin PHPstorm do Magento). Rzadko jednak spotykam się z generatorami do samego PHP.

Dla przykładu w testach często potrzebujemy zbudować jakiś obiekt aby przetestować jedną z jego właściwości. Czyli np. mając obiekt user i chcąc przetestować czy działa logika biznesowa zabraniająca tworzyć userów poniżej 18 lat musimy stworzyć cały obiekt z dumy data. Czyli na przykład mamy:

class User
{
    public function __construct(private Name $name, private Age $age)
    {
    }
}

Pisząc test robimy coś takiego

    public function test_can_not_crate_underage_user()
    {
        $user = new User(Name::fromNative('jan_kowalski'), Age::fromNative(18));
        ...

    }

Zazwyczaj aby uniknąć tego problemu tworzy się specjalnie dla celów testowych builder z wartościami domyślnymi i wtedy można zrobić coś w stylu


class UserTest
{
    public function test_can_not_crate_underage_user()
    {   
        $user = (new UserBuilder())->withAge(15)->build();
        ...
    }

}

Taki generator sobie właśnie teraz piszę aby przetestować dodane w PHP 8 atrybuty, ale chciałem zerknąć czy czegoś takiego nie ma i o dziwo nic do PHP8 na szybko nie znalazłem. Co by potrafiło generować klasy, a gdy już istnieją dopisywać brakujące metody itp.

Może znacie lub używacie jakichś ciekawych rozwiązań tego typu - niekoniecznie do Builderów, ale też innych boilerplatów?

5

Ja nie korzystam z niczego poza wbudowanymi. W ogóle ciekawy temat.

0

Też mnie właśnie ten temat zastanowił, bo wprowadzenie atrybutów powinno dać sporo możliwości. Przykładowo moje implementacja dla builderów będzie pozwalała na coś takiego:

#[Builder('\\App\\Builder\\User', 'standard')]
#[Builder('\\App\\Tests\\Builder\\User', 'test')]
class User
{
    public function __construct(
        #[BuilderParam(defaultValue: 'jan_kowalski', builder: ['test'])]
        private Name $name,
        #[BuilderParam(defaultValue: 25, builder: ['test'])]
        private Age $age,
    ) {
    }
}

Czyli dodanie tutal atrybutów powoduje stworzenie (dla kodu powyżej) 2 klas builderów. Jednej dla użytku dla aplikacji, która nie ma wartości domyślnych, a druga "test" dla testów, gdzie będą predefiniowane wartości domyślne dodwane atrybutem BulderParam. Jeśli builder już istnieją to dodawane będą tylko brakujące metody, czyli nie będzie trzeba poprawiać istniejących testów po dodaniu jakiegoś parametru w konstruktorze.

4

używałam jakiejś biblioteki do generowania pdfów która wewnętrznie generowała klasy php, chyba phpreports to było ale nie jestem pewna

6

https://symfony.com/bundles/SymfonyMakerBundle/current/index.html
Maker to coś takiego? Jeśli dzięki temu generuję poprawny kod bez adnotacji i magii, to spoko. Lombok w Javie z kolei bazuje na adnotacjach, co ma kilka wad. Na przykład IDE potrzebuje wtyczki, żeby wiedzieć, że klasa ma jakieś metody. OK, niby każdy używa IDE, ale aktualizacje mogą coś namieszać.

0

@PerlMonk: mówiąc o adnotacjach miałeś na myśli adnotacje w klasach źródłowych, czy tych już wygenerowanych?

ja ogólnie nie mam nic przeciwko adnotacjom (czy teraz atrybutom) w kodzie dopóki nie przecinają warstw aplikacji. Przykładowo bardzo nie lubię adnotacji Doctrine w entity tu mamy przecięcie warstwy domenowej z warstwą persystencji - tu zazwyczaj we wszystkim co nie jest crudem przechodzę na pliki xml. Przykład taki jak powyżej, gdzie adnotacje/atrybuty używane są wewnątrz jednej warstwy moim zdaniem są ok. Czasami mam mieszane uczucia - np. adnotacje Route w kontrolerach (choć akurat w tym przypadku akceptuje ten kompromis).

3

Ja jestem zdania że jeśli cokolwiek jest na tyle przewidywalne, że można to wygenerować, to możnaby również zaprojektować prostszy interfejs który nie wymagałby czegoś takiego. Np nie wiem czy wiecie ale prawie każdy giga framework, jak Laravel, Rails, Django czy inny, który ma ten cały generator wielkiego projektu można postawić na jednym pliku w 10-15 linijek.

Ale za to używam snippetów podczas pisania, np mam snippet test który mi wkleja unit test razem z given/when/then. Sporo innych rzeczy, jak np override method czy implementacje property może robić IDE jednym skrótem.

3

@TomRiddle: trochę się nie zgodzę z Twoją wypowiedzią. Ideą wielu generatorów z którymi pracowałem nie jest wygenerowanie kodu końcowego, tylko stworzenie całego boilerplate za programistę, ale to programista potem taki kod uzupełnia, zmienia etc. Zgodziłbym się z tym co napisałeś, gdyby przyjąć założenie, że generator generuje ostateczną wersję kodu, ale rzadko tak jest w praktyce.

Weźmy znowu jako przykład ten mój nieszczęsny generator builderów. Pisanie builderów jest dla mnie nudne jak flaki z olejem - zwłaszcza gdy mówimy o takich prostych builderach używany UT. Zgadzam się, że da się je w dużym stopniu wygenerować automatycznie z poziomu Ide, ale jednak jest to sporo klikania i poprawek gdy np. w builderze używach konwencji withVariavle zamiast setVariable itd.

Generator w takim przypadku daje Ci kilka fajnych opcji:

  • w przypadku prostych builderów praktycznie wszystko masz w klasie źródłowej i nie musisz skakać między zakładkami
  • w razie zmian w kodzie klasy źródłowej możesz dopisać nowe metody (czy np. wybierając odpowiednie ustawienie buildera nawet nadpisać cały builder)
  • w klasie źródłowej masz jasną informację, że masz powiązany z nią builder używany w testach, więc dodając jakieś parametry od razu możesz puścić generator aby zmiana została uwzględniona w klasie buildera
  • jeśli masz potrzebę w kilku miejscach jakiejś bardziej zaawansowanej logiki to po wygenerowaniu możesz ją po prostu nadpisać.
  • możesz przypisać domyślne wartości, ale tylko w przypadku buildera testowego a nie produkcyjnego

Myślałem trochę nad tym problemem, bo zgadzam się, że generowanie boilerplate często jest takim "code smell", ale w wielu przypadkach ma to sens - zwłaszcza w takich jak powyżej, gdzie po prostu jest to punkt wyjścia do dalszego kodu.

Inne ciekawe przypadki stosowania to na przykład:

  • generator Entity dla Doctrine w maker bundle
  • generator klas w Magento (tam na przykład stworzenie nowego kontrollera w module wymaga dodania ustawień routingu, stworzenia bloku z logiką i pliku z widokiem - generator robi wszystkie te rzeczy za Ciebie i oszczędzasz 5 minut życia + ewentualne debugowanie literówek w konfiguracji
  • rożne generatory pliko migracji baz danych

Myślę, że generator mają swoją rację bytu jeśli stosuje się je z głową.

Co do reszty się zgadzam - sam również stosuję rzeczy typu live templates, refactoring z użyciem IDE, integracje UT z IDE itp.

1
hadwao napisał(a):

@TomRiddle: trochę się nie zgodzę z Twoją wypowiedzią. Ideą wielu generatorów z którymi pracowałem nie jest wygenerowanie kodu końcowego, tylko stworzenie całego boilerplate za programistę, ale to programista potem taki kod uzupełnia, zmienia etc. Zgodziłbym się z tym co napisałeś, gdyby przyjąć założenie, że generator generuje ostateczną wersję kodu, ale rzadko tak jest w praktyce.

No ja dokładnie o tym mówię. Skoro ten boilerplate, skoro jeśli jest on na tyle przewidywalny że można go wygenerować, to dlaczego nie zaprojektować czystszego API/prostszego interfejsu, w którego po prostu mógłbyś "użyć", bez niepotrzebnie verbose kodu, bez niepotrzebne skomplikowanych funkcji i klas.

Weźmy znowu jako przykład ten mój nieszczęsny generator builderów. Pisanie builderów jest dla mnie nudne jak flaki z olejem - zwłaszcza gdy mówimy o takich prostych builderach używany UT. Zgadzam się, że da się je w dużym stopniu wygenerować automatycznie z poziomu Ide, ale jednak jest to sporo klikania i poprawek gdy np. w builderze używach konwencji withVariavle zamiast setVariable itd.

Again, jeśli chcesz mieć tak niepotrzebnie skomplikowane API, że tak ciężko go użyć że potrzebujesz Buildera, to faktycznie możesz go generować. Ja jednak kiedy projektuje swoje aplikacje nigdy/prawie nigdy nie mam takiego skomplikowanego use'caseu, gdzie używanie go wymagałoby builderów, ergo, nie mam potrzeby też ich generować.

Więc dla mnie Twoja potrzeba "generowania" boilerplateów to bardziej objaw niż przyczyna.

  • w razie zmian w kodzie klasy źródłowej możesz dopisać nowe metody (czy np. wybierając odpowiednie ustawienie buildera nawet nadpisać cały builder)
  • jeśli masz potrzebę w kilku miejscach jakiejś bardziej zaawansowanej logiki to po wygenerowaniu możesz ją po prostu nadpisać.

Na te argumenty nawet nie odpowiem, bo jak nie używasz generatorów to te punkty masz za darmo.

Myślałem trochę nad tym problemem, bo zgadzam się, że generowanie boilerplate często jest takim "code smell", ale w wielu przypadkach ma to sens - zwłaszcza w takich jak powyżej, gdzie po prostu jest to punkt wyjścia do dalszego kodu.

Ma sens tylko wtedy kiedy jeśli masz boilerplate. Jeśli nie ma boilerplate'u, używanie generatorów nie ma sensu.

Inne ciekawe przypadki stosowania to na przykład:

  • generator Entity dla Doctrine w maker bundle
  • generator klas w Magento (tam na przykład stworzenie nowego kontrollera w module wymaga dodania ustawień routingu, stworzenia bloku z logiką i pliku z widokiem - generator robi wszystkie te rzeczy za Ciebie i oszczędzasz 5 minut życia + ewentualne debugowanie literówek w konfiguracji
  • rożne generatory pliko migracji baz danych

To wygląda jak zbyt duże przywiązanie do framework'a IMO.

Myślę, że generator mają swoją rację bytu jeśli stosuje się je z głową.

A ja myślę, że ich sens istnienia to obecność przesadnie roztrzepanego API; bo gdyby API było proste i zrobione z głową, to prościej byłoby go użyć po prostu, niż korzystać z generatora. Tak długo jak będziesz korzystał z niepotrzebnie skomplikowanych rozwiązań tak długo będziesz widział sens dla generatorów, ale to nie w nich jest problem.

PS: Wyjątkiem tutaj MOŻE byłoby wygenerowanie jakiegoś pliku z konfiguracją, np konfiguracja apache'a albo nginx'a. Ale do kodu w języku programowania, raczej nie. Po to przecież istnieją języki programowania, żeby móc wyrazić intencje i implementacje na odpowiednim poziomie abstrakcji. Jeśli Twój poziom jest zbyt wysoki (tak że nie możesz czegoś edytować), albo tak niski (że jest przesanie wyrazisty, do stopnia w którym możesz go wręcz wygenerować) to powinieneś go zmienić.

PS2: Mogę dać przykład żeby nie być gołosłownym.

Jeśli korzystasz ze swojego generatora np w taki sposób:

./generateSomething.sh name value type

To możesz też w swoim języku programowania zrobić taki element/funkcje/strukture/klase, którą mógłbyś użyć tak:

something(name, value, type)

i tak to oprogramować, żeby działało tak samo jak to wygenerowane. Jeśli potrzebujesz czegoś bardziej skoplikowanego, to możesz rozwinąć tą funkcję troszkę bardziej (czyli rozwinąć interfejs). Jeśli będziesz to rozwijał bardziej, to w końcu nic z tego nie zostanie i będzie Twój boilerplate tak jak chciałeś. Z tą różnicą, że jak np potem zechcesz zrezygnować z rozwiązania to po prostu sobie usuniesz something(name, value, type).

0
hadwao napisał(a):

@PerlMonk: mówiąc o adnotacjach miałeś na myśli adnotacje w klasach źródłowych, czy tych już wygenerowanych?

W klasach źródłowych. Generowane rozumiem, że nie potrzebują.

0

prostszego interfejsu, w którego po prostu mógłbyś "użyć", bez niepotrzebnie verbose kodu, bez niepotrzebne skomplikowanych funkcji i klas

@TomRiddle - czyli chcesz po prostu te wszystkie "śmieci" ukryć w klasie? W sensie że zamiast pisać te szablonowe linijki, to tworzysz jakiś obiekt tej klasy, a cały boilerplate się w niej zawiera? Dobrze rozumiem, czy chodzi o coś innego?

1
cerrato napisał(a):

prostszego interfejsu, w którego po prostu mógłbyś "użyć", bez niepotrzebnie verbose kodu, bez niepotrzebne skomplikowanych funkcji i klas

@TomRiddle - czyli chcesz po prostu te wszystkie "śmieci" ukryć w klasie? W sensie że zamiast pisać te szablonowe linijki, to tworzysz jakiś obiekt tej klasy, a cały boilerplate się w niej zawiera? Dobrze rozumiem, czy chodzi o coś innego?

Tak i nie.

To wymaga dobrej znajomości biblioteki z której korzystasz oraz języka programowania, ale ja się staram robić tak:

Dla przykładu teraz robię nowy edytor dla 4programmer, korzystam tam z biblioteki CodeMirror; pracuję właśnie nad dekoracją dla <b></b> oraz <u></u>. Znam domenę biznesową (edytor), i wiem że dla tego requirement wystarczy użyć API tak: highlight('b', {fontWeight: 'bold'}). Nie znam jeszcze implementacji, i nie wiem jak to zrobię, ale WIEM że nie potrzebuję do tego nic innego niż nazwa taga i style. (bo co innego?). Nie muszę zawierać żadnych dodatkowych informacji, więc zaczynam od takiego kodu. Następnie próbuję zaimplementować to co chcę korzystając tylko z highlight('b', {fontWeight: 'bold'}), i mogę w CodeMirror to zrobić albo Highlight.define(), albo ViewPlugin(), albo extensionem, albo pewnie na inne sposoby. Mam napisane pod to już unity testy, że jak wpiszę tekst "Witaj <b>cerrato</b> to dostanę taki i taki HTML z takim i takim stylem, więc implementacja tego już jest do wyboru.

Odpowiadając na Twoje pytanie, czasem się nie da tego uniknąć i ten "boilerplate" jest nieunikniony, tzn że dana bilioteka nie pozwala na użycie jej w sposób jaki chce bez oklikania wszystkiego, i wtedy niestety się kończy na "schowaniu boilerplate'a w klasie", jak to ująłeś. Ale nawet w takim przypadku ja tak robię, dlatego że wolę wszędzie w moim kodzie mieć to co chcę, czyli highlight('u', style) i nie zawierać w nim informacji o tym jak to się robi konkretnie, przez to że biblioteka jest na tyle zrypana że nie da się prościej. Poza tym, wtedy przynajmniej będzie to w jednym miejscu, a nie w tylu miejscach ile mam tagów.

Natomiast w innych, częstszych przypadkach da się ogarnąć to co chcę inaczej/prościej, i wtedy ta funkcja stanowi taką "nakładkę" która chowa konkretną implementację. W sumie, jako żę używam ich tak samo, i przechodzą takie same testy, to właściwie te dwa podejścia: "prostszy sposób" i "chowanie boilerplaye'a w kodzie" to jedno i to samo.

Ma oczywiście dodatkowe zalety, takie jak np:

  • Czytając mój kod, i widząc highlight('u', style) wiem co ja/autor miał na myśli i wiem co chciał zrobić. Widząc Highlight.define(), plugin do widoku albo extension nie wiem, mogę się tylko domyślać. Poza tym ryzykuję że ktoś mi ją zepsuje, bo np doda mi do tego highlight innych tagów, a ja chciałem żeby to było SRP.
  • Może teraz nie znam sposobu na zrobienie tego przez boilerplate, ale za tydzień znajde na to nowy sposób, wtedy przerobienie funkcji highlight() pod spodem będzie banalnie proste, a testy nadal będą przechodzić. Jest też case żę teraz się nie da, ale wyjdzie nowa wersja biblioteki w której się będzie dało. Wtedy bez problemu sobie to zamienię.

Podkreślam jednak, że ja to robię głównie po to żeby mój kod był czysty, przejrzysty i wyrażał intencje. To że mogę potem zrefaktorować implementację to dla mnie tylko dodatkowy bonus.

Kolega z postów wyżej pewnie skorzystałby z generatora do pluginów do widoków.

PS: Teraz w sumie jak tak na to patrzę na chłodno, to to trochę wygląda tak jakby podejście "generowania boilerplateów" to była chęć zrobienia z aplikacji jednej wielkiej konfiguracji, zamiast programu.

3

Czytając mój kod, i widząc highlight('u', style) wiem co ja/autor miał na myśli i wiem co chciał zrobić. Widząc Highlight.define(), plugin do widoku albo extension nie wiem, mogę się tylko domyślać. Poza tym ryzykuję że ktoś mi ją zepsuje, bo np doda mi do tego highlight innych tagów, a ja chciałem żeby to było SRP

No i jeszcze jedna ważna rzecz - wyciągając abstrakcję na poziom wyższy, jeśli za jakiś czas będziesz chciał zmienić bibliotekę do edytora, to podmieniasz implementację/zależności, ale sam sposób korzystania z całości pozostaje bez zmian.

Jakiś czas temu ktoś wrzucił na 4P (nie pamiętam, czy to była własna opowieść, czy podlinkowany wpis do jakiegoś artylułu/bloga) opis podobnej sytuacji. Mieli jakiś system, który był na sztywno spięty z jakimś silnikiem map. Nie było warstwy pośredniczącej - i to pięknie działało. Tylko zamiast zrobić jakieś opakowanie do komunikacji z mapami - coś w stylu map.getCoords() to walili bezpośrednio do API. I jak nagle dostawca nawigacji coś pozmieniał (o ile dobrze kojarzę to chodziło o moment, gdzie Google zaczęło wprowadzać opłaty z korzystanie z ich map, jeśli ruch przekraczał jakąś skalę), aplikacja przestała działać, a całe przerobienie, zamiast (względnie) prostego podmienienia czegoś wewnątrz klasy pomocniczej, sprowadziło się do doraźnego grzebania w kodzie, który sobie bezpośrednio gadał z mapami, a docelowo - poważny refactor. A dałoby się tego uniknąc, jakby od razu wprowadzili pośrednika.

0
cerrato napisał(a):

Czytając mój kod, i widząc highlight('u', style) wiem co ja/autor miał na myśli i wiem co chciał zrobić. Widząc Highlight.define(), plugin do widoku albo extension nie wiem, mogę się tylko domyślać. Poza tym ryzykuję że ktoś mi ją zepsuje, bo np doda mi do tego highlight innych tagów, a ja chciałem żeby to było SRP

No i jeszcze jedna ważna rzecz - wyciągając abstrakcję na poziom wyższy, jeśli za jakiś czas będziesz chciał zmienić bibliotekę do edytora, to podmieniasz implementację/zależności, ale sam sposób korzystania z całości pozostaje bez zmian.

No tak, to jest dodatkowa ważna rzecz. Dependency Inversion oraz Separation of Concerns.

Jakiś czas temu ktoś wrzucił na 4P (nie pamiętam, czy to była własna opowieść, czy podlinkowany wpis do jakiegoś artylułu/bloga) opis podobnej sytuacji. Mieli jakiś system, który był na sztywno spięty z jakimś silnikiem map. Nie było warstwy pośredniczącej - i to pięknie działało. Tylko zamiast zrobić jakieś opakowanie do komunikacji z mapami - coś w stylu map.getCoords() to walili bezpośrednio do API. I jak nagle dostawca nawigacji coś pozmieniał (o ile dobrze kojarzę to chodziło o moment, gdzie Google zaczęło wprowadzać opłaty z korzystanie z ich map, jeśli ruch przekraczał jakąś skalę), aplikacja przestała działać, a całe przerobienie, zamiast (względnie) prostego podmienienia czegoś wewnątrz klasy pomocniczej, sprowadziło się do doraźnego grzebania w kodzie, który sobie bezpośrednio gadał z mapami, a docelowo - poważny refactor. A dałoby się tego uniknąc, jakby od razu wprowadzili pośrednika.

A to nie widziałem, ale wierzę na słowo że tak mogło być :D

4

@TomRiddle: wydaje mi się, że mówimy o różnych sytuacjach. Po pierwsze boilerplate jest często wymagany z natury danej biblioteki/frameworka, który stosujesz. Przykładowo jak piszesz aplikację w Symfony to kontroller musi spełniać pewne założenia (o ile tego jakoś nie zmienisz w konfiguracji) - wtedy po co masz tworzyć klasę kontrolera ręcznie i narażać się na jakieś błędy typu literówki jak możesz użyć prostego make:controller i wszystko zostanie stworzone za Ciebie (uprzedzając, wiem że w Syfmony to nie jest jedyna droga, bo na przykład teraz piszę sobie testowo apkę w architekturze hex i mój kontroller zupełnie nie przypomina tego co przez kontroller rozumie Symfony, ale to dygresja. Chodzi mi tylko o to, że jak pracujesz w danym projekcie to trzymasz się jego konwencji i czasami boilerplate jest po prostu konieczny.

Drugi przykład (o zgrozo znowu mój side-project). Api wcale nie musi być skomlikowane aby builder był wymagany. Dla przykładu ja jestem zdania, że encje domenowe powinny być tworzone od razu w formie zwalidowanej, czyli nie pozwalam w aplikacjach na stworzenie pustej encji i potem uzupełnienie jej seterami, bo przez to dopuszczam do encji w stanie nieprawidłowym. Zamiast tego mam encje, które w kontrolerze przyjmują po X parametrów - np. niech będzie 10.

Jestem również bardzo gorącym przeciwnikiem default values w funkcjach - uważam, że klient kodu zawsze gdy tylko to możliwe powinien świadomie wywoływać wszystkie parametry kodu.

I teraz kończę z encją typu user i konstruktorem typu

__construct(firstName, lastName, age, pssword, ..., address)

W przypadku zwykłego kodu to nie jest problem, bo tworząc usera muszę te wszystkie parametry podać czy chce czy nie chcę.

W przypadku testów sytuacja wygląd inaczej. Mogę albo zrobić klasę buildera dedykowaną dla testów, która ma hardkodowane jakieś poprawne wartości domyślne albo w każdym teście tworzyć obiekt ręcznie. Drugie podejście jest bez sensu, bo jakakolwiek zmiana w konstruktorze wymaga zmiany w wielu testach i zaciera samą istotę testu bo ustawiam np. 15 parametrów zamiast tego jednego, który chcę przetestować. Podejście z builderem na potrzeby testów ma znacznie więcej sensu i jest bardzo często stosowane, a zazwyczaj taki builder jest jednym wielkim boilerplatem. Moim zdaniem taki generator ma sporą rację bytu, jak i wiele innych boilerplatów typu wspomniane dedykowane dla konwencji używanych w danym frameworku.

PS. Niezły oftopik się zrobił, ale podoba mi się ta dyskusja ;-)

1
hadwao napisał(a):

@TomRiddle: wydaje mi się, że mówimy o różnych sytuacjach. Po pierwsze boilerplate jest często wymagany z natury danej biblioteki/frameworka, który stosujesz. Przykładowo jak piszesz aplikację w Symfony to kontroller musi spełniać pewne założenia (o ile tego jakoś nie zmienisz w konfiguracji) - wtedy po co masz tworzyć klasę kontrolera ręcznie i narażać się na jakieś błędy typu literówki jak możesz użyć prostego make:controller i wszystko zostanie stworzone za Ciebie (uprzedzając, wiem że w Syfmony to nie jest jedyna droga, bo na przykład teraz piszę sobie testowo apkę w architekturze hex i mój kontroller zupełnie nie przypomina tego co przez kontroller rozumie Symfony, ale to dygresja. Chodzi mi tylko o to, że jak pracujesz w danym projekcie to trzymasz się jego konwencji i czasami boilerplate jest po prostu konieczny.

No, jak mówiłem. Zbyt duże przywiązanie do frameworka. Dużo zbyt duże, jeśli nie możesz go użyć tak jak chcesz. Jeśli biblioteka udostępnia Ci API które wymaga 30,40,50 linijek w 2,3,4,5ech plikach, żeby zrobić coś, co wydaje Ci się proste, to powinieneś to wydelegować do jakiejś swojej klasy/funkcji, w która wyraża intencje i jest prosta do użycia i zmiany.

Drugi przykład (o zgrozo znowu mój side-project). Api wcale nie musi być skomlikowane aby builder był wymagany. Dla przykładu ja jestem zdania, że encje domenowe powinny być tworzone od razu w formie zwalidowanej, czyli nie pozwalam w aplikacjach na stworzenie pustej encji i potem uzupełnienie jej seterami, bo przez to dopuszczam do encji w stanie nieprawidłowym. Zamiast tego mam encje, które w kontrolerze przyjmują po X parametrów - np. niech będzie 10.

No to kolejny problem, którego symptomem jest skomplikowane API. Encje które mają 10 parametrów to dużo za dużo. Powinno być 1-3, może 4-5 jeśli to jest jakiś wyjątek, ale 10? Dużo za dużo. Jeśli pozwolisz na taki przerost, to nic dziwnego że trafisz w taką sytuacje kiedy używanie ich jest trudne, trudne do tego stopnia że robisz do nich buildery, a z których korzystanie jest znowu tak niewygodne że musisz je generować.

Cytując klasyka: "Weźmy rzecz która nie działa. Róbmy jej więcej".

Jestem również bardzo gorącym przeciwnikiem default values w funkcjach - uważam, że klient kodu zawsze gdy tylko to możliwe powinien świadomie wywoływać wszystkie parametry kodu.

No, zgadzam się.

I teraz kończę z encją typu user i konstruktorem typu

__construct(firstName, lastName, age, pssword, ..., address)

W przypadku zwykłego kodu to nie jest problem, bo tworząc usera muszę te wszystkie parametry podać czy chce czy nie chcę.

No tak. Jeśli nie stosujesz separation of concerns, i dodajesz encje do innych encji bezmyślnie, to tak.

W przypadku testów sytuacja wygląd inaczej. Mogę albo zrobić klasę buildera dedykowaną dla testów, która ma hardkodowane jakieś poprawne wartości domyślne albo w każdym teście tworzyć obiekt ręcznie. Drugie podejście jest bez sensu, bo jakakolwiek zmiana w konstruktorze wymaga zmiany w wielu testach i zaciera samą istotę testu bo ustawiam np. 15 parametrów zamiast tego jednego, który chcę przetestować. Podejście z builderem na potrzeby testów ma znacznie więcej sensu i jest bardzo często stosowane, a zazwyczaj taki builder jest jednym wielkim boilerplatem. Moim zdaniem taki generator ma sporą rację bytu, jak i wiele innych boilerplatów typu wspomniane dedykowane dla konwencji używanych w danym frameworku.

No, ja tu widzę kolejne dwa błędy.

Po pierwsze, to wygląda jakby Twoje testy były zbyt wrażliwe na zmiany, tak wrażliwe, że jak potem zmieniasz jedną encje, to wiele testów failuje. To się nazywa "Rigid tests", czyli nieodporność testów na zmiany. Testy powinny testować zachowanie, nie strukturę Twojej aplikacji. Jeśli refaktorujesz klasę, np zmieniasz parametry, ale nie zmieniasz logiki, testy nie powinny failować.

Po drugie, gdybyś najpierw tworzył testy, stosował TDD, to szansa na to że stworzyłbyś klasę która ma 10 parametrów w konstruktorze jest raczej mała.

Po trzecie, jeśli używasz builderów w testach, to nie możesz potem ich refaktorować bez zmiany w testach - kolejna wskazkówa przywiązania testów do klasy, wręcz można powiedzieć że złamana enkapsulacja, jeśli testy wiedzą które parametry są implicit a które nie.

@hadwao No zastanów się. Wyobraź sobie, że w bibliotece z której korzystasz ktoś zrobiły coś takiego, że wszystko co chcesz zrobić generatorem da się zrobić jedną funkcją. Że np w symfony da się zrobić te boilerplate controllery i widoki o których mówisz, ale da się zrobić to samo bez boilerplateu. Czy nie fajniej by się tego używało? Czy nie prościej by się pisało testy pod to, i nie prościej byłoby to zmienić i utrzymać? To byłaby utopia.

Ale jeśli nie ma tego w symfony to możesz zrobić tak:

  • Napisać sam, coś czego nie ma, i używać API tak jak Ty chcesz, podczas gdy biblioteka dostarcza tylko implementacje
  • Przywiązać swoją aplikacje ściśle do biblioteki, i używać boilerplateów tak jak piszą w dokumentacji, a potem dorobić do tego buildery, które przywiązują Twoje testy do aplikacji, i które potem się ich tak ciężko używa, że je też trzeba generować.

Ja żyję w tej utopii, bo nie pozwalam bibliotekom dyktować jak ma wyglądać moja aplikacja.

1

W zwykłych klasach zgadzam się, że im mniej zależności tym lepiej, ale w przypadku Encji relatywnie duża ilość parametrów nie jest moim zdaniem błędem. W jaki sposób ograniczasz ilość parametrów konstruktora w swoich encjach. Można oczywiście pójść w mały overenginiering i mniej lub bardziej sztucznie grupować je w obiekty, ale szczerze mówiąc nie to dla mnie sztuka dla sztuki i komplikuje niepotrzebnie potem samą encję. Tak więc co do zwykłych metod, czy konstruktorów np. klas typu usługowego to jak najbardziej ilość parametrów w konstruktorze musi być trzymana w ryzach. Ale w encjach domenowych nie widzę takiej potrzeby.

Co do problemu z testami to właśnie podejście z builderem to jest uniezależnienie testów od zmian - masz builder czy jakieś factory, które potrafi wygenerować poprawny obiekt encji w którym precyzyjnie zmieniasz tylko to co chcesz testować. Tutaj właśnie dokładnie masz to co opisujesz - zmiana w kodzie wpływa tylko na testy, które tego kodu dotyczą. Znowu tutaj zapytam w jakis sposób generujesz testową encję czy jakiś obiekt typu command/request dla swoich testów. Czyli załóżmy masz obsłużyć rejestracji usera przez api. Na wejściu masz Command/Request z 15 parametrami potrzebnymi w Twojej logice do zarejestrowania usera. Test ma przetestować czy zostanie wyrzucony wyjątek w trakcie tworzenia usera z wiekiem 15 lat.

Ja w tej sytuacji bym zrobił mniej więcej coś takiego:

test_can_register_underage_user()
{
   userCommand = testUserCommandBuilder.withAge(16).build();
   expectException()
   registrationService.execute(userCommand)
}

Mój test jest uniezależniony od zmian w strukturze command.

Co do uzależnienia do frameworka to w tej kwestii mam bardzo podobne podejście. Zazwyczaj całą logikę domenową piszę w oparciu o czysty PHP + interfejsy, a framework używam tylko do obsługi requestów czy wygodnego ogarnięcia infrastruktury. Nie jestem jakimś ogromnym purystom, ale też frameworka rzadko używam zgodnie z tutorialam bo tam są prezentowane same antywzorce.

Niemniej nawet w kodzie domenowym piszesz sporo boileprlate (sam wspominałeś o używaniu live templates czy generatorów w ide) - nie widzę tutaj większej różnicy w pisaniu czegoś w ten sposób i używaniu skryptów, które generują podstawowe rzeczy dla ciebie według założonej konwencji (czy to prze Ciebie czy przez framework/libkę)

Chętnie bym jednak dowiedział się jak byś podszedł do testu takiego jak opisałem powyżej - czyli przetestowania rejestracji użytkownika z komendy, która zawiera 10 parametrów. Pytam całkiem serio, bo akurat w testach czuję się bardzo słabo i nie mam od kogo się uczyć i może faktycznie gdzieś zrobiłem błąd w moich założeniach.

1
hadwao napisał(a):

@TomRiddle: wydaje mi się, że mówimy o różnych sytuacjach. Po pierwsze boilerplate jest często wymagany z natury danej biblioteki/frameworka, który stosujesz. Przykładowo jak piszesz aplikację w Symfony to kontroller musi spełniać pewne założenia (o ile tego jakoś nie zmienisz w konfiguracji) - wtedy po co masz tworzyć klasę kontrolera ręcznie i narażać się na jakieś błędy typu literówki jak możesz użyć prostego make:controller i wszystko zostanie stworzone za Ciebie (uprzedzając, wiem że w Syfmony to nie jest jedyna droga, bo na przykład teraz piszę sobie testowo apkę w architekturze hex i mój kontroller zupełnie nie przypomina tego co przez kontroller rozumie Symfony, ale to dygresja. Chodzi mi tylko o to, że jak pracujesz w danym projekcie to trzymasz się jego konwencji i czasami boilerplate jest po prostu konieczny.

z tym się zgadzam

z tym już nie
ale jak dać pół-łapki?

Drugi przykład (o zgrozo znowu mój side-project). Api wcale nie musi być skomlikowane aby builder był wymagany. Dla przykładu ja jestem zdania, że encje domenowe powinny być tworzone od razu w formie zwalidowanej, czyli nie pozwalam w aplikacjach na stworzenie pustej encji i potem uzupełnienie jej seterami, bo przez to dopuszczam do encji w stanie nieprawidłowym. Zamiast tego mam encje, które w kontrolerze przyjmują po X parametrów - np. niech będzie 10.

Jestem również bardzo gorącym przeciwnikiem default values w funkcjach - uważam, że klient kodu zawsze gdy tylko to możliwe powinien świadomie wywoływać wszystkie parametry kodu.

jak nie ma wartości domyślnych to kończy się na przekazywaniu parametrów w postaci struktur kobylastychjak w WinAPi

1

Jedyne co to w Javie z wykorzystaniem IntelliJ generuje sobie gettery, settery (gdy brakuje lomboka) i override functions

2
hadwao napisał(a):

Też mnie właśnie ten temat zastanowił, bo wprowadzenie atrybutów powinno dać sporo możliwości. Przykładowo moje implementacja dla builderów będzie pozwalała na coś takiego:

#[Builder('\\App\\Builder\\User', 'standard')]
#[Builder('\\App\\Tests\\Builder\\User', 'test')]
class User
{
    public function __construct(
        #[BuilderParam(defaultValue: 'jan_kowalski', builder: ['test'])]
        private Name $name,
        #[BuilderParam(defaultValue: 25, builder: ['test'])]
        private Age $age,
    ) {
    }
}

Czyli dodanie tutal atrybutów powoduje stworzenie (dla kodu powyżej) 2 klas builderów. Jednej dla użytku dla aplikacji, która nie ma wartości domyślnych, a druga "test" dla testów, gdzie będą predefiniowane wartości domyślne dodwane atrybutem BulderParam. Jeśli builder już istnieją to dodawane będą tylko brakujące metody, czyli nie będzie trzeba poprawiać istniejących testów po dodaniu jakiegoś parametru w konstruktorze.

Ok, czyli Twój kod biznesowy ma zależność od kodu testowego i jest zanieczyszczony jakimiś jego metadanymi. To jest coś, czego ja osobiście nie trawię, bo to jest bałagan, mieszanie warstw abstrakcji i rozpraszanie podczas analizy kodu. Dla mnie to wygląda jak robienie sobie krzywdy.

Zamiast tysięcy builderów nie można mieć po prostu jednego generycznego, który będzie działał tak? (Nie znam PHP, improwizuję ze składnią.)

meh user = new Builder<User>()
    .with(u -> u.name, "janek")
    .with(u.age, 25)
    .build();

Można też to rozbudować, aby użył jakichś wartości domyślnych, dostarczonych przez jakiś obiekt testowej konfiguracji łączący ze sobą pola i docelowe wartości:

class UserDefinitionForTests : DefinitionForTest<User>
{
    public function __construct() {
       Configure(v => v.name, "janek");
       Configure(v => v.age, 25);
    }
 }

a generyczny Builder znajdzie sobie taką klasę zgodnie z jakimiś konwencjami, albo jeszcze lepiej po prostu bazując na jakimś seedzie, np:

// Test code
//
User defaultTestUser = new User("janek", 25);
///
meh user = new Builder<User>(defaultTestUser)
    .with(u.age, 35)
    .build(); // wciąż janek, ale stary

Aczkolwiek konkretne dane w testach są zazwyczaj zbędne, i równie dobrze można je wygenerować losowo.

1
hadwao napisał(a):

W zwykłych klasach zgadzam się, że im mniej zależności tym lepiej, ale w przypadku Encji relatywnie duża ilość parametrów nie jest moim zdaniem błędem. W jaki sposób ograniczasz ilość parametrów konstruktora w swoich encjach. Można oczywiście pójść w mały overenginiering i mniej lub bardziej sztucznie grupować je w obiekty, ale szczerze mówiąc nie to dla mnie sztuka dla sztuki i komplikuje niepotrzebnie potem samą encję. Tak więc co do zwykłych metod, czy konstruktorów np. klas typu usługowego to jak najbardziej ilość parametrów w konstruktorze musi być trzymana w ryzach. Ale w encjach domenowych nie widzę takiej potrzeby.

Skoro tak, to czemu musisz używać buildera w testach? :)

Przecież skoro twierdzisz że ta ilość odpowiednia, i nie musisz ich sztucznie ich grupować, to powinieneś też móc bezproblemowo pisać testy z nimi, czyż nie? :) :>

Ale wiemy że to nie prawda, bo jeśli nie możesz tego prosto przetestować, to znaczy że nie możesz tego prosto użyć, a to znaczy że klasa/Twoje rozwiązanie nie jest proste, więc to grupowanie o którym mówisz nie byłoby sztuczne, a wymagane.

Co do problemu z testami to właśnie podejście z builderem to jest uniezależnienie testów od zmian - masz builder czy jakieś factory, które potrafi wygenerować poprawny obiekt encji w którym precyzyjnie zmieniasz tylko to co chcesz testować.

Uniezależniasz testy od swoich encji, ale uzależniasz ich od builderów i factory. Także nie wiem gdzie tu jakiś improvement.

Tutaj właśnie dokładnie masz to co opisujesz - zmiana w kodzie wpływa tylko na testy, które tego kodu dotyczą.

A powinny nie dotyczyć niczego. Zmiana implementacji, refactor, powinien sprawić że testy przechodzą tak jak działały, ani jeden nie powinien zfailować.

Znowu tutaj zapytam w jakis sposób generujesz testową encję czy jakiś obiekt typu command/request dla swoich testów. Czyli załóżmy masz obsłużyć rejestracji usera przez api. Na wejściu masz Command/Request z 15 parametrami potrzebnymi w Twojej logice do zarejestrowania usera. Test ma przetestować czy zostanie wyrzucony wyjątek w trakcie tworzenia usera z wiekiem 15 lat.

No ale przecież parametry wejściowe/command/request to nie to samo co encje biznesowe ;| Powinny być niezależne od siebie.

No ja bym zrobił tak:

  • Dwa moduły: jeden moduł integracyjny, jeśli to Web to app.http, jeśli to CLI to app.cli, on miałby kontroller, czyli klasę która umie przetłumaczyć request/command na prawdziwy call. Tutaj ma nie być żadnej walidacji wieku ani nic takiego.
  • Drugi moduł, encja biznesowe, które są używane przez kontroller. Tutaj powinien być check, czy jeśli user ma mniej niż 15 lat, to nie pozwala go stworzyć i informuje jakoś o tym kontroller: wyjątek, optional, cokolwiek innego.

Napisałbym odpowiednie unity testy do logiki biznesowej, które sprawdzają wszystko; a do kontrollera w module app.http/app.cli dodałbym tylko jeden test, czy faktycznie Taki i taki request jest tłumaczony na taki i taki call. Napisałbym wystarczająco mało testów, żeby tylko się upewnić że ruch jest delegowany do logiki biznesowej, ale nie testowałbym jej przez kontroller. To byłoby przywiązanie testów do biblioteki, a tego nie chcemy.

Co do uzależnienia do frameworka to w tej kwestii mam bardzo podobne podejście. Zazwyczaj całą logikę domenową piszę w oparciu o czysty PHP + interfejsy, a framework używam tylko do obsługi requestów czy wygodnego ogarnięcia infrastruktury. Nie jestem jakimś ogromnym purystom, ale też frameworka rzadko używam zgodnie z tutorialam bo tam są prezentowane same antywzorce.

Noi bardzo dobrze.

Niemniej nawet w kodzie domenowym piszesz sporo boileprlate (sam wspominałeś o używaniu live templates czy generatorów w ide) - nie widzę tutaj większej różnicy w pisaniu czegoś w ten sposób i używaniu skryptów, które generują podstawowe rzeczy dla ciebie według założonej konwencji (czy to prze Ciebie czy przez framework/libkę)

Ale moje live templates dodają jedną linijkę max. A generatory w ide to mam na myśli "Add property", "extract method", "Extract class". I one dodają to zależnie od kontekstu, tego co piszę. A nie z góry ustalony szablon z placeholderami.

Chętnie bym jednak dowiedział się jak byś podszedł do testu takiego jak opisałem powyżej - czyli przetestowania rejestracji użytkownika z komendy, która zawiera 10 parametrów. Pytam całkiem serio, bo akurat w testach czuję się bardzo słabo i nie mam od kogo się uczyć i może faktycznie gdzieś zrobiłem błąd w moich założeniach.

test_can_register_underage_user()
{
userCommand = testUserCommandBuilder.withAge(16).build();
expectException()
registrationService.execute(userCommand)
}


Moim zdaniem to jest bardzo zły test, bo jest pełen implicit inputu i outputu. Ja bym tu przekazał wszystkie parametry jawnie, po to żeby przetestować integrację, walidację przetestowałbym niżej.

Wyjaśniam dlaczego.

Jeśli piszesz test, który ma sprawdzić czy jedna wartość jednego parametru spowoduje wyjątek, a pozostałe 14 parametrów jest ignorowane, to MOOŻE ten test byłby spoko.

Ale to nie jest rzeczywistość, często te parametry wpływają na siebie, i np jest coś takiego że wymagany jest jeden z parametrów, albo dwa parametry nie mogą do siebie nie pasować (typu nie może być `city:Kraków`, jeśli `country:Germany`), i wtedy taki implicit state może zaszkodzić niż pomóc.
0
Miang napisał(a):

jak nie ma wartości domyślnych to kończy się na przekazywaniu parametrów w postaci struktur kobylastychjak w WinAPi

Jeśli nie umiesz zaprojektować dobrego API to tak.

Jeśli umiesz i dbasz o kod to nie.

2

@TomRiddle: chyba trochę źle odczytałeś mój przykład - w nim mamy na wejściu Command, który bym sobie potencjalnie wygenerował Builderem, a obiekt biznesowy jest tworzony w Service - nie ma tam żadnego kontrollera - kontroller by właśnie stworzył ten command i wywołał service. Czyli w tym przypadku mamy test integracyjny usługi. Oczywiście zgadzam się, że nie jest to najlepszy test integracyjny, bo to czy mogę utworzyć usera sub 18 lat to raczej powinien być test samej encji biznesowej, ale tu akurat nie o to mi chodziło tylko o samą zasadność stosowania buildera, zamiast tworzenia całego requesta - równie dobrze builder mógłby zwracać domyślny po prostu Command bez ustawiania w nim czegokolwiek. Czy to będzie jakiś builder czy faktorka obiektu testowego to jest sprawa drugorzędna. (PS teraz cofnąłem się do swojego posta i widzę, że na początku zacząłem pisać ogólnie o encji a dopiero w ostatnim zdaniu przed przykładem napisałem o przykładzie z Command/Request - przypuszczam, że stąd nieporozumienie - mój błąd, mogłem wyraźniej opisać, ale w kodzie po zmiennych wyraźnie widać, że chodziło mi o typową komendę)

Druga sprawa bardzo ogólnie piszesz o testach i architekturze, więc częściowo trudno mi dyskutować z wieloma argumentami, zwłaszcza że często się z nimi zgadzam ;-) Ale praktyką jest stosowanie w testach różnych "pomocniczych" klas typu buildery czy inne providery obiektów na potrzeby testów - raczej w praktyce nie ma środowiska testowego, w którym miałbyś same testy.

Ogólnie w dużym skrócie chodzi mi o to co jest lepsze - tworzenie danego obiektu np. Request czy EncjiBiznesowej za każdym razem ręcznie podająć wszystkie parametry jawnie czy wydzodząc z jakiejś struktury zawierającej poprawne dane i tworząc coś przydatnego do naszego testu - czyli np. zmieniając wiek usera. Moim zdaniem jeśli wyeliminujesz wszystkie buildery/factory z testów to znacznie pogorszysz ich jakość. Jeśli na przykład będziesz tworzył komendy ręcznie podając wszystkie parametry to znacznie trudniej taki test odczytać - w sensie nie widać dokładnie co testujesz.

@somekind kurde a ja myślałem, że Ty się ciągle tylko czepiasz, a tu nawet jakiś konstruktywny post ;p

A tak serio to bardzo fajne rozwiązanie - co prawda w PHP nie ma oficjalnie generyków, ale w praktyce da się je ogarnąć. Tak jak pisałem wcześniej ten mój builder to raczej nie był kod, który miał być wykorzystywany produkcyjnie tylko zabawa z nową rzeczą w PHP 8 tj. atrybutami i zrobił się z tego wielki offtopic, ale to co zaproponowałeś wygląda koncepcyjnie bardzo fajnie.

Co do adnotacji - to tak jak pisałem wyżej mi adnotacje przeszkadzają w momencie kiedy np mieszają domenę z infrastrukturą. W takim przypadku jak ten builder byłbym w stanie się z nimi pogodzić choć można by się przyczepić, że testy domeny to nie domena, więc powiedzmy miałbym mieszane uczucia, ale finalnie bym powiedział, ze korzyści przeważają nad trochę zaśmieconą encja czy komendą.

No ale tak jak pisałem to sprawa poboczna - fakt, że Twoje rozwiązanie faktycznie byłoby ciekawsze technicznie niż zaśmiecanie buildera adnotacjami/atrybutami i nawet chyba zamierzam je spróbować zaimplementować w PHP w swoim prywatnym projekcie.

1
hadwao napisał(a):

@TomRiddle: chyba trochę źle odczytałeś mój przykład - w nim mamy na wejściu Command, który bym sobie potencjalnie wygenerował Builderem, a obiekt biznesowy jest tworzony w Service - nie ma tam żadnego kontrollera - kontroller by właśnie stworzył ten command i wywołał service. Czyli w tym przypadku mamy test integracyjny usługi. Oczywiście zgadzam się, że nie jest to najlepszy test integracyjny, bo to czy mogę utworzyć usera sub 18 lat to raczej powinien być test samej encji biznesowej, ale tu akurat nie o to mi chodziło tylko o samą zasadność stosowania buildera, zamiast tworzenia całego requesta - równie dobrze builder mógłby zwracać domyślny po prostu Command bez ustawiania w nim czegokolwiek. Czy to będzie jakiś builder czy faktorka obiektu testowego to jest sprawa drugorzędna. (PS teraz cofnąłem się do swojego posta i widzę, że na początku zacząłem pisać ogólnie o encji a dopiero w ostatnim zdaniu przed przykładem napisałem o przykładzie z Command/Request - przypuszczam, że stąd nieporozumienie - mój błąd, mogłem wyraźniej opisać, ale w kodzie po zmiennych wyraźnie widać, że chodziło mi o typową komendę)

Druga sprawa bardzo ogólnie piszesz o testach i architekturze, więc częściowo trudno mi dyskutować z wieloma argumentami, zwłaszcza że często się z nimi zgadzam ;-) Ale praktyką jest stosowanie w testach różnych "pomocniczych" klas typu buildery czy inne providery obiektów na potrzeby testów - raczej w praktyce nie ma środowiska testowego, w którym miałbyś same testy.

To napisz jeszcze raz o co chodzi, żeby nie mieszać.

Ogólnie w dużym skrócie chodzi mi o to co jest lepsze - tworzenie danego obiektu np. Request czy EncjiBiznesowej za każdym razem ręcznie podająć wszystkie parametry jawnie czy wydzodząc z jakiejś struktury zawierającej poprawne dane i tworząc coś przydatnego do naszego testu - czyli np. zmieniając wiek usera. Moim zdaniem jeśli wyeliminujesz wszystkie buildery/factory z testów to znacznie pogorszysz ich jakość. Jeśli na przykład będziesz tworzył komendy ręcznie podając wszystkie parametry to znacznie trudniej taki test odczytać - w sensie nie widać dokładnie co testujesz.

Jeśli będziesz miał skomplikowane interfejsy, to tak. Jeśli będą dobrze zaprojektowane to nie.

Moim zdaniem, brzydkie, wielkie nieczytelne testy to jest obiaw. Wsadzenie do nich builderów i fabryk to ukrycie problemu. Jeśli masz test na 30 linijek, i encje na 10 parametrów, to Twoją pierwszą myślą nie powinno być "o kurde, lepiej to schowam do buildera", tylko "po kiego grzyba mam tak skomplikowaną encje?".

1

@TomRiddle: czyli w bardzo dużym skrócie musisz przetestować serwis który przyjmuje Command (czy tam Request) - chodzi mi o Request w rozumieniu typowego DTO już po przetworzeniu requestu prze np. kontroller. Załóżmy że taki command przyjmuje 3 parametry (np. pół z formularza czy parametrów z CLI) sam test wygląda tak w pseudokodzie

test() {
command = ... tutaj jakoś tworzysz komendę np. new Command(a,b.c) albo TestCommandBuilder.build()
sut.doSth(command)
assert.sth
}

Specjalnie generalizuje test aby nie dyskutować o tym co robi i czy to jest właściwe miejsce do testowania. Pytanie rozbija się o to jak rozumiem, czy obiekt jedynym właściwym podejściem jest stworzenie przykladowego obiektu Command ręcznie np. używając new albo jakiejś klasy z kodu produkcyjnego typu CommandFactory, czy też można na potrzeby testu tworzyć specjalne konstrukcje nie występujące w kodzie produkcyjnym.

W przykładzie naszej dyskusji takim obiektem jest na przykład TestCommandBuilder(), który pozwala wygenerować poprawną komendę i w kodzie ustawić tylko wybrane 1-2 pola, które chcesz przetestować (w sensie jak ich zmiana wpływa na SUT, bo wiadomo nie testujesz komendy)

Pomijam tutaj wszelkie kwestie czy to będzie test integracyjny, jednostkowy, czy komenda będzie miała 1 parametr czy 10, czy ten przykładowy builder będzie wygenerowany automatem czy napisany ręcznie czy w formie zaproponowanej przez @somekind

Ja stoję na stanowisku, że takie klasy pomocnicze w testach nie są złym wyjściem i w żaden sposób nie stanowią o źle zaprojektowanym API. Wręcz przeciwnie wg mnie są objawem dobrej konstrukcji testu bo nawet jeśli łatwiej jest dzisiaj napisać new UserCommand('jan', 'kowalski') to jak kiedyś do komendy zostanie dodany wiek to wszystkie testy używające tego Command'a przestaną działać. W przypadku wydzielenia buildera obiektu testowego zmiana UserCommand pociąga za sobą zmiany tylko w builderze i ewentualnei w testach, które na tym bazują. Czyli np. jak w Twoim przykładzie masz City, a po jakiejś iteracji wprowadzony zostaje "Country" i jest jakaś biznesowa zależność między tymi polami, to ewentualnie ten test się wysypie (i dobrze, bo powinien).

Reasumując nie uważam aby Buildery były same w sobie objawem złego API a w testach są moim zdaniem bardzo przydatne.

1
hadwao napisał(a):

@TomRiddle: czyli w bardzo dużym skrócie musisz przetestować serwis który przyjmuje Command (czy tam Request) - chodzi mi o Request w rozumieniu typowego DTO już po przetworzeniu requestu prze np. kontroller.

DTO to też jest część integracji. Ono też nie powinno mieć wjazdu do domeny biznesowej i encji. Jeśli Twój kontroller tłumaczy Request HTTP na DTO, to powinieneś też mieć coś co na podstawie tego DTO woła logikę biznesową.

Więc powiedzenie "po przetworzeniu requestu przez np. kontroller", to tak na prawdę mówisz że struktura requestu http powinna odpowiadać strukturze tego dto? A potem tego DTO używasz w dalszej części swojej aplikacji?

Bo jeśli tak to Twoja logika biznesowa jest tak samo przywiązana do tego DTO, jakby była do requestu HTTP. Więc, again, rozłoączenie conercnów nadal masz słabe i nadal masz takie same problemy.

Ja stoję na stanowisku, że takie klasy pomocnicze w testach nie są złym wyjściem i w żaden sposób nie stanowią o źle zaprojektowanym API. Wręcz przeciwnie wg mnie są objawem dobrej konstrukcji testu bo nawet jeśli łatwiej jest dzisiaj napisać new UserCommand('jan', 'kowalski') to jak kiedyś do komendy zostanie dodany wiek to wszystkie testy używające tego Command'a przestaną działać. W przypadku wydzielenia buildera obiektu testowego zmiana UserCommand pociąga za sobą zmiany tylko w builderze i ewentualnei w testach, które na tym bazują. Czyli np. jak w Twoim przykładzie masz City, a po jakiejś iteracji wprowadzony zostaje "Country" i jest jakaś biznesowa zależność między tymi polami, to ewentualnie ten test się wysypie (i dobrze, bo powinien).

To ma z kolei więcej wad:

bo nawet jeśli łatwiej jest dzisiaj napisać new UserCommand('jan', 'kowalski') to jak kiedyś do komendy zostanie dodany wiek to wszystkie testy używające tego Command'a przestaną działać.

Ale wtedy przywiązujesz testy do swojej struktury, zamiast testować zachowanie.

W przypadku wydzielenia buildera obiektu testowego zmiana UserCommand pociąga za sobą zmiany tylko w builderze i ewentualnei w testach, które na tym bazują.

A to ma jeszcze większą wadę, powiedziałbym największą z możliwych. Takie zmiany w testach to jest to co sprawia że większość programistów nie robi dobrych testów, i czemu ludzie nie lubią TDD.

Pozwól że zrozumiem:

  • Masz powiedzmy 100 testów logiki biznesowej
  • Każdy z tych 100 testów używa sobie buildera, do zrobienia encji, i ten builder np robi encję z jakimś potem, dajmy city.
  • Nadchodzi czas kiedy trzeba dodać jakieś nowe pole, które wypływa na stare.
  • Dodajesz takie pole, wprowadzasz taką zmianę w builderze, i automatycznie te 100 korzystając z niego również ma to dodatkowe pole.

Tak? To jest ten scenariusz? Bo jeśli tak, to moim zdaniem to jest katastrofa.

Dlatego, że jeśli faktycznie chcesz dodać pole które wpyłwa na inne pole do tej encji to MUSISZ zapoznać się z testami które już istnieją, żeby poprawnie wyrazić jak to dodatkowe pole wypłwa na już istniejące pole.

Jeśli się z nimi nie zapoznasz, tylko po prostu dodasz to nowe pole do buildera, to skąd możesz wiedzieć czy testy nadal testują co mają testować, czy ich zachowania nadal ma sens, czy niektóre testy nie stał się bezużyteczne/bezwartościowe, czy nie wprowadziłeś duplikatów zachowania i masy innych rzeczy?

Reasumując nie uważam aby Buildery były same w sobie objawem złego API a w testach są moim zdaniem bardzo przydatne.

Same w sobie nie są. Jeśli buildery są używane tylko w testach (nie jest tak że jeden builder jest użyty zarówno na prodzie i w testach), to spoko, ale są symptomem dwóch innych rzeczy:

  • Encje które mają zbyt dużo zależności
  • Testy które są zbyt przywiązane do logiki

Jeśli zrobiłbyś testy które są na prawdę de-coupled od swojego kodu, i nie miałbyś encji które mają 4+ parametrów, to owszem, mógłyś sobie użyć buildera, ale wtedy nie miałoby to aż takiego sensu bo encje byłby na tyle proste że nie musiałbyś tego robić.

Reasumując nie uważam aby Buildery były same w sobie objawem złego API a w testach są moim zdaniem bardzo przydatne.

Jeśli uznasz taki argument za prawdziwy, to równie dobrze mógłbyś mieć klasę która ma 100 parametrów, i builder który dodaje 99 defaultowych parametrów, i użyć go tak

void test() {
  // given
  hyperEntity = HyperBuilder.withArg(2);

  // test
}

No bo skoro może być builder z 10 parametrami, to może być też z 15, 20, 50, 100 czy nawet i więcej. Nigdzie w Twojej logice nie widzę jakiegoś sensownego ograniczenia.

PS: @hadwao Moim zdaniem, cały Twój problem polega na tym, że masz tą wizję, że w Twojej aplikacji musi być coś takiego jak Command który ma n zależności, a jednocześnie musi pokryć całą aplikację (otoczyć ją dookoła), i jakby delegować ruch do niej, a jednocześnie też nie chcesz przenieść jej na wyższy poziom abstrakcji.

Moim zdaniem to jest źle zaprojektowane api. Chcąc wykonać jedną rzecz, nie chcę musieć tworzyć Commanda z 15 parametrami.

2

Kurcze ciągle mam wrażenie, że się trochę rozmijamy w nazewnictwie o jakieś detale, ale do końca nie potrafię wyłapać.

U mnie encja biznesowa to np. User i tworząc Usera nie widzę problemu w tym, że będzie tam 10 parametrów w konstruktorze - używasz tutaj słowa zależność. Dla mnie zależność to zależność od jakiegoś serwisu, ale Ty raczej traktujesz to szerzej i np. przyjęcie stringa jako email też traktujesz jako zależność. Także dla mnie tworzenie encji biznesowych przyjmujących dane czy to jako skalary czy ValueObjects (a nie zależności w moim rozumieniu, czyli jakieś inne Servisy) to jest całkiem rozsądna praktyka. Nie zdarzyło mi się jeszcze tworzyć encji biznesowej ze 100 polami - takie zwyczajnie rozbijam bo np. w Userze adress to już będzie ValueObject albo jakaś inna Encja. Także w praktyce limitem jest tutaj po prostu zdrowy rozsądek.

W przypadku obiektów przyjmujących zależności w moim rozumieniu (czyli np. coś wstrzykniętego przez DI) to w 100% się zgadzam - im mniej tym lepiej, ale w przypadku DTO nie widzę powodu czemu miałbym się ograniczać do np. 3 parametrów w konstruktorze.

Co do opisu mojego zbyt szerokiego podejścia to mam wrażenie, że chyba tutaj też coś komunikacyjnie nie zagrało. U mnie architektura apki wygląda mniej więcej tak. Załóżmy, że mam standardowy przykład rejestracji usera.

Idąc od środka w domenie mam encję biznesową User, która w konstruktorze przyjmuje wszystkie parametry jakie powinien mieć user - najczęściej w postaci ValueObjectów typu UserAddress, Password itp. Zazwyczaj nie ma ich wiele, ale ~10 się zdarza.

Dalej ma Service Domenoy w postaci UseCase - w tym przypadku RegisterUserUseCase - do tego UseCasu mam obiekt typu Command - np. RegisterUserCommand, który niektórzy nazywają Requestem (ale nie w rozumieniu Requesta Http tylko requestu UseCasu).

Idąć wyżej - mam Adaptery np. dla HTTP i CLI, ich odpowiedzialnością jest przetłumaczenie natywnego Requestu np. Http na Command jaki przyjmuje dany Use Case -> czyli w tym przypadku kontroller tworzy DTO jakim jest UserRegisterCommand i wywoluje RegisterUserUseCase. W odpowiedzi Use Case zwraca RegisterUserRespone (czyli znowu jakieś DTO), które jest tłumaczone na odpowiedni Response HTTP. Ten sam Use Case używam w adapterze CLI. Robiąc testy domeny (w tym przypadku UseCasa) zazwyczaj punktem wejścia jest właśnie ten DTO, który normalnie jest tworzony w kontolerze albo komendzie CLI.
Zawsze uważałem, że to podejście jest właśnie prawidłowe bo ładnie odwiązuje logikę domenową od infrastruktury i poszczególnych adapterów. Oczywiście celowo pomijam tutaj detale typu używanie interejsów dla odwórcena zależności itp, ale to już taki detal. Wg mnie to całkiem solidna architektura.

I tutaj szczerze mówiąc nie wiem jak uzyskać Command, który zawsze miałbym 2-3 pola w konstruktorze. Jeśli np. jesteśmy w kontekście http i user wysyła formularz z 10 polami koniecznymi do rejestracji usera, to Command też w przybliżeniu będzie miał taką ilość.

Co do drugiej części - czyli tego buildera. To z grubsza dobrze zrozumiałeś mój zamiar. Jak testuje w danym teście jak wpływa wiek usera na proces rejestracji, bo nie widzę powodu dla którego miałbym tworzyć cały Command ręcznie. Dla mnie ważne jest aby w kontekście tego testu wszystkie pozostałe dane spełniały reguły biznesowe dla prawidłowego wywołania dane Use Casa aby nie dostać false positive, a wiek jest tym co w danym teście wpływa na SUT. Nie widzę powodu czemu np. miałbym w 30 testach zmieniać command dodając mu to dodatkowe pole, które nie wpływa na dany test. Oczywiście dodając takie pole jak najbardziej trzeba przejrzeć testy, ale tym bardziej będzie łatwiej wychwycić test gdzie manipuluję polem "city" i tam w pierwszej kolejności patrzeć czy dodanie pola "country" nie zmienia reguł biznesowych.

Kurcze takie rzeczy to chyba najłatwiej byłoby dyskutować twarzą w twarz. Może trzeba stworzyć w ramach 4programmers jakieś cykliczne spotkania typu phpCorner i sobie o tym dyskutować przez godzinkę czy dwie. Coś a'la grupa mastermind - w biznesie się sprawdza to może i w programowaniu byłby dobry pomysł.

0
hadwao napisał(a):

U mnie encja biznesowa to np. User i tworząc Usera nie widzę problemu w tym, że będzie tam 10 parametrów w konstruktorze - używasz tutaj słowa zależność.

Ja tu widzę mega dużo problemów. 10 parametrów, potencjalnie 10 powodów do zmiany. Różne permutacje tych parametrów daje nam 10^n gdzie n to ilość możliwych stanów, tysiące kombinacji. Klasa bardzo trudna do zrozumienia i utrzymania.

W domenie biznesowej, moim zdaniem nieakceptowalne.

Możesz nałożyć na to buildery i inne pomagajki, ale to będzie tylko ukrycie skomplikowanej klasy.

Dla mnie zależność to zależność od jakiegoś serwisu, ale Ty raczej traktujesz to szerzej i np. przyjęcie stringa jako email też traktujesz jako zależność. Także dla mnie tworzenie encji biznesowych przyjmujących dane czy to jako skalary czy ValueObjects (a nie zależności w moim rozumieniu, czyli jakieś inne Servisy) to jest całkiem rozsądna praktyka.

Wszystko jedno co. Jeśli musisz to znać w teście, nie ważne czy to service czy email, musisz to znać w teście, ergo musisz to dostarczyć w teście, ergo Twoja klasa nie może bez tego działać, ergo to zależność. Nazywaj sobie to jak chcesz.

Nie zdarzyło mi się jeszcze tworzyć encji biznesowej ze 100 polami - takie zwyczajnie rozbijam bo np. w Userze adress to już będzie ValueObject albo jakaś inna Encja. Także w praktyce limitem jest tutaj po prostu zdrowy rozsądek.

No widzisz, a mi się nie zdarzyło nigdy stworzyć encji która ma 7 pól. Większość u mnie ma 1-3.

Patrzę na Twoje argumenty z tym że masz 10 parametrów, tak jak Ty byś popatrzył na kogoś kto ma ich 100.

W przypadku obiektów przyjmujących zależności w moim rozumieniu (czyli np. coś wstrzykniętego przez DI) to w 100% się zgadzam - im mniej tym lepiej, ale w przypadku DTO nie widzę powodu czemu miałbym się ograniczać do np. 3 parametrów w konstruktorze.

W DTO nie musisz. Ale potem musisz je przetłumaczyć tak, żeby dało się ich użyć żeby zawołać logikę biznesową, i to już nie może ich mieć dużo.

Idąc od środka w domenie mam encję biznesową User, która w konstruktorze przyjmuje wszystkie parametry jakie powinien mieć user - najczęściej w postaci ValueObjectów typu UserAddress, Password itp. Zazwyczaj nie ma ich wiele, ale ~10 się zdarza.

No, moim zdaniem 10 to dużo za dużo jak mówiłem. Dla mnie spoko to 1-3, przy 4-5 bym się zastanawiał czy by tego nie podzielić, przy 6 na 100% bym je już wydzielił. Nigdy nie doprowadziłbym do sytuacji w której jest ich 10. Dla mnie to za dużo, dlatego że prowadzi DOKŁADNIE do takich problemów jakie masz, czyli buildery w testach, które są takim pain in the ass że musisz je generować.

Dalej ma Service Domenoy w postaci UseCase - w tym przypadku RegisterUserUseCase - do tego UseCasu mam obiekt typu Command - np. RegisterUserCommand, który niektórzy nazywają Requestem (ale nie w rozumieniu Requesta Http tylko requestu UseCasu).

Idąć wyżej - mam Adaptery np. dla HTTP i CLI, ich odpowiedzialnością jest przetłumaczenie natywnego Requestu np. Http na Command jaki przyjmuje dany Use Case -> czyli w tym przypadku kontroller tworzy DTO jakim jest UserRegisterCommand i wywoluje RegisterUserUseCase. W odpowiedzi Use Case zwraca RegisterUserRespone (czyli znowu jakieś DTO), które jest tłumaczone na odpowiedni Response HTTP. Ten sam Use Case używam w adapterze CLI. Robiąc testy domeny zazwyczaj punktem wejścia jest właśnie ten DTO, który normalnie jest tworzony w kontolerze albo komendzie CLI.
Zawsze uważałem, że to podejście jest właśnie prawidłowe bo ładnie odwiązuje logikę domenową od infrastruktury i poszczególnych adapterów. Oczywiście celowo pomijam tutaj detale typu używanie interejsów dla odwórcena zależności itp, ale to już taki detal. Wg mnie to całkiem solidna architektura.

Tak, zmiana requestu HTTP na RequestModel, czy tam jak sobie nazwałeś RequestUseCase, czy DTO, to jest spoko pomysł. Ale to tylko pierwszy krok. Jeszcze musisz użyć tego DTO, żeby zawołać logikę biznesową tak żeby ona nie wiedziała o tych DTO nic.

I tutaj szczerze mówiąc nie wiem jak uzyskać Command, który zawsze miałbym 2-3 pola w konstruktorze. Jeśli np. jesteśmy w kontekście http i user wysyła formularz z 10 polami koniecznymi do rejestracji usera, to Command też w przybliżeniu będzie miał taką ilość.

No zależy czy te dane wszystkie są używane do tego samego (np wszystko to jest adres do przesyłki, kraj, kod pocztowy, ulica, adres, numer mieszkania), czy każdy do czegoś innego (login do logowania, mail do powiadomień, numer telefonu do weryfikacji, imię do wyświetlania, wiek do cenzury, etc.).

Jeśli wszystko jest do jednego celu (co jest raczej rzadkie), to wsadzasz to do klasy Address, i wtedy Twoja command ma jeden argument - Address $address. Zapytasz pewnie jak to przetestować - otóż tak żę przekazujesz do Command w teśćie implementację Address która robi to co ma robić Fake/Mock/inna implementacja/implementacja z defaultami. Ale do tego nigdy nie potrzebujesz builderów bo to są gołe dane, a nie enjce. Tak na prawdę mógłbyś to wręcz trzymać w array.

Jeśli natomiast różne pola robią różne rzeczy, to tak na prawdę to co nazywasz "jedną Command" tak na prawdę jest wieloma commandami, które robią wiele rzeczy, i powinieneś je rozdzielić IMO.

Jak testuje w danym teście jak wpływa wiek usera na proces rejestracji,

...taaak, ale w połączeniu z 14oma innymi implicit parametrami. Więc tak na prawdę to ja nie byłbym taki pewien czy ten test testuje jedną rzeczy czy nie. Moim zdaniem chyba nie.

bo nie widzę powodu dla którego miałbym tworzyć cały Command ręcznie.

Gdyby byl prosty, to byłoby to banalne, np tak new Command(a,b,c). To że u Ciebie to wygląda tak new Command(a,b,c,d,e,f,g,h,i) to moim zdaniem źle zaprojektowane API.

Dla mnie ważne jest aby w kontekście tego testu wszystkie pozostałe dane spełniały reguły biznesowe dla prawidłowego wywołania dane Use Casa aby nie dostać false positive, a wiek jest tym co w danym teście wpływa na SUT.

Nie widzę powodu czemu np. miałbym w 30 testach zmieniać command dodając mu to dodatkowe pole, które nie wpływa na dany test.

Ale ono już by było dodane czy tego chcesz czy nie. Pytanie tylko czy byłoby dodane jawnie (lepiej), czy nie jawnie, za pośrednictwem tego builder.

Oczywiście dodając takie pole jak najbardziej trzeba przejrzeć testy, ale tym bardziej będzie łatwiej wychwycić test gdzie manipuluję polem "city" i tam w pierwszej kolejności patrzeć czy dodanie pola "country" nie zmienia reguł biznesowych.

To jest tylko jeden i prosty przypadek. Są inne case'y, np test shouldNotDoSomethingWhenNoCityIsSet, i tam już nie będzie tego withCity() więc nie znajdziesz tego tak łatwo.

Ale to co naprawdę byłoby łatwo znaleźć, to nie 30 testów Commanda, tylko 3 testy klasy która ma 3 pola, zamiast 10-15. Gdyby wszystkie Twoje klasy miały 1-3 zależności, to Twoje testy byłby dużo mniejsze i prostsze, nie byłoby takiego problemu jak "znaleźć test", nie musiałyś ich szukać, ponieważ byłby tak proste, że ich intencje byłyby oczywiste. Mając klasę która ma 10-12 parametrów, nie ma mowy o czystych i prostych testach. Owszem, możesz sobie je próbować schować w builder, ale to nie sprawi że będzie ich prościej szukać/zmieniać/rozwijać/edytować. Po pierwsze teraz testy są zależne od buildera, oraz są nie bezpośrednio związane również ze złamaną enkapsulacją.

Czy na prawdę nie widzisz tego, że klasa która ma 1-3 argumenty MUSI być prosta i łatwa (zakładając żę jesteś dobrym programistom)? Nigdy nie będziesz potrzebował buildera ani innych pomagajek. Złamać SRP jest znacznie ciężej, Twoja architektura sama się zrobi. Przekonanie że 10 parametrów w klasie jest "czasem okej" to jest Twój największy problem IMO. Moim zdaniem to jest zawsze problem (w domenie bizesowej, w DTO i requestach jest ok).

Pracowałem kiedyś w taki sposób w jaki opisujesz, więc wydaje mi się że wiem co myślisz - wydaje mi się że wiem, bo kiedyś myślałem podobnie. Dużo czasu zajęło mi wyrobienie na tyle dyscypliny żeby umieć się przed tym zabezpieczyć i umieć zrobić dobre aplikacje.


Mam wrażenie że schodzisz na dziwny temat.

  • Najpierw rozmowa zaczęła się od tego, że moim zdaniem generatory ukrywają skomplikowane API.
  • Ty dałeś kontr argument z builderami
  • Ja powiedziałem żę z reguł buildery chowają skomplikowane API, i builder to tylko maskowanie ich
  • Teraz Ty dajesz przykład z Commandem który wchodzi do Twojej logiki biznesowej z 10 parametrami.

Czyli mam rozumieć że tamte tematy mamy załatwione?

3

Ja tu widzę mega dużo problemów. 10 parametrów, potencjalnie 10 powodów do zmiany. Różne permutacje tych parametrów daje nam 10^n gdzie n to ilość możliwych stanów, tysiące kombinacji. Klasa bardzo trudna do zrozumienia i utrzymania.

No nie przesadzajmy, wystarczy prosty walidator, który sprawdzi czy nie mamy nulla (chyba, że taki jest dopuszczony) plus ewentualnie sprawdzi sensowność podanych wartości - czy np. nie masz jako wiek klienta podanego 35432234.

Można ewentualnie jeszcze sprawdzać, czy określone dane, które mają być unikalne w bazie, się nie powtarzają. Aczkolwiek - to jest do ustalenia, na jakim poziomie ma się odbywać ta walidacja, bo być może podczas tworzenia sprawdzisz jedynie wspomniane nulle czy inne podstawowe założenia, a reszta będzie gdzieś dalej ogarniania, w logice biznesowej. To zbyt ogólne hasło żeby teraz gadać o konkretach, ale jedynie chodzi mi o to, że nie zgadzam sie z 10^n kombinacji. Bo owszem, jest tyle możliwości błędnego wprowadzenia danych, ale nie oznacza że masz te miliardy wariantów rozpatrywać. Każde pole walidujesz indywidualnie i jeśli którekolwiek z nich nie przejdzie testu, to uwalasz całość. I tyle. Nie twórzmy paniki, że trzeba miliardy wariantów obsłużyć, bo to trochę na zasadzie nie używaj typu int, bo nawet przy 32 bitach masz ogromną liczbę wartości do rozważenia :P

0
cerrato napisał(a):

Ja tu widzę mega dużo problemów. 10 parametrów, potencjalnie 10 powodów do zmiany. Różne permutacje tych parametrów daje nam 10^n gdzie n to ilość możliwych stanów, tysiące kombinacji. Klasa bardzo trudna do zrozumienia i utrzymania.

No nie przesadzajmy, wystarczy prosty walidator, który sprawdzi czy nie mamy nulla (chyba, że taki jest dopuszczony) plus ewentualnie sprawdzi sensowność podanych wartości - czy np. nie masz jako wiek klienta podanego 35432234 ;)

Można ewentualnie jeszcze sprawdzać, czy określone dane, które mają być unikalne w bazie, się nie powtarzają. Aczkolwiek - to jest do ustalenia, na jakim poziomie ma się odbywać ta walidacja, bo być może podczas tworzenia sprawdzisz jedynie wspomniane nulle czy inne podstawowe założenia, a reszta będzie gdzieś dalej ogarniania, w logice biznesowej. To zbyt ogólne hasło żeby teraz gadać o konkretach, ale jedynie chodzi mi o to, że nie zgadzam sie z 10^n kombinacji. Bo owszem, jest tyle możliwości błędnego wprowadzenia danych, ale nie oznacza że masz te miliardy wariantów rozpatrywać. Każde pole walidujesz indywidualnie i jeśli którekolwiek z nich nie przejdzie testu, to uwalasz całość. I tyle. Nie twórzmy paniki, że trzeba miliardy wariantów obsłużyć, bo to trochę na zasadzie nie używaj typu int, bo nawet przy 32 bitach masz ogromną ilość wartości do rozważenia :P

Ale mi nie chodzi o kombinacje względem kodu jaki się wykona w niej/walidacja.

Ja mówię o tym, że ciężko jest koncepcyjnie myśleć o tej klasie, jeśli jest taka skomplikowana. Ja patrząc na klasę chcę wiedzieć czego się mogę od niej spodziewać. Jeśli mam 10 parametrów, i 10^n ich kombinacji, to ciężko wydedukować co może do niej wejść i co ona może zrobić.

Samo mówienie o takiej klasie jest trudne, i utrudnia jej zrozumienie. Łatwo powiedzieć że "Adres dostaje ulicę oraz numer", zamiast "User dostaje XService, YService, ZService, X1Service, X2Service" etc. Lubimy o sobie myśleć jako o inteligentnych ludziach, ale prawda jest taka że jeśli mamy w swoim projekcie zbyt dużo zbyt skomplikowanych klas, to utrzymanie takiego projektu przerasta nawet najlepszych z nas. Umiejętność dobrego wytwarzania aplikacji to nie umiejętność obeznania się w burdelu i dobudowania do niego builderów i pomagajak; a umiejętność nie robienia burdelu wcale.

Poza tym, ten "prosty walidator" też trzeba otestować, i im więcej tych kombinacji tym się to staje trudniejsze, a testy mniej rzetelne.

Poza tym; ciężko może nam być też przyznać przed samym sobą, że może jednak nie jesteśmy tak dobrzy. Każda książka o czystym kodzie powie nam, żęby ograniczać złożoność aplikacji, nie robić metod i obiektów które mają zbyt dużo argumentów/pól/powodów do zmiany. Ale kiedy będąc grzeszni zrobimy taki kod; często zamiast przyznać przed sobą: "zrobiłem zły kod, muszę się douczyć i poprawić" odrzucamy tą myśl, i zamiast tego wybieramy "tak, 10 parametrów tutaj to dobry pomysł".

1 użytkowników online, w tym zalogowanych: 0, gości: 1