Nástup funkcionálnych jazykov môže vytvárať predstavu, že objektovo orientovaný prístup je na ústupe. Avšak opak je pravdou. Funkcionálne programovanie veľmi dobre podporuje objektovo orientovaný dizajn.
Nasledujúci príklad demonštruje využitie funkcionálneho programovania na podporu princípu jednoduchej zodpovednosti („Single responsibility principle“), ktorý je jedným zo základných princípov kvalitného objektového dizajnu. Vychádza z modelu bodu v dvojrozmernom priestore so súradnicami [x,y]. Bod môže mať v jazyku Java takúto implementáciu:
/** Class responsibility: to provide coordinates for one point in 2D. */ public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } // ugly formatting - just to save a space public int x() { return x; } public int y() { return y; } }
Zodpovednosťou triedy je vedieť poskytnúť súradnice daného bodu v dvojrozmernom priestore. Preto trieda nemá bezparametrický konštruktor, aby neumožnila programátorovi vytvoriť objekt typu Point bez súradníc. Trieda neobsahuje žiadne set-metódy, takže oba jej atribúty sú „final“.
Môže sa v analýze vôbec objaviť požiadavka na zmenu súradníc? Čo napríklad matematická operácia známa ako „posunutie“?
/** Move point in specified direction. */ // COMPILER ERROR public void move( int deltaX, int deltaY ) { this.x += deltaX; this.y += deltaY; }
Pri pokuse o implementovanie posunutia v triede „Point“ vyhlási kompilátor chybu priradenia novej hodnoty atribútom X a Y, pretože sú v triede Point deklarované ako „final“. Môžu sa hodnoty atribútov X a Y zmeniť?
„Point“ je objekt, ktorého identita je daná hodnotou jeho atribútov. Bod so súradnicami [2,3] je iný objekt ako bod so súradnicami [3,4]. Doménovo je teda nezmysel hovoriť, že bodu [2,3] zmeníme súradnice na [3,4].
Priame zmeny súradníc navyše vedú aj k neželaným chybám v programe. Napríklad programátor môže použiť rovnaký objekt typu Point ako stred pre dve rôzne kružnice s rovnakým stredom. Ak však následne zmení atribúty [x,y] pre prvú kružnicu, tak sa zmení stred aj druhej kružnice.
„Posunutie bodu“ neznamená zmenu súradníc bodu, ale vznik nového bodu:
/** Creates new point moved in specified direction. */ // STILL BAD public Point move( int deltaX, int deltaY ) { return new Point( x + deltaX, y + deltaY ); }
Metóda sa síce dá skompilovať, ale trieda Point utrpela ťažkú dizajnérsku ujmu. Triede pribudla nová zodpovednosť, čiže porušuje „Single responsibility principle“. Zodpovednosťou triedy „Point“ je poskytovať údaje o bode v rovine. Na to slúžia metódy x() a y(). Metóda „move“ prináša triede Point novú zodpovednosť. Trieda po novom implementuje doménovú logiku pre posunutie bodu v rovine. Čiže metóda „move“ nepatrí do triedy Point. V tomto okamihu neexistuje žiaden vhodný analytický pojem, kam by bolo možné metódu „move“ presunúť. Metóda „move“ je z pohľadu dizajnu doménová služba:
public final class PointServices { /** Creates new point moved in specified direction. */ public static Point movePoint( Point original, int deltaX, int deltaY ) { return new Point( original.x() + deltaX, original.y() + deltaY ); } }
Posunutie bodu teda nie je metódou, ale funkciou. Trieda „Point“ je nemeniteľná (immutable) a vhodná pre využívanie techniky funkcionálneho programovania. Pôvodnú metódu „move“ je možné v Java 8 zapísať ako funkciu „movePoint“:
BiFunction<Point, Movement, Point> movePoint = ( point, movement ) -> new Point( point.x() + movement.deltaX(), point.y() + movement.deltaY() );
Pri definovaní funkcie je použitá nová trieda „Movement“, ktorá obsahuje atribúty „deltaX“ a „deltaY“. Použitie funkcie je vidno z nasledujúceho unit testu:
@Test void testPoint_2_3isMovedBy_1_1to_3_4() { Point pointX2Y3 = new Point( 2, 3 ); Point newPoint = movePoint.apply(pointX2Y3, new Movement(1, 1)); assertEquals( new Point( 3, 4 ), newPoint ); }
Obe implementácie „movePoint“ sú z pohľadu dizajnu ekvivalentné. Nie je chybou, ak programátor v Jave nevyužije možnosti funkcionálneho programovania a ponechá „movePoint“ ako statickú metódu v triede „PointServices“. Avšak prehlásiť „movePoint“ za funkciu je predsa len o niečo lepšie práve kvôli tomu, že zodpovedá chápaniu posunutia v doméne.
Pri navrhovaní tried je kľúčové držať sa domény a rešpektovať jej pojmy a pravidlá. Objektovo orientovaný prístup dáva programátorom do rúk možnosť preniesť analytický objektový model domény do zdrojového kódu. Dôležité je udržať zapúzdrenosť objektu a rešpektovať princíp jednoduchej zodpovednosť.
Funkcionálne programovanie je vhodné tam, kde analýza nájde nemeniteľné (immutable) objekty a takzvané „čisté“ funkcie. Definície funkcií pomáhajú udržať v modeli zásady kvalitného objektového dizajnu.
Funkcionálne programovanie tak predstavuje veľmi vhodný doplnok k objektovo orientovanému programovaniu. A naopak. Výborným predpokladom na zmysluplné využitie funkcionálneho programovania je kvalitný objektový dizajn.
V prípade záujmu o podrobnejšie štúdium tejto témy je vhodné prejsť si nasledujúce zdroje:
- Veľmi dobrá stránka so zoznamom základných princípov objektového dizajnu.
- Java 8 API pre prácu s funkciami a všeobecne s lambda výrazmi.
- Popis narábania s „high order“ funkciami – krátky, ale náročnejší článok.