Maszyna stanowa – zagada projektowa

Ciekawa, moim zdaniem, łamigłówka i zagadka projektowa. Niby każdy wie, ale nigdzie nie znalazłem ładnego wytłumaczenia.

Mamy sobie prostą maszynę stanową w postaci enuma wzbogaconego o metodę sprawdzającą dopuszczalność przejścia:

listing 1. enum State, czyli maszyna stanowa

public enum State {

	A, B, C, D;

	private List<State> validChanage;

	static {
		A.validChanage = Arrays.asList(B);
		B.validChanage = Arrays.asList(C);
		C.validChanage = Arrays.asList(A, D);
		D.validChanage = Arrays.asList(D);
	}

	public boolean couldChange(State newState) {
		return validChanage.contains(newState);
	}
}

Takie toto prymitywne, że aż żal patrzeć… do tego mamy sobie obiekt, który wykorzystuje tego enuma do zarządzania własnym stanem:

listing 2. klasa StateObject

public class StateObject {

	private State currentState;

	public State getCurrentState() {
		return currentState;
	}

	public void setCurrentState(State currentState) {
		if (this.currentState != null 
			&& !this.currentState.couldChange(currentState)) {
			throw new IllegalStateException(
				String.format("Can not change from %s to %s", 
					this.currentState, currentState));
		}
		this.currentState = currentState;
	}
}

Teraz zagadka:

  • Czy dodanie logiki do settera jest ok? Przy czym nie jest istotne co robi logika.
  • Kiedy takie rozwiązanie jest poprawne, a kiedy nie?
  • Jeżeli nie to dlaczego?

Wersja na Stackoverflow

4 myśli na temat “Maszyna stanowa – zagada projektowa

  1. z punktu widzenia pojo, setter nie powinien zawierać logiki

    jeśli zaś klasa z założenia nie jest pojo, to jest to po prostu jakaś metoda nazywająca się jak setter

    więc zależy od celu w jakim stworzona została klasa i tego, czy ktoś nie próbuje dostać się do pola przez refleksję (ręcznie czy wykorzystując do tego framework)

  2. jeszcze rozwijając myśl – można podzielić klasę na dwie klasy: StatePojo (prosty setter) i StateManager (metoda zawierająca właściwą logikę, niech nawet nazywa się wtedy „set”)

  3. Jeżeli zadaniem tego settera jest sterowanie logiką to w tym kontekście definicja nie jest właściwa. Jako parametr masz obiekt klasy State (a to jest wewnętrzna implementacja klasy StateObject). Więc obiekty które będą nim sterować także muszą znać State. Lepszym podejściem byłyby konkretne metody w klasie StateObject w stylu:

    public void markComplete() {
    //sprawdz czy operacja jest dozwolona
    //zmien wewnętrzny stan (State obiekt)
    }

    Ogólnie jeżeli mamy do czynienia z POJO to setter nie powinien mieć logiki. W innym przypadku jak zwykle „to zależy”. W większości przypadków od ustaleń w zespole. Ważne, żeby trzymać się konwencji i nie zaskakiwać innych.

  4. IMHO
    Jak zwykle zależy :). Bardziej skłonny jestem ku pozostawieniu settera bez logiki ku prostemu ustawianiu wartości. Parę moich spostrzeżeń:
    1.Settery są wykorzystywane przez masę różnych narzędzi – od ORM, marshalery do XMLa i JSONa. W przykładzie takie narzędzia nie miałyby problemu z ustawieniem dowolnego stanu bo dozwolone jest przejście w dowolny stan z nulla. Jednakże można by się pokusić, że tak na prawdę przejście z nulla też może być tylko w jeden stan początkowy aby nikt nie mógł nam namieszać w logice a maszyna była w pełni zabezpieczona.
    2. Maszyna stanów to maszyna stanów – bezduszna i arbitralna a rzeczywistość to rzeczywistość logiki się nie musi zawsze imać :P. Zwykle superuser musi mieć możliwość poprawek niektórych błędów wykonanych przez użyszkodników. Oznacza to także przejście w nieprawidłowy stan z punktu widzenia maszyny.
    3. Tworząc testy jednostkowe często nie przechodzimy przez całość logiki biznesowej tylko skupiamy się na fragmentach, które nas interesują. Blokując na seterze dowolną manipulację stanu kładziemy sobie kłody pod nogi bo wymuszamy zajmowanie się w teście aspektami, które z jego punktu widzenia są nieistotne.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax