Beszédünk van. Múltkor azt állítottam, hogy az MVC-t szinte mindenki ismeri a szakmában. Az az igazság, hogy sokan azt hiszik, hogy ismerik.

Mielőtt azonban belevágunk, nézzük is meg, hogy mi az az MVC valójában.

Mi is az az MVC?

A Model-View-Controller elv alapötlete nem rossz. Adott egy vezérlő (controller), ami megkapja a felhasználótól a kérést egy bizonyos művelet elvégzésére, majd a modellből megszerzi az adatot, amit átad a view-nak megjelenítésre. A lényege a megjelenítés leválasztása a működési logikáról. (Aki keresett már hibát kevert HTML és PHP kódban, az tudja, miről beszélek.)

mvc

Az MVC tervezési mintát Trygve Reenskaug találta ki a 70-es években a Smalltalk nevű nyelvre. Ahogy a kétezres években a web hatalmas teret nyert, gyakorlatilag de-facto szabvánnyá vált és szinte eretnekségnek számít a mai napig nem MVC-re építeni egy webes alkalmazást.

Tudtad?
Smalltalkban minden egyes gombnak, szövegmezőnek, egyéb megjeleníthető elemnek volt egy MVC-je, koránt sem volt az a mindent átfogó, globális tervezési minta, amiként ma használatos.

Hol a hiba ebben?

Leginkább sehol. Az elv szép tiszta. A gond ott van, ahogyan használod.

Mire is gondolok? Nézzünk egy pár példát modern webes frameworkök dokumentációiból:

// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

class HelloController
{
    public function indexAction($name)
    {
        return new Response('<html><body>Hello '.$name.'!</body></html>');
    }
}

<?php

namespace App\Http\Controllers;

use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    /**
     * Show the profile for the given user.
     *
     * @param  int  $id
     * @return Response
     */
    public function showProfile($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

@Controller
public class HelloWorldController {

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World!");
        return "helloWorld";
    }
}

Mint látható, a controller minden esetben közvetlenül foglalkozik legalább egy, a webhez kapcsolható dologgal. Ilyen vagy olyan módon, de a Controller felelős a webes kiszolgálás egy részéért és az üzleti logikáért (vagyis ezekben a példákban az adatok beszerzéséért) is.

Mi történik akkor, ha holnap odajön a főnököd, hogy márpedig egy rakás adatot módosítani kellene? Megkerülöd az alkalmazás-logikát és közvetlenül módosítasz az adatbázisban? Esetleg írsz egy scriptet ami a weboldalt hívogatja? Vagy írsz egy konzolos alkalmazást, ami megvalósítja a kívánt funkciót?

Hoppá, az utóbbit nem lehet, hiszen az üzleti logikád legalább egy része a controllerben lakik! Hiszen ahogy az az MVC-ben le volt írva, az adatok beszerzését (SQL lekérdezések, stb) betetted a modellbe, a működési logika pedig a controllerben van.

A Business Logic réteg

Na ezen probléma kikerülésére találták ki a dotkom lufi kidurranása után azt, hogy a controller és a model között szükség van egy Business Logic rétegre. Ez alapvetően egy jó gondolat, de a gyakorlati megvalósítás finoman szólva problémás volt. Rengeteg programban ugyan van Business Logic réteg, de az üzleti logika egy része továbbra is a controllerben lakik, a BL réteg pedig nem biztosít szép, jól dokumentált felületet az üzleti logika megszólítására, mert igény szerint, ad-hoc nőtt.

Ha szeretnénk egy tiszta Business Logic réteget, meg kell szívlelnünk a sorozat korábbi részeiben írtakat. Csoportosítsuk tehát a Controllert és a Viewt és szögezzük le, hogy ez a programunk azon része, amely például a webes kiszolgálással, megjelenéssel foglalkozik. Elfogadjuk, hogy ezek a programrészek mindig függeni fognak valamilyen szinten a platformtól, amivel foglalkozunk. Ha web, akkor itt lesznek URL-ek, ha CLI, akkor itt lesz valamilyen konzolos működésre vonatkozó program kód.

Nevezzük ezt a részt, ami a platformmal foglalkozik, delivery (szállító) mechanizmusnak, és kössük ki, hogy ebben semmilyen üzleti logika nem foglalhat helyet. Az üzleti logikát úgy kell megalkotnunk, hogy ettől a delivery mechanizmustól teljesen független legyen.

boundary

Ezzel a módosítással elérjük azt, hogy akárhány delivery mechanizmust rá tudunk kötni ugyanarra az üzleti logikára. Egy gond viszont van, ezzel elég komolyan megsértettünk egy S.O.L.I.D. elvet, nevezetesen a függőség megfordítás elvét.

Tegyük fel, hogy van egy rendelés-feldolgozó rendszered, és az üzleti logikát implementáló osztályod OrderBusinessLogic névre hallgat. Ha ezt a logikát úgy kell lecserélned, hogy az alkalmazás egyik része még a régi logika szerint működik, a másik része már az új logika szerint, akkor gondban vagy. Hiszen mehetsz végig azokon a programrészeken, amelyek az új működést használják, és írhatod át mindenhol, hogy milyen osztályt kellene használni.

A Boundary és az Interactor

A korábban bemutatott módszer szerint fordítsuk meg a függőséget azzal, hogy bevezetünk egy interface-t. Nevezzük ezt az interfacet Boundary-nak (ejtése: baundri), az ezt megvalósító tényleges osztályt pedig Interactornak. Tehát például így:

interface OrderBoundary {
    OrderResponse placeOrder(OrderRequest request);
}
class OrderInteractor implements OrderBoundary {
    OrderResponse placeOrder(OrderRequest request) {
        // ...
    }
}

Látható, hogy a függvényt meghívjuk egy OrderRequest osztállyal, és visszatér egy OrderResponse válasszal. Természetesen megtehetjük, osztályok helyett egyszerű paramétereket használunk, azonban ez a későbbi bővíthetőség szempontjából hátrányos lehet, hiszen gondoljunk csak bele: egy megrendelésnek meglehetősen nagy számú paramétere van, és bármikor hozzá jöhet még egy.

boundary-interactor

Felmerül a kérdés, hogy hogyan használjuk ezt? Hiszen attól, hogy van egy interface-ünk, még valahol létre kell hozni az Interactor osztályt. És a kérdés jogos. Szerencsére a mai MVC frameworkök nagy része rendelkezik egy Dependency Injection módszerrel (például egy Dependency Injection Containerrel). A modernebb változatok képesek felismerni azt, ha valamilyen paraméterre szükségünk van, tehát írhatunk például ilyet:

class OrderController {
    private OrderBoundary orderBoundary;

    OrderController(OrderBoundary orderBoundary) {
        this.orderBoundary = orderBoundary;
    }

    public void placeOrderAction() {
        //...
        orderBoundary.placeOrder(...)
    }
}

Magyarán a framework a konfiguráció alapján automatikusan paraméterezi a controllerünket, és biztosítja azt, hogy rendelkezésre álljon egy, az OrderBoundary interface-t megvalósító objektum.

Az Entity

Az Interactor képviseli azt az üzleti logikát, ami a Boundary túloldaláról látható, az az alkalmazás-függő. Van azonban egy másik fajta üzleti logika is, ami a belső adatstruktúránkat képezi le.

Ha maradunk a megrendelés-kezelő rendszernél, ilyen adatstruktúrák lehetnének például a Felhasználó, a Megrendelés, vagy a Szállítási cím, illetve ezek viszonya egymáshoz. Ezeket gyakran Business Object, Data Object vagy Entity néven emlegetik. (Nem összekeverendő a különböző ORM megoldások entitásaival!)

Nézzünk egy példát:

class Order {
    public void setCustomer(Customer customer) { ... }
    public Customer getCustomer() { ... }
    //...
}

Semmi bonyolult, ugye? De vajon miért nem használjuk ezeket az entitásokat a Boundaryn keresztüli kommunikációhoz? A válasz viszonylag egyszerű: ha változik a belső adatstruktúránk, nem szeretnénk az összes Delivery mechanizmust átírni. Ha fejlesztettél már hosszabb távon működő alkalmazást, ez a probléma ismerős lehet.

Bonyolultabb alkalmazás fejlesztésénél mindig érdemes rétegekre bontani az alkalmazást, és a rétegek közötti függőségeket minimalizálni.

Tipp:
Amikor Entity-ket tervezel, ne adatbázis táblákban gondolkozz! Rengeteg előnye van, ha a konkrét adatbázis-motor kiválasztását későbbre halasztod, így az Entity-k legyenek függetlenek az adatbázis szerkezetétől!

Az Entity-Boundary-Interactor tervezési minta

Az, amit eddig leírtunk, semmi más, mint az Ivar Jacobson és Robert C. Martin munkáján alapuló Entity-Boundary-Interactor tervezési minta. Összességében nézve így néz ki:

ebi

Mi a helyzet az adatbázissal?

Egy dologról még nem beszéltünk: az adatbázisról. Az előző cikkben leírtakból és az EBI jellegéből adódóan kiindulva ezt is le kell választanunk, nemde?

entity-gateway

Robert C. Martin a következőt javasolja: legyen egy, az EBI-hez hasonló mechanizmus, az Entity Gateway, ami felelős azért, hogy a kapott Entityket átalakítsa az adatbázisnak megfelelő formára, tehát például SQL lekérdezésekké. Ugyanez az objektum felelős azért, hogy az adatbázisból kiolvasson adatokat és Entitykké alakítsa őket.

Ahogy azt már megszokhattuk, ezt a gatewayt leválasztjuk egy interface segítségével, ami definiálja a megvalósítandó függvényeket, például: getUserById, getUserByEmail, stb.

Ismerős? Naná, ez a régi fogalmaink szerinti DAO.

Most ez komoly?

Bevallottan, az MVC egyszerűségéhez képest ez az architektúra meglehetősen bonyolult, és nem is feltétlenül érdemes minden áron implementálni. Ha viszont egy bonyolultabb alkalmazás fejlesztése elé nézünk, vagy egy egyszerűnek indult alkalmazás kezd elbonyolódni, érdemes megfontolni a bevezetését.

Az EBI architektúra hatalmas előnye, hogy leválasztja a mindenféle kimeneti-bementi csatornákat, mind a felhasználói oldalról, mind az adatbázis részéről, így az alkalmazás központi logikája egyszerűen és gyorsan tesztelhetővé válik. Ha nem is implementálod a teljes EBI-t, ezt a fajta leválasztást mindenképpen érdemes bevezetni.

TL;DR
Az üzleti logikád ne függjön a webtől vagy az adatbázisodtól! Teljesen válaszd le!

Források