Kihagyás

Either (Vagy)

Kategória: Modern minta (Modern Pattern)

Lényege és célja

Az Either egy generikus konténer típus, ami pontosan kétféle értéket tud magában hordozni: egy bal (Left) vagy egy jobb (Right) oldalit. Konvenció szerint a Right a sikeres eredmény, a Left pedig a hiba (a "right" magyarul "jobb" és "helyes" is – innen jön a megnevezés).

A minta a funkcionális programozásból érkezett (Haskell, Scala, Kotlin Arrow, Vavr), és a kivételek helyett ad egy típusos, explicit módot a hibakezelésre: a metódus visszatérési értéke maga árulja el, hogy sikerült-e a művelet.

Miért jobb ez a kivételeknél (legalábbis bizonyos esetekben)?

  1. A hiba szerepel a típusban: Either<String, User> – a hívóra rá van kényszerítve, hogy gondoljon a hiba ágra. Egy sima User findUser(int) szignatúrából nem derül ki, hogy dobhat UserNotFoundException-t.
  2. Nincs control-flow kivétellel: a kivételek drágák, és könnyen "elveszhetnek" több réteg között. Az Either egy normál visszatérési érték, ami pontosan ott van, ahol odatettük.
  3. Komponálható: funkcionális stílusban (map, flatMap) több, hibára futó művelet láncolható úgy, hogy az első Left esetén a többi automatikusan kimarad.

Either vs. Optional vs. Exception

  • Optional<T>: "van érték vagy nincs" – nem mondja meg, miért nincs.
  • Either<L, R>: "van érték vagy van egy konkrét hibainfó" – a Left oldalon vissza tudunk adni hibaüzenetet, hibakódot, validációs listát stb.
  • Exception: váratlan, "kivételes" eseményekre való (programhiba, IO megszakadás). Várt üzleti hibákra (pl. "nincs ilyen user") a Either tisztább.

Megjegyzés: A JDK alapból nem tartalmaz Either-t. Éles projektekben legtöbbször a Vavr (io.vavr.control.Either) vagy a Functional Java könyvtár implementációját használjuk – a példában lévő saját változat csak a minta lényegét mutatja be.

Mikor használjuk?

  • Amikor egy művelet várhatóan két, jól definiált kimenetel közül adhat egyet (sikeres eredmény, vagy ismert üzleti hiba), és mindkettőt típusosan akarjuk kezelni.
  • Validációnál: a Left oldalon visszaadunk egy listányi hibaüzenetet, a Right oldalon a validált objektumot.
  • Funkcionális stílusú láncolt műveleteknél, ahol a hibát nem akarjuk kivételként tovább dobni a teljes hívási láncon.

Mermaid Diagram

A diagram az Either<L, R> generikus szerkezetét és a két gyártófüggvényt (left, right) mutatja, valamint egy konkrét használatát (UserService), ahol a Left oldal egy hibaüzenet (String), a Right pedig a sikeres eredmény (User).

classDiagram
    class Either~L, R~ {
        -left: L
        -right: R
        -Either(left: L, right: R)
        +left(value: L)$ Either~L, R~
        +right(value: R)$ Either~L, R~
        +isLeft() boolean
        +isRight() boolean
        +getLeft() L
        +getRight() R
    }

    class UserService {
        +findUser(id: int) Either~String, User~
    }

    class User {
        -name: String
    }

    class Client

    UserService ..> Either : "visszaad"
    UserService ..> User : "létrehoz (Right)"

    Client --> UserService : "hívja"
    Client --> Either : "isLeft() / isRight() alapján ágazik"

    note for UserService "id <= 0 → Either.left(&quot;Hibás azonosító&quot;)\nkülönben     → Either.right(new User(...))"

Forráskód

package modern.either;

class Either<L, R> {

    private L left;

    private R right;

    private Either(L left, R right) {
        this.left = left;
        this.right = right;
    }

    public static <L, R> Either<L, R> left(L value) {
        return new Either<>(value, null);
    }

    public static <L, R> Either<L, R> right(R value) {
        return new Either<>(null, value);
    }

    public boolean isLeft() {
        return left != null;
    }

    public boolean isRight() {
        return right != null;
    }

    public L getLeft() {
        return left;
    }

    public R getRight() {
        return right;
    }

}

record User(String name) {

}

class UserService {

    public Either<String, User> findUser(int id) {
        if (id <= 0) {
            return Either.left("Hibás azonosító: " + id);
        }
        return Either.right(new User("Felhasználó_" + id));
    }
}

class Main {

    // modern Java vavr nevű külső könyvtár beépített Either osztályát szokták használni

    static void main() {
        var userService = new UserService();

        var userFetch = userService.findUser(-1);
        if (userFetch.isLeft()) {
            System.out.println(userFetch.getLeft());
        }

        userFetch = userService.findUser(1);
        if (userFetch.isRight()) {
            System.out.println(userFetch.getRight());
        }
    }
}