Łańcuch zobowiązań

Wzorce projektowe

  |   8 min czytania

Zastosowanie

Łańcuch zobowiązań (ang. Chain of responsibility) (wzorzec behawioralny) zwany także Łańcuchem odpowiedzialności umożliwia przetwarzanie żądania przez łańcuch różnych obiektów. Łańcuch oparty jest o strukturę listy jednokierunkowej, tzn. obiekty mają ustaloną kolejność i znają swojego następnika. Klient zleca wykonanie operacji pierwszemu elementowi łańcucha, a gdy ten nie jest w stanie lub nie jest uprawniony do zakończenia żądania to przekazuje odpowiedzialność jego realizacji swojemu następnikowi. Jeśli następnik nie może podjąć działania wówczas również przekazuje je kolejnemu elementowi. Przetwarzanie i przekazywanie zadania działa rekurencyjnie dla każdego obiektu w łańcuchu. Elementy łańcucha analizują zadania i zapewniają jego obsługę lub przekazują zadanie dalej. Dzięki temu struktura łańcucha jest otwarta na modyfikacje, może być dynamicznie zmieniana, a także zmniejsza się liczba zależności klientem, a potencjalnymi odbiorcami.

Ograniczenia

Klient nie zna ostatecznego wykonawcy. Co więcej Łańcuch zobowiązań nie gwarantuje obsługi zleconego zadania. Ponadto w zależności od implementacji, żądanie może być częściowo obsługiwane przez różne elementy. Debugowanie staje się zatem utrudnione, a wydajność maleje.

Użycie

Wzorzec używany jest w mechanizmach przetwarzania podobnych żądań, dla których proces klasyfikacji powinien przebiegać w ustalonym porządku. Klient nie musi znać dokładnego wykonawcy oraz szczegółów implementacji dzięki czemu wykorzystywany jest w sytuacjach dynamicznie zmieniających się wymagań.

Implementacja

Abstrakcyjna klasa Handler definiuje szablon metody przepływu obsługi żądania, natomiast klasy ją rozszerzające Handler1, Handler2 itd dostarczają szczegółów implementacji zadania.

Łańcuch zobowiązań diagram

Poniższy listing przedstawia implementację Łańcucha zobowiązań dla żądania Request i wykonawców typu Handler.

public abstract class Handler {

    protected Handler successor;
    //some fields

    protected abstract void action(Request request);
    protected abstract boolean canExecute(Request request);

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public void handle(Request request) {
        if(canExecute(request))
            action(request);
        else if(successor != null)
            successor.handle(request);
    }

    //some other methods
}

public class Handler1 extends Handler {

    @Override
    protected void action(Request request) {
        //do specific action for Handler1
    }

    @Override
    protected boolean canExecute(Request request) {
        if(request.getType() == Type.TYPE1)
            return true;
        else
            return false;
    }
}

public class Handler2 extends Handler {

    @Override
    protected void action(Request request) {
        //do specific action for Handler2
    }

    @Override
    protected boolean canExecute(Request request) {
        if(request.getType() == Type.TYPE2)
            return true;
        else
            return false;
    }
}

public class Handler3 extends Handler {

    @Override
    protected void action(Request request) {
        //do specific action for Handler3
    }

    @Override
    protected boolean canExecute(Request request) {
        if(request.getType() == Type.TYPE3)
            return true;
        else
            return false;
    }
}

Klient tworzy Łańcuch zobowiązań i zleca obsługę żądania.

//create chain of responsibility depends
Handler main = new Handler1();
Handler handler2 = new Handler2();
Handler handler3 = new Handler3();
main.setSuccessor(handler2);
handler2.setSuccessor(handler3);

//carry out request to chain
Request request1 = new Request("Content", Type.TYPE2);
main.handle(request1); //handler2 will act

Request request2 = new Request("Content", Type.TYPE4);
main.handle(request2); //no handler acts

Przykład

Kantor wymiany walut wykorzystuje aplikację Exchanger do optymalizacji wydawanych nominałów w przeprowadzanych transakcjach. Kasjer otrzymuje zapytanie od klienta, a następnie definiuje zasady wydawanych nominałów. Na podstawie zleconej transakcji i zasad przeprowadzana jest symulacja, która w rezultacie podaje informacje o nominałach, które należy wypłacić klientowi. Do realizacji tego zadania wykorzystany został wzorzec Łańcuch zobowiązań do prezentuje poniższy listing.

public abstract class Nominal {

    protected Nominal successor;
    //some fields

    protected abstract void withdraw(Exchange transaction);
    protected abstract boolean canWithdraw(Exchange transaction);

    public void setSuccessor(Nominal successor) {
        this.successor = successor;
    }

    public Exchange calculate(Exchange transaction) {
        if(canWithdraw(transaction))
            withdraw(transaction);
        if(transaction.getLeftAmount() > 0 && successor != null)
            return successor.calculate(transaction);
        else
            return transaction;
    }

    //some other methods
}

public class ConcreteNominal extends Nominal {

    private final int NOMINAL;

    public Nominal(int nominal) {
        this.NOMINAL = nominal;
    }

    @Override
    protected void withdraw(Exchange transaction) {
        if(transaction.getLeftAmount() >= NOMINAL) {
            int count = transaction.getLeftAmount() / NOMINAL;
            int rest = transaction.getLeftAmount() % NOMINAL;
            transaction.setLeftAmount(rest);
            transaction.addOperation(count + " x Nominal " + NOMINAL);
        }
    }

    @Override
    protected boolean canWithdraw(Exchange transaction) {
        if(transaction.getLeftAmount() >= NOMINAL && isDepositEnough(transaction))
            return true;
        else
            return false;
    }

    private boolean isDepositEnough(Exchange transaction) {
        //check is concrete nominal enough in deposit 
        return true; //mocked
    }
}

Kasjer na podstawie zaleceń oraz stanu depozytu definiuje priorytety wydawanych nominałów. Wprowadza zlecenie klienta do systemu i dokonuje symulacji.

//create default nominals scheme
Nominal nominal100 = new ConcreteNominal(100);
Nominal nominal50 = new ConcreteNominal(50);
Nominal nominal20 = new ConcreteNominal(20);
Nominal nominal10 = new ConcreteNominal(10)
Nominal nominal5 = new ConcreteNominal(5);
Nominal nominal2 = new ConcreteNominal(2);
Nominal nominal1 = new ConcreteNominal(1);

//cashier gets client's transaction
Exchange transaction1 = new Exchange(1500, Currency.PLN, Currency.USD);

//cashier defines default nominals scheme
nominal100.setSuccessor(nominal50);
nominal50.setSuccessor(nominal20);
nominal20.setSuccessor(nominal10);
nominal10.setSuccessor(nominal5);
nominal5.setSuccessor(nominal2);
nominal2.setSuccessor(nominal1);

//simulate transaction
Exchange result1 = nominal100.calculate(transaction1);
if(result.getLeftAmount() == 0) {
    //success - can withdraw
}
else {
    //fail - can not withdraw this transaction based on current nominals scheme
}
//show nominals to withdraw
result.getOperations();
//for course 3.67 the result is:
//4x100, 1x5, 1x2, 1x1

//cashier gets new transaction
Exchange transaction2 = new Exchange(5000, Currency.USD, Currency.GBP);
//nominals GBP are the same like USD so no need to update the chain

//in deposit there is a lot of nominal 1
//so cashier wants to spent nominal 1 before nominal 5 and 2
nominal10.setSuccessor(nominal1);
nominal1.setSuccessor(nominal2);
nominal2.setSuccessor(nominal5);

//simulate transaction like above
nominal100.calculate(transaction2);
//for course 0.761 and 100x20, 50x30, 10x20 nominals available in deposit to withdraw the result is:
//20x100, 30x50, 10x20, 10x10, 5x1 

Biblioteki

Metoda log klasy Logger standardowej biblioteki Java wykorzystuje wzorzec Łańcuch zobowiązań.