Dyspozytor (Dispatch Pattern)

Note

W ogólnym ujęciu dyspozytor pozwala przypisywać akcje (funkcje, metody) zmiennym o różych wartościach bądź różnych typów.

Co oznacza “przypisywać akcje zmiennym o różnych wartościach”? Na przykład w grze terenowej możliwych jest 8 kierunków ruchu gracza:

W powyższym przykładzie wpisałem tylko 4 możliwości (północ, południe, wschód, zachód); dodanie pozostały 4 (NE, SW itd) wymagać będzie dodanie kolejnych czterech instrukcji elif. Aby uniknąć problemów w przyszłości, warto już teraz zaimplementować opcję WS (west-south), która będzie robiła to samo, co SW. Liczba linii elif, które trzeba by napisać szybko rośnie …

Dyspozytor to rozwiązanie, w którym akcje (w powyższym przykładzie: dodawanie 1 do x lub y) przechowujemy w słowniku. Kluczami do słownika są wartości zmiennej sterującej (w powyższym przykładzie: napisy “N”, “SW” itp). Rozwiązanie to przedstawiono w poniższym przykładzie.

Wykorzystanie dyspozytora wymaga napisania obiektów - po jednym na każdą akcję. Ma on jednak kilka poważnych zalet:

  • dodawanie nowych akcji nie wymaga modyfikowania bloku if ... elif a jedynie napisania oddzielnej klasy i dodanie jej instacji do słownika; podobnie modyfikacja pojedynczej akcji wymaga zmodyfikowania klasy tej akcji

  • łatwo możemy rozszerzać akcje, rozbudowując kod odpowiednich __call__().

Kierowanie wg typu - przeciążanie w Pythonie

W pogramowaniu obiektowym często pojawia się też konieczność różnego potraktowania obiektów o różnych typach. Najlepiej z punktu widzenia projektowania i programowania było by, gdyby wszystkie różne rodzaje argumentów przetwarzane były tą samą metodą albo co najmniej - metodami o tej samej nazwie. Ta druga możliwość dostępna jest w niektórych językach pogramowania jako przeciążanie (overloading) metod.

1double min(double a, double b) { return (a<b) ? a : b; }
2double min(double *a, int n) { ... }

W powyższym przykładzie wywołanie min(1, 3) wywoła pierwszą z tych metod, a min(1.0, 3.0) - drugą. W Pythonie mechanizm ten niestety nie jest dostępny. Można jednak dynamicznie sprawdzać typ argumentu metody, przeciążenie można więc zastąpić połączeniem wielu warunków. Przydają się też argumenty *args:

 1class Vec2:
 2    def __init__(self, x, y): self.x, self.y = x, y
 3
 4class Circle:
 5    def __init__(self, x, y, r):
 6        self.pos = Vec2(y,y)
 7        self.r = r
 8
 9class Polygon(Circle):
10    def __init__(self):
11        super().__init__(0, 0, 0)
12        self.nodes = []
13
14    def node(self, *args):
15        if isinstance(args[0], Vec2):
16            self.nodes.append(args[0])
17            return self
18        elif len(args) == 2 :
19            self.nodes.append(Vec2(args[0],args[1]))
20            return self
21        else:
22            raise ValueError("Can't create a vertex from an object of type: ")
23
24
25def draw(o):
26    if isinstance(o, Polygon):
27        for i in range(len(o.nodes)):
28            print('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="black" />'
29                  % (o.nodes[i-1].x, o.nodes[i-1].y,o.nodes[i].x, o.nodes[i].y))
30    elif isinstance(o, Circle):
31        print('<circle cx="%d" cy="%d" r="%d" stroke="red" />' % (o.pos.x, o.pos.y, o.r))
32
33
34if __name__ == "__main__":
35    c = Circle(10,12,6)
36    p = Polygon()
37    p.node(6, 0).node(10, 0).node(Vec2(8, 5))
38
39    for o in [c, p] : draw(o)

Przeciążenie wykorzystane zostało w powyższym przykładzie w dwóch miejscach: do rysowania obiektów (format SVG) oraz do dodwania wierzchołków wielokąta. Zauważ, że ponieważ klasa Polygon dziedziczy po klasie Circle, obiekt klasy Polygon jest również obiektem typu Circle Obiekt klast Circle jednak nie jest Polygon’em. Dlatego najpierw sprawdzamy, czy argument o to wielokąt (linia26) a dopiero potem czy jest to koło (linia 30).

Instrukcja warunkowa jest mało elastyczna a na dodatek wraz z rosnącą liczbą możliwości do wyboru kod szybko staje się mało czytelny. Spróbujmy napisać grę w papier, kamień i nożyce:

 1class Paper:
 2    def name(self): return "Paper"
 3
 4class Rock:
 5    def name(self): return "Rock"
 6
 7class Scissors:
 8    def name(self): return "Scissors"
 9
10def play(player1, player2):
11    if isinstance(player1, Rock) and isinstance(player2, Rock):
12        return "draw"
13    elif isinstance(player1, Paper) and isinstance(player2, Paper):
14        return "draw"
15    elif isinstance(player1, Scissors) and isinstance(player2, Scissors):
16        return "draw"
17    elif isinstance(player1, Rock) and isinstance(player2, Paper):
18        return "player 2 won"
19    elif isinstance(player1, Paper) and isinstance(player2, Rock):
20        return "player 1 won"
21    elif isinstance(player1, Scissors) and isinstance(player2, Rock):
22        return "player 2 won"
23    elif isinstance(player1, Rock) and isinstance(player2, Scissors):
24        return "player 1 won"
25    elif isinstance(player1, Scissors) and isinstance(player2, Paper):
26        return "player 1 won"
27    elif isinstance(player1, Paper) and isinstance(player2, Scissors):
28        return "player 2 won"
29    else: return "UNKNOWN PLAYER"
30
31if __name__ == "__main__":
32    p1 = Scissors()
33    p2 = Paper()
34    print(p1.name(),"versus",p2.name()," : ",play(p1,p2))
35
36

Funkcja play() jest dwuargumentowa a każdy z argumentów może być jednego z trzech typów. Mamy zatem już 9 kombinacji. Dodanie czwartego typu argumentów zwiększyłoby liczbę kombinacji do 16. Kod ten może być nieco czytelniejszy, jeżeli zastosujemy dynamic dispatch, jak w programie poniżej:

 1class Paper:
 2    def name(self): return "Paper"
 3
 4class Rock:
 5    def name(self): return "Rock"
 6
 7class Scissors:
 8    def name(self): return "Scissors"
 9
10
11outcomes = { (Paper, Paper) : "draw",
12             (Scissors, Scissors): "draw",
13             (Rock, Rock): "draw",
14             (Rock, Paper): "player 2 won",
15             (Paper, Rock): "player 2 won",
16             (Rock, Scissors): "player 1 won",
17             (Scissors, Rock): "player 2 won",
18             (Scissors, Paper): "player 1 won",
19             (Paper, Scissors): "player 1 won"}
20
21def play(player1, player2):
22    return outcomes[(player1.__class__, player2.__class__)]
23
24if __name__ == "__main__":
25    p1 = Scissors()
26    p2 = Paper()
27    print(p1.name(),"versus",p2.name()," : ",play(p1,p2))
28
29

Oczywiście wciąż musimy wpisać owe 9 możliwych wyników gry. Słownik poprawia jednak czytelność kodu i ułatwia dodawanie nowych wariantów.