Az infokukac blog 2010. márciusában átköltözött. Átirányítás folyamatban...

Az átirányítás automatikus. Ha mégsem sikerülne, látogasd meg az új oldalt a http://infokukac.com címen, és frissítsd a könyvjelzőidet!

2010. január 29., péntek

Fluent interface

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:

void doRegistration(String firstName, 
                    String lastName, 
                    String email, 
                    String password, 
                    String country, 
                    String city, 
                    String streetAddress1, 
                    String streetAddress2, 
                    String postalCode, 
                    boolean male, 
                    int age, 
                    boolean wantsNews) {

// ...
}

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.

void doRegistration(RegistrationData data) {
// ...
}

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):

RegistrationData registrationData = new RegistrationData()
    .withName("Béla", "Kovács")
    .withAccount("bela.kovacs@gmail.com", "pass1234")
    .withCountry("Hungary")
    .withCity("Budapest")
    .withAddress("Kossuth Lajos utca 13.", ""),
    .withPostalCode("1234"),
    .withMale(true)
    .withAge(20)
    .withWantsNews(false);

doRegistration(registrationData);

A RegistrationData osztály implementációja a következő:

public class RegistrationData {
    private String firstName;
    private String lastName;
    // további mezők

    RegistrationData() {
    }
    
    RegistrationData withName(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        return this;
    }

    // további with-ek

    String getFirstName() {
        return firstName;
    }

    // további getter-ek (ha szükségesek egyáltalán)
}

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.



9 megjegyzés:

Blogger Kristof Jozsa írta...

Nem 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 :)

2010. január 29. 10:22  
Blogger Kristof Jozsa írta...

ja és nyilván ha több alternatív konstruktor van akkor static factory method, ha még komplexebb a logika akkor factory..

2010. január 29. 10:26  
Blogger Marhefka, István írta...

Szerintem 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 :)

2010. január 29. 13:02  
Blogger tvik írta...

Egyszerű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)

2010. január 29. 15:06  
Blogger Marhefka, István írta...

A 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.)

2010. január 29. 15:22  
Blogger elek írta...

É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.

2010. január 30. 13:42  
Blogger tvik írta...

A 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.

2010. február 3. 15:49  
Blogger Marhefka, István írta...

pcjuzer: 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 )

2010. február 3. 16:38  
Blogger István írta...

Egy 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.

2010. február 7. 13:30  

Megjegyzés küldése

Megjegyzés: Megjegyzéseket csak a blog tagjai írhatnak a blogba.

Feliratkozás Megjegyzések küldése [Atom]

<< Főoldal