Kb. 2 éve olvastam először a fluent interface-ekről. Akkor nagyon megtetszett, és azóta is folyamatosan alkalmazom a módszert. Nem is gondoltam akkor, hogy Java-ban (vagy C#-ban) a legalapvetőbb nyelvi eszközökkel ilyen kifejező kódot lehet írni.
Az alapprobléma
Az eredeti probléma abban áll, hogy hogyan valósítsuk meg viszonylag sok paraméter átadását a kódban. A legalapvetőbb megoldás a standard módszer:
Jelen esetben 12 paraméter átadásáról van szó. Mondanom sem kell, hogy kínszevedés kliensként az ilyen metódust hívni, hiszen ahhoz, hogy tudjam, mit kell éppen átadni, az IDE-t kell segítségül hívnom. Ráadásul a keletkező kódot ilyen esetben borzasztó utólag is olvasni, hiszen nem rí le róla, hogy a híváskor milyen paraméterket, milyen sorrendben is adok át.
Martin Fowler Refactoring c. könyvében az Introduce Parameter Object c. minta próbál egyszerűsíteni a problémán, és azt javasolja, hogy rakjuk egy objektumba (osztályba) ezeket a paramétereket, majd ezt az objektumot adjuk át.
Sajnos, a gond még mindig ugyanaz, hiszen magát a RegistrationData típusú objektumot is fel kell töltenünk ugyanezekkel a paraméterekkel. Megoldás lenne a setter-ek alkalmazása, de őszintén szólva, ha ilyet meglátok a kódban, egy kisebb agyvérzést kapok. Mivel mások által sem javasolt az objektumok kívülről történő piszkálása (sérti az egységbezárás elvét), célszerű más megoldást keresni, és itt jönnek a fluent interface-ek a képbe.
A megoldás
Kliens oldalon a következő a használandó kód (pl. egy tesztben):
A RegistrationData osztály implementációja a következő:
A módszer előnye az, hogy amikor olvassuk a kódot, látszik, hogy a hívás pillanatában milyen paraméterek kapják a megfelelő értékeket. Ezért is kapta a fluent nevet a minta, hiszen folyamatosan, gondolkozás nélkül olvashatjuk a kódot, “folyékonyan”, mintha természetes nyelven lenne írva.
Látható az is, hogy nem muszáj egy az egyben átkonvertálni minden paramétert egy with… hívássá, azok csoportosíthatóak nagyobb, jól nevesíthető tömbökbe. Például a firstName-et és lastName-et együtt a withName… metódussal olvassuk be. (Viszont egy ilyen with… hívásba sem szabad sok paramétert bezsúfolni, mert akkor ugyanott tartunk, ahonnan elindultunk.)
A módszer egyetlen hátránya, hogy – a standard, sokparaméteres átadással szemben – nem kényszeríti ki az összes paraméter átadását. Az eredeti példa pl. nem fordul addig, amíg át nem adunk 12 akármilyen paramétert (a típusoknak azért egyezniük kell). Ezt azzal szokták orvosolni, hogy a paraméterek átadása után hívnak egy validate(), void visszatérési értékű metódust, amely elszáll, ha valamilyen paraméter nem lett kitöltve. Másik módszer a probléma megoldására, ha teszteket írunk. Amúgy – megsúgom – ha kihagyunk véletlenül egy paramétert, azt rövid időn belül úgyis észrevesszük, és korrigálhatunk, tehát nem célszerű emiatt túlságosan aggódni. Amit viszont cserébe kapunk, az a nagyfokú olvashatóság.
Kristof Jozsa
Jan 29th, 2010Nem szeretem.. igazad van hogy a fluent interface-n látszik, hogy melyik érték melyik propertyhez tartozik, csak pont azt nem kommunikálja hogy hányat kéne még megadni, így mindenképp doksit kell olvasnom, még az IDE sem segít. Egy standard konstruktor pont fordítva, de az legalább előtérbe tolja az encapsulationt és nem csinálhatsz félkész objektumot.. de vitatkozzunk
Kristof Jozsa
Jan 29th, 2010ja és nyilván ha több alternatív konstruktor van akkor static factory method, ha még komplexebb a logika akkor factory..
pityufiu
Jan 29th, 2010Szerintem kimerítettük a témát Én a kódolvashatóságot tartom ebben az esetben az előtérben, dehát mindannyian mások vagyunk, és ez a szép
pcjuzer
Jan 29th, 2010Egyszerűbb POJO-k inicializálására szerintem pont nem annyira jól használható, de pl. fastruktúrák kialakításához tökéletes. (ld. Hibernate Criteria API)
pityufiu
Jan 29th, 2010A saját projektünkben nem ritka, hogy 30-40 adatot is át kell tölteni. Ezekben az egyszerű adattípusokon kívül még tipikusan listák is szoktak szerepelni. (Szolgáltatás rétegről van szó.) Nekünk elég jól működik a fluent interface, és nem cserélném le másra.
Másrészről a Hibernate-es dologra kitérve: én személy szerint nem szeretem használni a Hibernate Criteria API-t, mert nehezebb áttekinteni, hogy miről is szól a query. Ráadásul elég hamar performancia problémákba ütközik az ember, ha nagyobb adatmennyiséggel dolgozik, tehát a natív queryk egy idő után elkerülhetetlenné válnak.
Az egyszerűbb queryk esetében használunk csupán HQL-t (és akkor is a sztringek direktben szerepelnek a kódban). Sok esetben az IntelliJ IDEA képes a sztringekben megfogalmazott queryket is refaktorálni. (Ha nem jönne össze, a tesztek meg úgyis elszállnak.)
karenin
Jan 30th, 2010Én szeretem, de szerintem se a fenti eset a jó példa a szükségességére (sőt). Inkább a liquidform vagy a mockito API-ját mondaná, nekem azok elég szépek.
pcjuzer
Feb 3rd, 2010A Criteria API-t nem dícsértem, csak felhoztam példának. Mi akkor használjuk amikor dinamikusan kell összeállítani a query-t.
pityufiu
Feb 3rd, 2010pcjuzer: Igen, tudom Csak gondoltam, ha már ráterelődött a szó, akkor a napi igét oda is elhintem egy kicsit:) Amúgy a Hibernate használata megérne (legalább) egy külön posztot is. (És kiváncsi lennék mások véleményére is!)
Amúgy azóta már rájöttem, hogy tényleg elég szerencsétlen volt a példaválasztás, szemléletesebb lett volna, ha például már meglévő frameworkökből hozok fel példát. Pl. JMock és társai ( egy példa: http://www.jmock.org/jmock1-dispatch.html )
Viczián István
Feb 7th, 2010Egy alternatív, bár nagyon hasonló megoldás a builder tervezési mintával: készítesz egy RegistrationDataBuilder osztályt ezekkel a metódusokkal, és az állítja össze a végén a RegistrationData objektumot. Ez talán oo-bb, encapsulation-t megtartja, a mindenféle validáció bele tehető a végén a RegistrationData-t visszaadó metódusba, stb.