Konzeptsuche: Aktionen auf Objekt anwenden

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Hallo,

ich suche ein Programmiermuster oder -konzept, mit dem ich auf einem Objekt bestimmte Aktionen anwenden kann. Um das besser zu illustrieren, habe ich mir ein Beispiel ausgedacht. Gegeben sei ein Quadrat (oder ein anderes geometrisches Element). Auf dieses Quadrat soll man nun bestimmte Aktionen anwenden können, so z.B. an eine andere Position im (Koordinaten)System bewegen (Move), rotieren lassen (Rotate), spiegeln (Flip) oder die Größe ändern (Resize).

Nun kann man ja platt einfach entsprechende Methoden auf die Quadrat-Klasse implementieren, das ist mir aber zu unflexibel, denn für jede neue Aktion muss ich ja auch eine neue Methode implementieren. Deshalb würde ich eigentlich favorisieren, die Aktionen als Klasse zu implementieren und denen einfach ein Protokoll vorzuschreiben, dass die Aktionsklassen implementieren müssen, damit sie als Aktion "durchgehen". Die Quadratklasse bekäme dann lediglich eine apply - Methode, der eine Liste an Aktionen zu übergeben ist.

Wie könnte man das noch implementieren? Für jede Aktion ein neues Quadrat erstellen und zurückgeben lassen? Das Ursprungsquadrat immer ändern? Insgesamt klingt das Konzept ein wenig wie das Decorator-Pattern aus meinem Entwurfsmuster-Buch (für Java).

Pseudocode meines bisherigen Konzepts:

Code: Alles auswählen

q = Quadrat(size=4, position=(2,1))
q.apply([Resize(factor=0.5), Move(4,4), Rotate(angle=45)])
oder eher sowas:

Code: Alles auswählen

q = Quadrat(size=4, position=(2,1))
for action in [Resize(factor=0.5), Move(4,4), Rotate(angle=45)]:
    q = q.apply(action)
Für Tipps oder ein paar Stichworte wäre ich doch recht dankbar :)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo.

Du solltest deine Idee vielleicht noch ein kleines Stück bis zum Ende durchdenken ;-) Was passiert denn, wenn du die `q.apply(action)` aufrufst? Dort stehtst du wieder genau vor dem selben Problem, dass eine spezifische Aktion ausgeführt werden muss. Das kannst du entweder durch eine große if/elif-Kaskade in `Q.apply` machen oder (besser) `Q.apply` ruft eine Methode von `action` auf und `Action` wiederrum die entsprechende Transformationsmethode auf `q`. In beiden Fällen musst du immer neuen Code für neue Transformationen schreiben. Die letzte Lösung halte ich aber für übersichtlicher.

Noch besser ist es natürlich, wenn du weiter generalisieren könntest. `Resize`, `Move` und `Rotate` lassen sich alle locker mit Matrizen ausdrücken. Wenn du dein Geometrieobjekt geschickt anlegst, dann könntest du dir spezielle Implementierungen sparen. Unter Umständen möchtest du später aber auch beliebige andere Transformationen haben, welche du auf diesem Weg nicht mehr abbilden kannst. Das weißt aber nur du ^^ Die Ansätze lassen sich aber auch miteinander kombinieren.

Sebastian
Das Leben ist wie ein Tennisball.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Danke für die Hinweise. Wie das mit Beispielen nun immer so ist, mein konkreter Anwendungsfall ist natürlich kein Quadrat. Ich will eine schönere API für gdal schreiben für meine meist benötigten Anwendungsfälle. Das ist eine Bibliothek zur Bearbeitung und Veränderung von Geodaten (umwandeln in andere Koordinatensysteme, Teilausschnitte, umwandeln in andere Datenformate, usw., nur um ein paar zu nennen). Ich habe bisher einen eher funktionalen Ansatz, der mir aber zu unflexibel wird.
Zentrales Objekt ist hier eine Datasource, auf die ich nun die Aktionen (Koordinatensystem ändern, Datenformat ändern, ...) anwenden möchte. Dazu suche ich halt einen API Ansatz.
`Q.apply` ruft eine Methode von `action` auf und `Action` wiederrum die entsprechende Transformationsmethode auf `q`
Meine Idee war da auch in die Richtung, wohl mit dem Hintergrundwissen, dass ich nur Quadrate habe (Datasource - Objekte) und nicht auch noch Dreiecke und Kreise. Meine Problembeschreibung war da leider nicht ganz eindeutig ...

Code: Alles auswählen

class Action(object):

    def __init__(self, obj):
        self.obj = obj
        self.perform()
        
    def perform(self):
        pass
        
        
class Resize(Action):

    def __init__(self, factor):
        Action.__init__(self)
        self.factor = factor

    def perform(self):
        # pseudo resize
        self.obj.size = self.obj.size * self.factor
        return self.obj

class Quadrat(object):

    def apply(self, action):
        return action(obj=self)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Bei deinem Ansatz handelst du dir im Prinzip mehrere Nachteile ein: Die perform-Methoden der einzelnen Action-Implementierungen werden in einem einzigen Chaos enden, da du für - um bei dem Beispiel der geometrischen Objekte zu bleiben - für jeden Objekttyp den Code in den perform-Methode anpassen musst. Hinzu kommt, dass die Funktionalität einzelner Klassen damit quer durch den Code verstreut wird. Ich würde nicht unbedingt erwarten, dass ein Action-Objekt Arbeit in einem anderen Objekt durchführt.

Da du nur einen Typ hast stellt sich mir die Frage, warum du diesen ganzen Aufwand überhaupt treiben möchtest. Die Action-Klassen sind dann nicht mehr als unnöteger Overhead, wo Methodenaufrufe vollkommen ausreichen würden. Im Prinzip sehe ich nur zwei Gründe hier so etwas zu tun: Du könntest eine Undo-Funktion anbieten (bei einer API wohl eher unwahrscheinlich) oder du möchtest Transformationssequenzen zusammenfassen und auf verschiedene Objekte anwenden. Entweder, wie oben, mehrere Aktionen in einer Liste, oder zum Beispiel als

Code: Alles auswählen

transformation = Move(3, 5) * Rotate(45) * Move(1,4) * Scale(0.5)
Ich würde versuchen den Code bei der entsprechenden Klasse zu belassen, da hier auf den internen Daten gearbeitet wird. In zwei Fällen würde ich Funktionalität auslagern: Die betreffende Klasse wird zu groß und sie kann nicht sinnvoll geteilt werden oder wenn Objekte nicht verändert sondern neu erzeugt werden (funktional).

Sebastian
Das Leben ist wie ein Tennisball.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

EyDu hat geschrieben:Da du nur einen Typ hast stellt sich mir die Frage, warum du diesen ganzen Aufwand überhaupt treiben möchtest.
Ich fand die Möglichkeit, Aktionen flexibel zu kombinieren ganz nett. Die Anwendung wird von anderen Anwendungen benutzt, so dass je nach Anwendungsfall mal nur ein Teilauschnitt benötigt und zurückgeliefert werden soll, mal nur eine Koordinatentransformation, eine Kombi aus beiden, usw.. Mit so einer Aktionsliste könnte man die Arbeit auf der Datasource flexibel zusammenstückeln. Und die Datasource ist nicht verantwortlich für die Durchführung der Aktionen, sondern nur für die Delegation, so dass man die Datasource-Klasse kleiner halten kann. IMO muss eine Datasource nicht wissen, wie man die Transformation, Ausschnittbildung, etc. durchführt.
So ein bisschen schwebte mir da SQLAlchemy als Vorbild im Kopf herum, wo man ja auch so eine Kette von Aktionen auf der Session durchführt:

Code: Alles auswählen

session.query(User).order_by(User.id)[1:3]
nur dass ich die Verkettung (über den .) nicht so schick finde.
EyDu hat geschrieben:Bei deinem Ansatz handelst du dir im Prinzip mehrere Nachteile ein: Die perform-Methoden der einzelnen Action-Implementierungen werden in einem einzigen Chaos enden, da du für - um bei dem Beispiel der geometrischen Objekte zu bleiben - für jeden Objekttyp den Code in den perform-Methode anpassen musst
Kann ich nachvollziehen. Das ist auch etwas, was mir Kopfzerbrechen bereitet.
deets

Vor allem sehe ich es als design-flaw, dass die Action-Objekte eine Referenz auf das Objekt haben muessen. Stattdessen sollten die einzelnen (oder kombinierten) Aktionen bei Aufruf von apply das Ziel uebergeben bekommen - du willst doch potentiell dieselben Aktionen auf verschiedene Objekte anwenden koennen, ohne jedes mal neu instanziieren zu muessen.

Und dann zu deinem OO-Problem: eine Action sollte einfach an das unterliegende Objekt durchdelegieren. Und zB eine translate-Methode aufrufen, die dann durch Square, Circle & co implementiert wird (oder einer ihrer Basisklassen).

Alternativ koenntest du auch eine generische funktion benutzen, wie sie zB PEAK rules bereitstellen. Das separiert dann die Implementierungen.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

@deets: In diesem Fall finde ich es gar nicht so abwegig Funktionalität nach außen zu verlagern. Wie frabron weiter oben schrieb, existiert nur eine einzelne Klasse die verarbeitet werden soll und nicht ganzes Bündel an abgeleiteten Typen. Hinzu kommt, dass die Klasse wohl vorrangig Daten kapselt. Ich würde mir dann zwar eine Klasse erstellen, welche noch einmal von den Daten abstrahiert und dann das von mir oben beschriebene Pattern anwendet, aber als Action getarnte Funktionen scheinen mir durchaus gangbar.
Das Leben ist wie ein Tennisball.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

deets hat geschrieben:Vor allem sehe ich es als design-flaw, dass die Action-Objekte eine Referenz auf das Objekt haben muessen. Stattdessen sollten die einzelnen (oder kombinierten) Aktionen bei Aufruf von apply das Ziel uebergeben bekommen - du willst doch potentiell dieselben Aktionen auf verschiedene Objekte anwenden koennen, ohne jedes mal neu instanziieren zu muessen.
Du meinst sowas?

Code: Alles auswählen

q = Quadrat(size=4)
r = Resize(fac=0.5)
new_q = r.apply(q)
BlackJack

@frabron: Und wenn Du den `Action`\s noch ein `next_action`-Attribut verpasst, welches man zum Beispiel mit dem ``+``-Operator setzen kann, dann könntest Du auch Aktionen kombinieren. Zu guter Letzt dann noch statt einer `apply()`-Methode die `__call__()`-Methode dafür implementieren, und Du kannst so etwas schreiben:

Code: Alles auswählen

quadrat = Quadrat(size=4)
combined_action = Move(x=23, y=-42) + Scale(x=47.11, keep_aspect_ratio=True)
new_quadrat = combined_action(quadrat)
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Mh, an so eine "mathematische" Actions-API hatte ich gar nicht gedacht, ist aber auch sehr schick. Das next_action Attribut für die Move-Action wäre dann Scale, und bei Scale wäre es None, richtig? Für die Addition __add__ implementieren ...
Auf jeden Fall ein interessanter Tipp, danke dafür :)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

frabron hat geschrieben:Mh, an so eine "mathematische" Actions-API hatte ich gar nicht gedacht, ist aber auch sehr schick.
Dann werfe ich dir jetzt einfach mal vor, dass du meinen zweiten Post nicht gelesen hast :D
frabron hat geschrieben:Das next_action Attribut für die Move-Action wäre dann Scale, und bei Scale wäre es None, richtig?
So würde ich es implementieren.
Das Leben ist wie ein Tennisball.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Dann werfe ich dir jetzt einfach mal vor, dass du meinen zweiten Post nicht gelesen hast
:D Doch, doch, nur hatte ich das noch nicht richtig so erfasst. Die Analogie ist mir dann aber beim Lesen von BlackJacks Post direkt aufgefallen. Für manche Dinge braucht's halt schon mal etwas mehr als nur einen einmaligen Hinweis, bzw. ich brauchte den Hinweis mit dem next_action Attribut, damit es "Klick" gemacht hat. Nichts für Ungut, bitte :)
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

EyDu und BlackJack, woher habt ihr denn (beide!) die Idee, Aktionen mittels mathematischer Operatoren zu verknüpfen? Wenn zwei Leute das selbe empfehlen, scheint mir das schon interessant genug für eine Nachfrage :)
deets

Das ist eine ziemlich naheliegende - gerade bei deiner Problem-Domaene. In der Mathematik, aber besonders in der linearen Algebra, die ja fuer 3D-Transformationen notwendig ist, arbeitet man gerne mit Funktionskomposition. Und das ist halt kompakt mit einem Infix-Operator ala + oder * getan.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

BlackJack hat geschrieben:@frabron: Und wenn Du den `Action`\s noch ein `next_action`-Attribut verpasst, welches man zum Beispiel mit dem ``+``-Operator setzen kann, dann könntest Du auch Aktionen kombinieren. Zu guter Letzt dann noch statt einer `apply()`-Methode die `__call__()`-Methode dafür implementieren, und Du kannst so etwas schreiben:

Code: Alles auswählen

quadrat = Quadrat(size=4)
combined_action = Move(x=23, y=-42) + Scale(x=47.11, keep_aspect_ratio=True)
new_quadrat = combined_action(quadrat)
Um noch einmal hierauf zurückzukommen: Bei mehr als zwei "Faktoren" oder "Multiplikatoren" ;) klappt das aber nicht mehr mit einem einzelnen next_action Attribut, da braucht man doch wieder eine Liste, oder? Denn ohne das Quadrat lassen sich die Aktionen ja gar nicht ausführen und bei einer Zuweisung à la:

Code: Alles auswählen

combined_action = Move(x=23, y=-42) + Scale(x=47.11, keep_aspect_ratio=True) + Rotate(deg=45)
bleibt am Ende ja nur Rotate als combined_action "übrig" und von Rotate wird ja dann auch die __call__ aufgerufen - mit nur einen next_action kommt man ja gar nicht mehr auf die erste Aktion. Bzw. man bräuchte nicht nur eine next_action, sondern auch eine previous_action, um sich durch die Actions durchzuhangeln ...


Edit:
So würde es gehen:

Code: Alles auswählen

    def __add__(self, action):
        action.previous_action = self
        return action
Also doch mit einem Attribut :)
Zuletzt geändert von frabron am Dienstag 22. November 2011, 11:16, insgesamt 1-mal geändert.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Du könntest das ``__add__`` einfach so intelligent implementieren, dass es nicht einfach das ``next_action``-Atribut überschreibt, sondern darin so lange hinab steigt, bis ``next_action is None`` erfüllt ist. Oder alternativ:

Code: Alles auswählen

class Action(object):
    def __add__(self, other):
        return CombinedAction(self, other)
    
    def evalulate(self, x):
        raise NotImplementedError

class CombinedAction(Action):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def evaluate(self, x):
        return self.right.evaluate(self.left.evaluate(x))
Das Leben ist wie ein Tennisball.
frabron
User
Beiträge: 306
Registriert: Dienstag 31. März 2009, 14:36

Hallo EyDu,

danke für die Idee, die hatte ich auch, aber dann wieder verworfen. Ich habe an meinen vorherigen Post noch einen Edit angehängt, leider zu spät (also nach deinem Post hier). Allerdings denke ich, wenn man immer den Rechten Teil der Addition/Multiplikation durchreicht und den vorangegangenen speichert, müsste man sich auch rückwärts durchhangeln können ...

Quasi so:

Code: Alles auswählen

class Action(object):

    def __init__(self, path=None):
        self.previous_action = None

    def __call__(self, datasource):
        print "%s.__call__" % self
        print "previous action is: %s" % self.previous_action

        if self.previous_action is not None:
            self.previous_action.__call__(datasource)

        print "Now performing %s action" % self

    def __add__(self, action):
        print "Adding %s to %s" % (self, action)
        action.previous_action = self
        print "Returning %s" % action
        print
        return action

    # EyDus acknowledgement of first mentioning the idea
    __mul__ = __add__
Die Queue

Code: Alles auswählen

    source_ds = DataSource(Driver('ESRI Shapefile'), input_file)
    queue = (
            Format(Driver('GeoJSON'), output_file)
             + SpatialFilter(BBox(97634.51, 85515.65, 98085.69, 85827.89))
             + Transform(SpatialReference(2169), SpatialReference(900913))
        )
    target_ds = queue(source_ds)

Output:

Code: Alles auswählen

Adding <clyde.actions.Format object at 0x146e750> to <clyde.actions.SpatialFilter object at 0x146e890>
Returning <clyde.actions.SpatialFilter object at 0x146e890>

Adding <clyde.actions.SpatialFilter object at 0x146e890> to <clyde.actions.Transform object at 0x146ead0>
Returning <clyde.actions.Transform object at 0x146ead0>

<clyde.actions.Transform object at 0x146ead0>.__call__
previous action is: <clyde.actions.SpatialFilter object at 0x146e890>
<clyde.actions.SpatialFilter object at 0x146e890>.__call__
previous action is: <clyde.actions.Format object at 0x146e750>
<clyde.actions.Format object at 0x146e750>.__call__
previous action is: None
Now performing <clyde.actions.Format object at 0x146e750> action
Now performing <clyde.actions.SpatialFilter object at 0x146e890> action
Now performing <clyde.actions.Transform object at 0x146ead0> action
Sieht IMO erst mal gut aus ...
Antworten