Interaktives Tutorial mit Async auf Basis des MVC

Gute Links und Tutorials könnt ihr hier posten.
Antworten
sohqu
User
Beiträge: 5
Registriert: Donnerstag 28. April 2016, 12:54

Hallo liebes Forum,

nachdem ich einigermaßen das Konzept Model-View-Controller (nach Trygve Reenskaug von 1979) verdaut habe, würde ich gerne zum besseren Verständnis ein kleines Programm schreiben.

Dieses soll mit Python 2.7.10 möglichst ohne Zusatzmodule auskommen. Also Module wie Asyncio in Python 3 stehen mir zu Lernzwecken ausdrücklich nicht zur Verfügung.

Ziel des Programms ist eine von View und Controller entkoppelte Abfrage der aktuellen Uhrzeit des Systems, welche natürlich im Model erfasst und bereitgehalten wird. Nach einem Statuswechsel des Model fragt das View die Daten automatisiert ab.
Die Ausgabe des View geht der Übersichtlichkeit halber zur Kommandozeile. Der Controller soll zunächst einmal nur das Programm beenden (Steuerung des View). Später sind auch Steuerungsmöglichkeiten des Model angedacht (z.B. Speicherung zweier Zeitpunkte und Darstellungsänderungen des View, dass genau diese Zeitpunkte dargestellt werden sollen).

Das Lösungskonzept dieser Aufgabe ist denkbar einfach:
Führe im Hintergrund eine Endlosschleife durch, die die Uhrzeit alle 0,1 Sekunden abfragt. Sobald sich die Uhrzeit ändert, soll dem View signalisiert werden, dass sich die Uhrzeit geändert hat und das View aktualisieren kann. Daraufhin macht das View das einzig sinnvolle in dieser Konstellation: Es fragt vom Model die aktuelle Uhrzeit ab und stellt diese dar (Pull-Pattern FTW!). Ein Push in dieser Situation würde nämlich grundlegendes Wissen über das View im Model voraussetzen. Nämlich welche Daten für das View relevant sind. Damit wäre aber die lose Kopplung zwischen View und Model im Eimer. Das Pull-Pattern mit dem notify() Richtung View ist bereits eine Grauzone: Das View übergibt sich selbst als Referenz an das Model und das Model weiß zwar etwas über das View aber halt nur, dass es das View hin und wieder mal anrufen muss. Die Aufbereitung geschieht also weiterhin komplett im View während der Controller die Steuerung des View und zukünftig auch des Model übernimmt (deshalb schonmal die Bindung zwischen Controller und Model in controller.py, obwohl diese im nachfolgenden Code noch nicht benötigt wird).

Momentan stehe ich ein bisschen wie der Ochs vorm Berg: Das grundlegende MVC ist fertig, aktualisiert aber nur dann, wenn ich eine Taste betätige. Das Observer-Pattern habe ich mal fachgerecht zurückgebaut, da dieses die Übersichtlichkeit und das Verständnis erschwert (nur noch "self._sleep = 0.1" im model.py erinnert an die gescheiterten Versuche). Mit dem Oserver-Pattern ohne Async lief ich nämlich in eines von zwei Problemen:
1. Entweder das Interface musste auf den Statuswechsel des Models warten (Model in Endlosschleife). Dabei war dann keine Interaktion mehr möglich.
2. Oder das Model gab nur dann Informationen weiter, wenn ich eine Taste drücke. Also genau dieselbe Funktionalität wie im nachfolgenden Code.

Über Anregungen und Hinweise, wie das zu einem Async-Programm umgebaut werden könnte, würde ich mich sehr freuen. Ansonsten viel Spaß beim Experimentieren mit dem nachfolgend dargestellten MVC-Pattern.


Viele Grüße
sohqu

[Codebox=python file=model.py]
# -*- coding:utf-8 -*-

import time


class Model():

def __init__(self):
self._sleep = 0.1

# API fuer View
def get_time(self):
return(time.ctime())
[/Codebox]

[Codebox=python file=view.py]
# -*- coding:utf-8 -*-

import curses


class View():

def __init__(self, model_object):
self.mref = model_object
self.actual_time = None

# API fuer Controller
def update_view(self):
self.actual_time = self.mref.get_time()

def draw_view(self, scr):
scr.clear()
scr.border()
scr.addstr(1, 1, "UHR", curses.A_UNDERLINE)
scr.addstr(2, 1, "Aktuelle Uhrzeit: " + str(self.actual_time))
scr.addstr(4, 1, "Taste 'q' zum Beenden.")
[/Codebox]

[Codebox=python file=controller.py]
# -*- coding:utf-8 -*-


class Controller():

def __init__(self, model_object, view_object):
self.mref = model_object
self.vref = view_object

def run_interface(self, scr):
self.vref.update_view()
self.vref.draw_view(scr)
while True:
event = scr.getch()
if event == ord('q'):
break
else:
self.vref.update_view()
self.vref.draw_view(scr)
[/Codebox]

[Codebox=python file=main.py]
# -*- coding:utf-8 -*-


from model import Model
from view import View
from controller import Controller

import curses
import sys


def start():
m = Model()
v = View(m)
c = Controller(m, v)
curses.wrapper(c.run_interface)

if __name__ == "__main__":
sys.exit(start())
[/Codebox]
BlackJack

@sohqu: Man kann es sich auch kompliziert machen. :-)

Als erstes mal der Hinweis das MVC nicht gleich MVC ist. Da versteht anscheinend jedes Rahmenwerk etwas leicht anderes drunter. Sollte man im Hinterkopf behalten, falls man mal mit Rahmenwerken arbeiten muss, bedeutet das oft das man MVC neu lernen muss. Oder falls jemand sagt „das ist kein MVC“, sollte man nachfragen welches MVC er denn (kennen)gelernt hat. :-)

Ich würde das ja in *ein* Modul schreiben. Wir sind hier ja nicht bei Java wo man eine Klasse pro Datei definiert.

Die Namen sind teilweise schlecht. `mref`, `vref`? Eine einbuchstabige Abkürzung und ein nichtssagender Suffix. Letztlich ist ja alles was man in Python an einen Namen bindet oder in einer Datenstruktur speichert eine ”Referenz” auf das jeweilige Objekt. Womit der Namenszusatz `ref` keinerlei Mehrwert bietet.

`Controller` ist keine wirkliche Klasse. Man erstellt das Exemplar und ruft die eine Methode auf. Das ist eine einfache Funktion unnötig kompliziert als Klasse formuliert. `Model` ist als Klasse noch sinnfreier weil das nur aus einer ”Methode” besteht, die eine Funktion aufruft, ohne irgendwas mit dem Umstand anzufangen das es auf einem Objekt aufgerufen wird.

Die `update_view()` erscheint mir überflüssig. Warum muss der View den Zustand des Models selbst noch mal als Zustand zwischenspeichern, wo er doch Zugriff auf das Model hat und ihn von dort abfragen kann‽

Das englische Wort „actual“ bedeutet *nicht* „aktuell“ sondern „tatsächlich“/„wirklich“. Wenn man das überhaupt an der Stelle speichern möchte, würde es eher `current_time` heissen.

Das die Taste q zum Abbruch dient ist im Code an zwei Stellen hart kodiert.
sohqu
User
Beiträge: 5
Registriert: Donnerstag 28. April 2016, 12:54

Hallo BlackJack,

danke für die Antwort.
BlackJack hat geschrieben:@sohqu: Man kann es sich auch kompliziert machen. :-)

Als erstes mal der Hinweis das MVC nicht gleich MVC ist. Da versteht anscheinend jedes Rahmenwerk etwas leicht anderes drunter. Sollte man im Hinterkopf behalten, falls man mal mit Rahmenwerken arbeiten muss, bedeutet das oft das man MVC neu lernen muss. Oder falls jemand sagt „das ist kein MVC“, sollte man nachfragen welches MVC er denn (kennen)gelernt hat. :-)
Also noch schlimmer, als ich es schon mit MVC und MVC/Model2 erlebte? Danke für die Warnung!
BlackJack hat geschrieben: Ich würde das ja in *ein* Modul schreiben. Wir sind hier ja nicht bei Java wo man eine Klasse pro Datei definiert.
Das ist mir klar. Allerdings geht es mir damit vor allem darum, die Verteilung von Aufgaben und das einfache Erweitern der Module zu begreifen. Momentan ist das designtechnischer Bloat. Da stimme ich voll und ganz zu. Aber das ändert sich, sobald das Async und der Observer hinzukommen. Um das zu begreifen, brauche ich es halt sehr detailliert. Und zwar so kleinteilig, dass ich es quasi "from scratch" selbst bauen könnte.
BlackJack hat geschrieben: Die Namen sind teilweise schlecht. `mref`, `vref`? Eine einbuchstabige Abkürzung und ein nichtssagender Suffix. Letztlich ist ja alles was man in Python an einen Namen bindet oder in einer Datenstruktur speichert eine ”Referenz” auf das jeweilige Objekt. Womit der Namenszusatz `ref` keinerlei Mehrwert bietet.
Guter Einwand. Es aber z.B. "model" zu nennen oder "m" oder "model_reference" macht es leider nicht besser. Innerhalb der Klasse muss die Referenz aber vorgehalten werden, damit es als Pattern nach Reenskaug funktionieren kann. Andernfalls ist die Zusammenarbeit der Klassen ohne feste Kopplung ein Problem. Dann muss ich entweder die lose Kopplung opfern oder habe Klassen, die über Sockets miteinander kommunizieren müssten. Das verbessert das Verständnis nicht gerade.
BlackJack hat geschrieben: `Controller` ist keine wirkliche Klasse. Man erstellt das Exemplar und ruft die eine Methode auf. Das ist eine einfache Funktion unnötig kompliziert als Klasse formuliert. `Model` ist als Klasse noch sinnfreier weil das nur aus einer ”Methode” besteht, die eine Funktion aufruft, ohne irgendwas mit dem Umstand anzufangen das es auf einem Objekt aufgerufen wird.
Das ändert sich alles noch, sobald die eigentliche Funktionalität eingebaut werden soll. Zunächst einmal muss ich das Prinzip begriffen haben, bevor ich die Klassen um die gewünschte Funktionalität erweitere.
BlackJack hat geschrieben: Die `update_view()` erscheint mir überflüssig. Warum muss der View den Zustand des Models selbst noch mal als Zustand zwischenspeichern, wo er doch Zugriff auf das Model hat und ihn von dort abfragen kann‽
Weil der View später nicht mehr das gesamte Model spiegelt. Sobald ich das Zusammenspiel begriffen habe, kann ich die eigentlichen Projekte bzw. meine Ideen voranbringen. Dann wird das View immer nur die gerade benötigten Informationen aus dem Model abfragen und darstellen.
Aufruf ist dann: Controller -> Model -notify-> View
BlackJack hat geschrieben: Das englische Wort „actual“ bedeutet *nicht* „aktuell“ sondern „tatsächlich“/„wirklich“. Wenn man das überhaupt an der Stelle speichern möchte, würde es eher `current_time` heissen.
Danke für den Hinweis. Habe ich in der Eile, um das Beispiel zusammenzubauen, verbockt.
BlackJack hat geschrieben: Das die Taste q zum Abbruch dient ist im Code an zwei Stellen hart kodiert.
Was ist das Problem damit? Einerseits muss auf der Benutzeroberfläche gezeigt werden, dass 'q' das Programm beendet und andererseits muss die Logik dann auch diese Taste als "Beenden" umsetzen.
Also einmal in der Logik und einmal als Anzeige. Eine dynamische Abfrage (Taste in Konfigurationsdatei ablegen, Auslesen der Konfiguration von View und Controller) führt aber für mich vom eigentlichen Lernzweck dieser Übung weg bzw. macht es dann wirklich zu kompliziert.

Sorry, ich habe gerade bemerkt, dass ich den Titel falsch wählte. Es geht hauptsächlich darum, hier ein Tutorial zu entwickeln, bei dem ich durch Anmerkungen der Leserschaft selbst etwas lerne und Neuankömmlinge vielleicht eine Art Blaupause der grundsätzlichen Interna bei verschiedenen Frameworks bzw. Umsetzung bestimmter Patterns haben. Also eigentlich war das als "Win-Win" sowohl für mich als auch für das Forum gedacht.


Viele Grüße
sohqu
BlackJack

@sohqu: Die Antwort auf den Einwand der teilweisen schlechten Namensgebung verstehe ich nicht. `mref` ist schlecht, genau wie `m`, und `model_reference` hat wieder das Problem dass das `reference` am Ende keinen Mehrwert liefert. Sonst müsstest Du `_reference` an *alle* Namen anhängen, oder erklären können warum es bei einigen Namen da steht, und bei vielen anderen nicht. `model` wäre der passende generische Namen bei dem man nicht gross herumraten muss was der Name denn bedeuten mag.

Auch den Einwand gegen *ein* Modul verstehe ich nicht. Die Klassen und damit deren Aufgaben sind ja weiterhin getrennt. Und da diese drei Klassen zusammengehören, macht es sogar Sinn die in ein Modul zu stecken.

Das Muster setzt sich fort: Auch die Begründung für `update_view()` kann ich nicht nachvollziehen. Auch wenn ein View nur einen Teilaspekt eines Models darstellt, braucht man diesen Teilzustand nicht im View zwischenspeichern wenn man ihn doch vom Model abfragen kann. Nehmen wir beispielsweise mal an der View stellt nur das Datum dar, dann kann der doch trotzdem nur diesen Teil vom Model abfragen. Das Problem erscheint mir im vorliegenden Fall eher das Model zu sein das es nicht erlaubt den Zustand so abzufragen das der sich zwischen zwei Aufrufen nicht geändert hat. *Dort* müsste man den Zustand zwischenspeichern, so dass der Konstant bleibt wenn das Model die Listener informiert, dass sich etwas geändert hat.

Das Problem mit dem 'q' ist das man das an zwei Stellen ändern muss wenn man beispielsweise Escape zum Abbrechen verwenden möchte. Konfigurationsdatei muss ja nicht gleich sein, aber eine Konstante könnte man schon definieren. Das wäre supereinfach wenn das alles in einem Modul stehen würde. Ansonsten könnte man das auch den Controller an den View übergeben lassen.

Vielleicht ist das Beispiel mit der Uhrzeit, wo Du irgendwo von aussen eine Benachrichtigung bekommen müsstest, dass sich die Zeit geändert hat, nicht besonders glücklich gewählt. Wenn das Model von sich aus über Änderungen benachrichtigen können soll ohne zu blockieren, sind wir bei irgendeiner Art von Nebenläufigkeit. In Python 2.7 nur mit der Standardbibliothek also `threading`, oder man bastelt sich selbst einfache Coroutinen mittels Generatorfunktionen.

Oder Du gehst von `curses` weg, das es unter Windows sowieso nicht in der Standardbibliothek gibt, hin zu Tk. Dort würde ich dann aber den View einfach per `after()` regelmässig die Zeit abfragen lassen. Ich habe persönlich aber sowieso ein Problem mit solchen Beispielen, wo einfache Sachen *viel* zu kompliziert gelöst werden. In der Praxis verwendet man (also zumindest die Leute die ich kenne :-) MVC auch nur wenn das etwas bringt. Sonst reicht eine Trennung zwischen Logik und GUI völlig aus. Ein Beispiel für MVC sollte zeigen was der Vorteil davon ist. Wenn man das Beispiel problemlos auf eine UI-Klasse, die regelmässig die Zeitfunktion aufruft, zusammendampfen kann, dann war das Beispiel IMHO schlecht.
sohqu
User
Beiträge: 5
Registriert: Donnerstag 28. April 2016, 12:54

Hi BlackJack,
BlackJack hat geschrieben:@sohqu: Die Antwort auf den Einwand der teilweisen schlechten Namensgebung verstehe ich nicht. `mref` ist schlecht, genau wie `m`, und `model_reference` hat wieder das Problem dass das `reference` am Ende keinen Mehrwert liefert. Sonst müsstest Du `_reference` an *alle* Namen anhängen, oder erklären können warum es bei einigen Namen da steht, und bei vielen anderen nicht. `model` wäre der passende generische Namen bei dem man nicht gross herumraten muss was der Name denn bedeuten mag.
Jetzt ist mir klar, woran die Lesbarkeit krankt. Danke. Das hat ein bisschen länger gedauert, weil ich Kollisionen im Namensraum befürchtete.
BlackJack hat geschrieben: Auch den Einwand gegen *ein* Modul verstehe ich nicht. Die Klassen und damit deren Aufgaben sind ja weiterhin getrennt. Und da diese drei Klassen zusammengehören, macht es sogar Sinn die in ein Modul zu stecken.
Beim Erweitern muss ich die Klassen dann aber wieder auf mehrere Dateien verteilen. Den Mehraufwand wollte ich einfach einsparen.
BlackJack hat geschrieben: Das Muster setzt sich fort: Auch die Begründung für `update_view()` kann ich nicht nachvollziehen. Auch wenn ein View nur einen Teilaspekt eines Models darstellt, braucht man diesen Teilzustand nicht im View zwischenspeichern wenn man ihn doch vom Model abfragen kann. Nehmen wir beispielsweise mal an der View stellt nur das Datum dar, dann kann der doch trotzdem nur diesen Teil vom Model abfragen. Das Problem erscheint mir im vorliegenden Fall eher das Model zu sein das es nicht erlaubt den Zustand so abzufragen das der sich zwischen zwei Aufrufen nicht geändert hat. *Dort* müsste man den Zustand zwischenspeichern, so dass der Konstant bleibt wenn das Model die Listener informiert, dass sich etwas geändert hat.
Guter Punkt. Das Prinzip hatte ich bisher so verstanden, dass das Model nur die Aufgabe erledigen soll. Das View hingegen soll Informationen vom Model einsammeln und für die Darstellung aufbereiten bzw. vorhalten. Also nach meinem Verständnis sollte hierbei eine ganz klare Aufgabenteilung zwischen "Berechnung" und "Informationsaufbereitung/Darstellung" stattfinden.

In dem Beispiel ist das update_view() eigentlich nur ein funktionierender Platzhalter für deutlich komplexere Datenfluten.
BlackJack hat geschrieben: Das Problem mit dem 'q' ist das man das an zwei Stellen ändern muss wenn man beispielsweise Escape zum Abbrechen verwenden möchte. Konfigurationsdatei muss ja nicht gleich sein, aber eine Konstante könnte man schon definieren. Das wäre supereinfach wenn das alles in einem Modul stehen würde. Ansonsten könnte man das auch den Controller an den View übergeben lassen.
Krass. Auf den Trichter bin ich noch gar nicht gekommen. Der Wald und die Bäume...

Danke!
BlackJack hat geschrieben: Vielleicht ist das Beispiel mit der Uhrzeit, wo Du irgendwo von aussen eine Benachrichtigung bekommen müsstest, dass sich die Zeit geändert hat, nicht besonders glücklich gewählt. Wenn das Model von sich aus über Änderungen benachrichtigen können soll ohne zu blockieren, sind wir bei irgendeiner Art von Nebenläufigkeit. In Python 2.7 nur mit der Standardbibliothek also `threading`, oder man bastelt sich selbst einfache Coroutinen mittels Generatorfunktionen.
Und das ist genau der Grund für diesen Thread. Das Beispiel ist halt nicht ganz passend, aber es hat die gleiche Problematik, wie das Projekt, für das ich mir gerade das Verständnis zu erarbeiten versuche. Die kurze Frage lautet: Wie bekomme ich Nebenläufigkeit in eine MVC-Umgebung eingebaut?

Die Situation ist genauso wie bei der Uhrzeit: Ich habe eine externe Ressource, die sich praktisch selbst verwaltet und deren Zustand sich unabhängig von View und Controller ändert. Dann habe ich eine View und einen Controller. View soll mir den Status einzelner Komponenten wiedergeben. Controller soll das View und das Model steuern.

Ob es sich bei der externen Ressource nun um die Systemzeit, einen IMAP-Server, ein expect-Skript für NetHack (Linux-Rollenspiel) oder ein simuliertes Gewächshaus für ein Computerspiel handelt, ist für das Begreifen des Prinzips ziemlich egal. An dieser Stelle fehlt mir klar das Verständnis für die Nebenläufigkeit beim MVC-Pattern. Das möchte ich nun ändern.

Was verwende ich da am Besten? Python 2.7 hat kaum Möglichkeiten, Threadsafe zu programmieren (race-conditions sind ein echtes Problem, sobald die Aufgabe etwas komplexer wird). D.h. ich müsste erstmal groß und breit mit Locks warm werden, bevor ich mich auf das eigentliche Erlernen der Nebenläufigkeit konzentrieren könnte. Also sind asynchrone Aufrufe (z.B. via select auf sockets) zunächst einmal der Standardweg, den ich verstanden haben sollte. Und da hänge ich komplett fest. Deshalb bin ich hier.

Je komplexer ich aber das Lernobjekt wähle, desto länger dauert es, bis ich die wesentlichen Punkte verstanden habe. Deshalb meine Entscheidung für den kleinsten gemeinsamen Nenner, für den ich den designtechnischen Overkill fahre: die Systemzeit im MVC darstellen und später auch ändern können.
BlackJack hat geschrieben: Oder Du gehst von `curses` weg, das es unter Windows sowieso nicht in der Standardbibliothek gibt, hin zu Tk. Dort würde ich dann aber den View einfach per `after()` regelmässig die Zeit abfragen lassen.
Naja, das ändert aber an meinem Lernziel wenig.

MVC bekomme ich hin. MVC mit Observer zwischen Model und View bekomme ich ebenfalls gut hin. Nebenläufigkeit im MVC? WTH! Wie soll das klappen? Wo trenne ich die Strukturen? Welcher Teil der Nebenläufigkeit sollte besser wo eingesetzt werden? Wo starte ich welchen Teil? Kann ich eine Endlosschleife in einem select()-Aufruf so starten, dass dieser unterbrochen wird, sobald eine andere Ressource frei wird?

Ich schreibe im nächsten Post mal das Programm mit eingebauten Observer als eine Datei rein. Sofern ich rausbekomme, wie der Code auch wirklich als Code dargestellt wird.
BlackJack hat geschrieben:Ich habe persönlich aber sowieso ein Problem mit solchen Beispielen, wo einfache Sachen *viel* zu kompliziert gelöst werden. In der Praxis verwendet man (also zumindest die Leute die ich kenne :-) MVC auch nur wenn das etwas bringt. Sonst reicht eine Trennung zwischen Logik und GUI völlig aus. Ein Beispiel für MVC sollte zeigen was der Vorteil davon ist. Wenn man das Beispiel problemlos auf eine UI-Klasse, die regelmässig die Zeitfunktion aufruft, zusammendampfen kann, dann war das Beispiel IMHO schlecht.
Sofern ich die Äußerungen und das Paper von Reenskaug [1][2] richtig verstanden habe, sind die Vorteile des MVC-Patterns:
- Generelle Struktur für eine Vielzahl ähnlicher Probleme (typisches Pattern-Kriterium), d.h. ich muss später nicht nochmal das Rad neu erfinden: Ein Blick in diesen Thread und fünf Minuten darüber nachdenken, sollten reichen, diese Lösung auf eine andere (deutlich komplexere) Problemlösung anzupassen
- Bedingte Änderungen an View und Controller, ohne die jeweils andere Komponente ändern zu müssen (natürlich nur solange, wie die APIs nicht geändert werden)
- Erweiterbarkeit des Models, ohne View oder Controller ändern zu müssen (Natürlich muss dafür die alte Model-API beibehalten werden bzw. darf diese nur erweitert werden)
- Änderungen an Controller und View, ohne das Modell dafür ändern zu müssen (Model-API muss natürlich berücksichtigt werden)
- Klare Aufgabenteilung: Gibt es z.B. ein Problem mit der Darstellung muss nur im View gesucht werden, alle anderen Komponenten können ignoriert werden

Diese Struktur in Einklang mit Nebenläufigkeit zu bringen ist ein programmiertechnisches Niveau, welches ich erreichen möchte.


Viele Grüße
soqhu

Quellen:
[1] http://folk.uio.no/trygver/
[2] http://folk.uio.no/trygver/themes/mvc/mvc-index.html
sohqu
User
Beiträge: 5
Registriert: Donnerstag 28. April 2016, 12:54

Hi BlackJack,

nachfolgend habe ich mal alle Deine Anmerkungen in den Code einfließen lassen. Außerdem habe ich das MVC nun um den erwähnten Observer zwischen View und Model erweitert. Dieser hat jetzt noch keine Funktion. Das wird sich aber noch ändern, sobald die Nebenläufigkeit funktioniert.


Viele Grüße
sohqu

Code: Alles auswählen

# -*- coding:utf-8 -*-

import time
import curses
import sys


class Model():

    def __init__(self):
        self._sleep = 0.1
        self._observers = []

    # Observable-API fuer Observer
    def register(self, observer):
        if observer not in self._observers:
            self._observers.append(observer)

    def unregister(self, observer):
        if observer in self._observers:
            self._observers.remove(observer)

    def notify(self, observer):
        observer.get_notice()

    def notify_all(self):
        for i in self._observers:
            self.notify(i)

    # API fuer View
    def get_time(self):
        return(time.ctime())


class View():

    def __init__(self, model):
        self.model = model
        self.QUIT_KEY = None

    # Observer-Struktur
    def register_to_model(self):
        self.model.register(self)

    def unregister_to_model(self):
        self.model.unregister(self)

    def get_notice(self):
        self.update_view()

    # API fuer Controller und Observer
    def draw_view(self, scr):
        scr.clear()
        scr.border()
        scr.addstr(1, 1, "UHR", curses.A_UNDERLINE)
        scr.addstr(2, 1, "Aktuelle Uhrzeit: " + str(self.model.get_time()))
        scr.addstr(4, 1, "Taste '" + str(self.QUIT_KEY) + "' zum Beenden.")


class Controller():

    def __init__(self, model, view):
        self.model = model
        self.view = view
        self.QUIT_KEY = "q"
        self.view.QUIT_KEY = self.QUIT_KEY

    def run_interface(self, scr):
        self.view.draw_view(scr)
        while True:
            event = scr.getch()
            if event == ord(self.QUIT_KEY):
                break
            else:
                self.view.draw_view(scr)

def start():
    m = Model()
    v = View(m)
    c = Controller(m, v)
    curses.wrapper(c.run_interface)

if __name__ == "__main__":
    sys.exit(start())
BlackJack

Das mit Model und View hast Du schon richtig verstanden, aber wieso muss der View dazu den Zustand vom Model noch mal merken wo er doch Zugriff auf das Model hat und sich die Informationen von dort holen kann. Also das tust Du ja sogar, nur dass Du die Daten dann nicht gleich verarbeitest sondern noch mal speicherst um sie später zu verarbeiten. Der Zwischenschritt erscheint mir nicht sinnvoll.

Nebenläufigkeit und MVC sind zwei orthogonale Themen. Die Frage wie man Nebenläufigkeit in eine MVC-Umgebung bekommt ist also nicht sinnvoll, weil man da alles machen kann was man auch ohne MVC machen könnte. Bei GUIs hat man in der Regel die Hauptschleife des GUI-Rahmenwerks die man nutzen kann um das Programm am laufen zu halten. Und das auch mit allen Möglichkeiten von Nebenläufigkeit. Man kann da Timer verwenden, oder Threads die Ereignisse in diese Hauptschleife einspeisen. An der Stelle ist es IMHO ungünstig wenn Du ein Beispiel für eine GUI ohne GUI-Rahmenwerk machen willst, weil Du dann alles mögliche selber nachprogrammieren musst, das GUI-Rahmenwerke schon liefern.

Was genau Du machst, hängt davon ab was Du integrieren möchtest. Wenn da Messinstrumente dranhängen die aktiv über ein serielles Protokoll Daten liefern, würde man beispielsweise eher einen Thread verwenden in dem die Kommunikation abläuft. Bei der Systemzeit, wo man eigentlich nur ”pollen” kann, würde man eher den Controller regelmässig das Model bitten seine Daten zu aktualisieren.

Python 2.7 hat eigentlich alle Möglichkeiten threadsicher zu programmieren. Was würde Dir denn da fehlen? Das mit `select()` und `socket` klingt auch nicht wirklich einfach. Vor allem hat das ja nichts mit der Systemzeit oder Benutzereingaben über `curses` zu tun. Falls Du das alles über Sockets in eine serielle Form bringen wolltest: Das geht mit `threading` und `Queue.Queue` sehr wahrscheinlich einfacher und verständlicher.

Ein wesentlicher Punkt am verstehen von komplexeren Entwurfsmustern ist IMHO auch zu sehen wann man sie einsetzt, und dazu braucht man auch ein Problem das komplex genug ist. Denn sonst hat man einfach nur eine unnötig komplexe Lösung die man am Ende für andere Probleme als Vorlage verwendet, die diese Komplexität auch nicht benötigen. Das verletzt das KISS-Prinzip (Keep It Simple and Stupid). Das ist IMHO wichtiger als irgendwelche Entwurfsmuster überall drauf zu werfen und damit Code zu schaffen der komplexer als notwendig ist. Falls das Problem komplexer wird, kann man auch *dann* noch eine komplexere, weil dann *passende*, Lösung dafür schreiben. An einem zu einfachen Lernobjekt kann man die wesentlichen Punkte nicht verstehen weil das gar nicht die Herausforderungen und Probleme aufweist, die durch das MVC-Entwurfsmuster gelöst werden. Genau wie Klassen, sind auch Entwurfsmuster kein selbstzweck. Wenn eine Klasse oder ein Entwurfsmuster kein Problem löst, das man mit einer Funktion oder einem einfacheren Muster lösen kann, dann ist das die falsche Lösung.

Wenn man beispielsweise MVC hat und jeweils nur eine Klasse für die drei Komponenten, dann ist MVC eigentlich zu viel. Und eine Trennung zwischen Geschäftslogik (M) und GUI (VC) ist ausreichend. Letzteres lohnt sich erst zu trennen wenn man verschiedene Views oder verschiedene Controller hat. Bei einer Uhr beispielsweise eine Digitaluhr- und eine Analoguhr-Darstellung.

Von `curses` zu Tk zu wechseln ändert vielleicht am Lernziel MVC wenig, aber Du bekommst die ganze Infrasruktur die man bei GUI-Programme so hat und brauchst das nicht selber programmieren.

Wenn Du Beispiele für das MVC-Muster brauchst, dann schau Dir das in einem GUI-Rahmenwerk an. Da gibt es üblicherweise Beispiele und wie gesagt gibt es da subtile bis deutliche Unterschiede zu dem was Du Dir da gerade anschaust. Das heisst Dein selbstgebasteltes Beispiel nützt Dir wenn Du dann tatsächlich eine GUI mit Rahmenwerk XY bauen willst, nicht wirklich viel.

Generell für Muster gilt übrigens das die auch nicht so eingfach 1:1 übertragbar sind, beziehungsweise je nach Programmiersprache oder Rahmenwerk leicht anders aussehen können. Das Observer-Pattern welches Du da implementiert hast, sieht nach Lehrbuch aus, aber nicht nach etwas was ich in Python *so* umsetzen würde. Lehrbuch ist ja meistens statisch typisierte Sprachen mit expliziten Interfaces. In Python würde ich einfach Rückruffunktionen vorsehen und eine Klasse schreiben deren Exemplare aufrufbar sind und bei denen man andere Rückruffunktionen registrieren kann. Dann braucht man dieses ganze `register()`/`unregister()`/…-Geraffel nur *einmal* schreiben, und nicht in jedem ”Observable” im Grunde den gleichen Code wiederholt schreiben. Zudem kann ein ”Observable” dann auch mehr als eine Art von Benachrichtigungen haben und nicht nur eine generisch benannte `notify()`-Methode. Ein Uhr-Model kann dann beispielsweise über die Aktualisierung des Zeitwertes und den Weckalarm über zwei verschiedene ”Signale” informieren, bei denen man sich getrennt registrieren kann. Also so wie der Signal-Mechanismus, den man von Qt oder Gtk kennt. Hatte ich schon mal erwähnt, dass es Sinn machen könnte ein GUI-Rahmenwerk zu verwenden, damit man das nicht zur Hälfte nachprogrammieren muss was die so bieten? ;-)

Eine Methode die `get_irgendwas()` heisst, sollte etwas zurück geben. `get_notice()` verhält sich also sehr überraschend. In Java heisst das bei `Observer`\n beispielsweise `update()`. Das ist aber alles sehr starr, denn in Python (und anderen Sprachen) kann man einfach beliebige aufrufbare Objekte registrieren, egal wie die heissen oder ob die überhaupt einen Namen haben. Eine `get_notify()`-Methode die einfach nur eine andere Method aufruft ist unnötig. Da kann man auch gleich die *richtige* Methode (`update_view()`) registrieren. Das Zeug aus dem GoF-Buch ist wie gesagt für statisch typisierte Sprachen gedacht und man sollte das nicht so übernehmen wie es dort steht. Das steht übrigens auch im Buch selbst. Das sind nur *Muster*. Deren Struktur ist wichtig, nicht das die Sachen alle genau so heissen und mit den gleichen Sprachmitteln umgesetzt werden wie in C++, Java, oder Smalltalk.

Zusammenstückeln von Zeichenketten und Werten mittels `str()` und ``+`` ist übrigens auch kein ”echtes” Python. Das ist okay in BASIC, aber in Python gibt es dafür Zeichenkettenformatierung.

Ich komme überkompliziert auf das hier:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import curses
from datetime import datetime as DateTime


class Time(object):

    def __init__(self, time_changed=lambda: None):
        self.time_changed = time_changed
        self.time = None
        self.update()

    def update(self):
        self.time = DateTime.now()
        self.time_changed()


class TimeUI(object):

    def __init__(self, screen, model):
        self.screen = screen
        self.model = model
        self.quit_char = None

    def redraw(self):
        self.screen.clear()
        self.screen.border()
        self.screen.addstr(1, 1, 'UHR', curses.A_UNDERLINE)
        self.screen.addstr(
            2, 1, 'Aktuelle Uhrzeit: {0}'.format(self.model.time)
        )
        self.screen.addstr(
            4, 1, 'Taste {0!r} zum Beenden.'.format(self.quit_char)
        )


class Controller(object):

    def __init__(self, screen, model, view, quit_char='q'):
        self.screen = screen
        self.model = model
        self.view = view
        self.quit_char = self.view.quit_char = quit_char
        self.model.time_changed = self.view.redraw

    def run(self):
        curses.curs_set(0)
        curses.halfdelay(1)
        while self.screen.getch() != ord(self.quit_char):
            self.model.update()


def main(screen):
    model = Time()
    time_ui = TimeUI(screen, model)
    controller = Controller(screen, model, time_ui)
    controller.run()


if __name__ == '__main__':
    curses.wrapper(main)
Ich habe aber deutliche Bauchschmerzen dabei, weil das unsinnig kompliziert. `Controller` ist eine Funktion zu einer Klasse aufgeplustert. Das ist superhässlich.

Das `Time` ein Callback hat macht keinen Sinn, weil das grundsätzlich bei jedem `update()` feuert und das vom Controller ausgelöst wird. Statt das der den Callback mit dem UI verbindet, könnte er auch einfach selber `redraw()` auf dem View nach `update()` auf dem Model aufrufen.

Eigentlich ist das alles sehr umständlich für folgendes:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import curses
from datetime import datetime as DateTime

QUIT_CHAR = 'q'


def main(screen):
    curses.curs_set(0)
    curses.halfdelay(1)

    while screen.getch() != ord(QUIT_CHAR):
        screen.clear()
        screen.border()
        screen.addstr(1, 1, 'UHR', curses.A_UNDERLINE)
        screen.addstr(
            2, 1, 'Aktuelle Uhrzeit: {0}'.format(DateTime.now())
        )
        screen.addstr(
            4, 1, 'Taste {0!r} zum Beenden.'.format(QUIT_CHAR)
        )
        

if __name__ == '__main__':
    curses.wrapper(main)
sohqu
User
Beiträge: 5
Registriert: Donnerstag 28. April 2016, 12:54

Hallo BlackJack,

danke für die sehr ausführliche Antwort und die Codebeispiele.
BlackJack hat geschrieben:Das mit Model und View hast Du schon richtig verstanden, aber wieso muss der View dazu den Zustand vom Model noch mal merken wo er doch Zugriff auf das Model hat und sich die Informationen von dort holen kann. Also das tust Du ja sogar, nur dass Du die Daten dann nicht gleich verarbeitest sondern noch mal speicherst um sie später zu verarbeiten. Der Zwischenschritt erscheint mir nicht sinnvoll.
Es war aus meiner Sicht einfacher, eine "update_view()" Funktion zu haben, anstatt im Code dann so Geschichten wie z.B. "self.model.get_time()" einzubauen. Faustregel, die mir mal beigebracht wurde: "Je mehr Referenzen in einem Aufruf, desto schlechter zu lesen und später zu verstehen". In dem Zusammenhang meine ich mich an "Wagon-Programming" oder "Train-Programming" oder so ähnlich zu erinnern. Weil es wie ein langer Zug aussieht und dennoch nur die selbe Strecke zurücklegen kann, aber dabei mit steigender Güterwagenanzahl schwerer anzufahren und abzubremsen ist.
Somit ist ein "referenz1.referenz2.referenz3.referenz4.funktion()"-Aufruf deutlich schlechter zu lesen als ein "referenz1.funktion()", bei der der eigentliche Arbeitsteil in einer Methode zusammengefasst wurde.

Stand das nicht auch mal im Zusammenhang mit der McCabe-Metrik oder erinnere ich mich gerade falsch?
BlackJack hat geschrieben: Nebenläufigkeit und MVC sind zwei orthogonale Themen. Die Frage wie man Nebenläufigkeit in eine MVC-Umgebung bekommt ist also nicht sinnvoll, weil man da alles machen kann was man auch ohne MVC machen könnte. Bei GUIs hat man in der Regel die Hauptschleife des GUI-Rahmenwerks die man nutzen kann um das Programm am laufen zu halten. Und das auch mit allen Möglichkeiten von Nebenläufigkeit. Man kann da Timer verwenden, oder Threads die Ereignisse in diese Hauptschleife einspeisen. An der Stelle ist es IMHO ungünstig wenn Du ein Beispiel für eine GUI ohne GUI-Rahmenwerk machen willst, weil Du dann alles mögliche selber nachprogrammieren musst, das GUI-Rahmenwerke schon liefern.
Das leuchtet mir gerade nicht ein. Sofern ich das Konzept jetzt richtig verinnerlicht habe, benötige ich eigentlich zwei Schleifen: Eine für die Events im User Interface und eine für die Statusänderungen im Modell. Was übersehe ich?
BlackJack hat geschrieben: Was genau Du machst, hängt davon ab was Du integrieren möchtest. Wenn da Messinstrumente dranhängen die aktiv über ein serielles Protokoll Daten liefern, würde man beispielsweise eher einen Thread verwenden in dem die Kommunikation abläuft. Bei der Systemzeit, wo man eigentlich nur ”pollen” kann, würde man eher den Controller regelmässig das Model bitten seine Daten zu aktualisieren.
Das ist ein verdammt guter Ansatz. Mir fällt gerade auf, dass die eigentliche Problemlösung auch unabhängig von den Livedaten geschehen kann. Damit fällt ein großer Teil der Programm-Komplexität weg. Danke!
BlackJack hat geschrieben: Python 2.7 hat eigentlich alle Möglichkeiten threadsicher zu programmieren. Was würde Dir denn da fehlen? Das mit `select()` und `socket` klingt auch nicht wirklich einfach. Vor allem hat das ja nichts mit der Systemzeit oder Benutzereingaben über `curses` zu tun. Falls Du das alles über Sockets in eine serielle Form bringen wolltest: Das geht mit `threading` und `Queue.Queue` sehr wahrscheinlich einfacher und verständlicher.
Leider finde ich den Artikel nicht mehr. Vermutlich hat sich in der Zeit seit dem Artikel auch einiges geändert. Zumindest Queue war mir nicht bekannt. Danke für den Hinweis darauf.
BlackJack hat geschrieben: Ein wesentlicher Punkt am verstehen von komplexeren Entwurfsmustern ist IMHO auch zu sehen wann man sie einsetzt, und dazu braucht man auch ein Problem das komplex genug ist. Denn sonst hat man einfach nur eine unnötig komplexe Lösung die man am Ende für andere Probleme als Vorlage verwendet, die diese Komplexität auch nicht benötigen. Das verletzt das KISS-Prinzip (Keep It Simple and Stupid).
Dem stimme ich zu. Allerdings macht es einen Unterschied im Lernaufwand, ob ich jetzt eine Bibliothek von 1k SLOC durchstöbere, um die Anwendung des Entwurfsmusters zu analysieren, oder ob ich ein Beispiel mit 100 SLOC zur Verfügung habe, welches das Prinzip verdeutlicht.
Der unterschiedliche Zeitaufwand ist aus meiner Sicht auch "kaputte Beispiele" wert.
BlackJack hat geschrieben: Von `curses` zu Tk zu wechseln ändert vielleicht am Lernziel MVC wenig, aber Du bekommst die ganze Infrasruktur die man bei GUI-Programme so hat und brauchst das nicht selber programmieren.
Naja, es gibt da halt noch ein anderes Problem: Tkinter braucht meines Wissens nach einen laufenden X-Server. Die finale Implementierung soll aber möglichst schlank auf einem RasPi ohne X laufen.

Genauer gesagt soll der RasPi den Status von vier Rechnern im lokalen Netzwerk prüfen. Sollte sich der Status eines der Rechner ändern, soll das im Textual User Interface (TUI) schön aufbereitet angezeigt werden. Zusammen mit ein paar Optionen, was jetzt geschehen soll. Also es sollen Details angezeigt und/oder der Rechner neu gestartet werden. Der Ansatz mit dem Polling ist schonmal echt gut. Da habe ich bis zu Deinem Einwand zu kompliziert gedacht. Danke für das Wegholen vom Holzweg.
BlackJack hat geschrieben: Generell für Muster gilt übrigens das die auch nicht so eingfach 1:1 übertragbar sind, beziehungsweise je nach Programmiersprache oder Rahmenwerk leicht anders aussehen können. Das Observer-Pattern welches Du da implementiert hast, sieht nach Lehrbuch aus, aber nicht nach etwas was ich in Python *so* umsetzen würde. Lehrbuch ist ja meistens statisch typisierte Sprachen mit expliziten Interfaces. In Python würde ich einfach Rückruffunktionen vorsehen und eine Klasse schreiben deren Exemplare aufrufbar sind und bei denen man andere Rückruffunktionen registrieren kann.
Darauf gehe ich zu einem späteren Zeitpunkt nochmal ein. Momentan muss ich das erstmal in meinem Weltbild über Patterns unterbekommen.
BlackJack hat geschrieben: Dann braucht man dieses ganze `register()`/`unregister()`/…-Geraffel nur *einmal* schreiben, und nicht in jedem ”Observable” im Grunde den gleichen Code wiederholt schreiben. Zudem kann ein ”Observable” dann auch mehr als eine Art von Benachrichtigungen haben und nicht nur eine generisch benannte `notify()`-Methode. Ein Uhr-Model kann dann beispielsweise über die Aktualisierung des Zeitwertes und den Weckalarm über zwei verschiedene ”Signale” informieren, bei denen man sich getrennt registrieren kann. Also so wie der Signal-Mechanismus, den man von Qt oder Gtk kennt. Hatte ich schon mal erwähnt, dass es Sinn machen könnte ein GUI-Rahmenwerk zu verwenden, damit man das nicht zur Hälfte nachprogrammieren muss was die so bieten? ;-)
In dieser Situation gab es nur einen Observer und ein Observable/Subject. Deshalb habe ich nicht eingesehen, warum ich dafür extra zwei Klassen bauen soll, von denen ich ableite. Deshalb ist der Rohentwurf erstmal in die Klassen gewandert. Den kann ich später immernoch verschlanken.
BlackJack hat geschrieben: Eine Methode die `get_irgendwas()` heisst, sollte etwas zurück geben. `get_notice()` verhält sich also sehr überraschend. In Java heisst das bei `Observer`\n beispielsweise `update()`. Das ist aber alles sehr starr, denn in Python (und anderen Sprachen) kann man einfach beliebige aufrufbare Objekte registrieren, egal wie die heissen oder ob die überhaupt einen Namen haben. Eine `get_notify()`-Methode die einfach nur eine andere Method aufruft ist unnötig. Da kann man auch gleich die *richtige* Methode (`update_view()`) registrieren. Das Zeug aus dem GoF-Buch ist wie gesagt für statisch typisierte Sprachen gedacht und man sollte das nicht so übernehmen wie es dort steht. Das steht übrigens auch im Buch selbst. Das sind nur *Muster*. Deren Struktur ist wichtig, nicht das die Sachen alle genau so heissen und mit den gleichen Sprachmitteln umgesetzt werden wie in C++, Java, oder Smalltalk.
Zunächst einmal ist es zum Erlernen besser, nicht gleich von den Büchern abzuweichen. In erster Linie ging es hierbei gerade darum, dass ich das Zusammenspiel der Konzepte erstmal in meinen Betonschädel reinbekomme. Dabei ist "explizit" immernoch besser als "implizit". D.h. zwar mehr Bloat, aber auch mehr Chancen, evtl. Missveständnisse frühzeitig zu erkennen und zu beheben.

Die "get_notice()"-Methode hätte ich in der Tat besser "get_status()" genannt. Mir war es wichtig, an der Stelle ein Pull-Pattern zu praktizieren, damit bei einem Umbau der Datenabfrage/Darstellung die notwendigen Änderungen ausschließlich im View passieren müssen. Ziel war es ja gerade, dass das Model so gut wie nichts über das View wissen braucht. Laut Lehrbuch ist "notify()" im Observer-Pull-Pattern nur das Signal für "Ruf mich bitte schnellstens zurück, ich habe Neuigkeiten für Dich!". Also löst Model durch "notify()" den "Hinweis" im View aus, damit das View sich dann per "get_status()" (fälschlicherweise "get_notice()") die eigentlichen Informationen abholt. Dadurch bleibt die Kopplung wenigstens teilweise "lose". Das notify() könnte z.B. im View auch ein Flag setzen, das dann zu gegebener Zeit einen "Rückruf" auslöst (z.B. wenn die Beschaffung unterschiedlicher Informationen unterschiedlich viel Zeit braucht und im Augenblick der Darstellung alle Informationen möglichst wenig zeitlichen Abstand zueinander haben sollten).

Wenn ich auf "weniger Methoden" optimieren wollte, hätte ich ein Observer-Push-Pattern umgesetzt. In dem Fall würde ich jedoch beim Umbau des View mindestens auch die notifiy()-Methode im Model mit ändern müssen. Das läuft der MVC-Idee komplett zuwider, weil dann die notify()-Methode geradezu "intime" Details über das View kennen müsste (nämlich in dieser Situation mindestens "self.observer.current_time" oder alternativ "self.observer.draw_view()"; wieso sollte das Model von der Variable "current_time" oder der Methode "draw_view()" wissen?). Es erhöht also den Aufwand bei Änderungen und löst die scharfe Abgrenzung zwischen Model und View auf. Damit ist die Arbeitsteilung gefährdet, was die Maintenance erschwert.
BlackJack hat geschrieben: Zusammenstückeln von Zeichenketten und Werten mittels `str()` und ``+`` ist übrigens auch kein ”echtes” Python. Das ist okay in BASIC, aber in Python gibt es dafür Zeichenkettenformatierung.
Guter Punkt. Laut offizieller Dokumentation ist beides gültig. Also ist beides "echtes" Python. Die Format-Geschichten finde ich während des Rapid-Prototypings eher störend, weil ich mir dann bei mehreren Variablen kurz mal Gedanken über die Variablen, deren Reihenfolge und deren Darstellung machen muss. Das sind dann zwar nur ein paar Sekunden, aber die bringen mich dann schon wieder aus dem eigentlichen Programmablauf, in den ich dann wieder mit mehr Aufwand zurückfinden muss. Deshalb erlaube ich mir den Luxus, die Ausgaben Quick-and-Dirty zu praktizieren und die Format-Sachen während des Polishings einzubauen.
BlackJack hat geschrieben: Ich komme überkompliziert auf das hier:
Danke für die Codebeispiele. Das sind Augenöffner, aus denen ich gerade viel lerne. Damit werde ich noch einige Zeit beschäftigt sein.


Viele Grüße
sohqu
Sirius3
User
Beiträge: 17703
Registriert: Sonntag 21. Oktober 2012, 17:20

sohqu hat geschrieben:Es war aus meiner Sicht einfacher, eine "update_view()" Funktion zu haben, anstatt im Code dann so Geschichten wie z.B. "self.model.get_time()" einzubauen.
Das View greift auf das Model zu. Das muß es ja auch per Definition. Da sehe ich jetzt noch keine Güterzüge fahren. Der Model-View Ansatz soll ja gerade verhindern, dass unterschiedliche Views mit potentiell unterschiedlichen Daten arbeiten, daher die Bündelung in einem Model.
sohqu hat geschrieben:Das leuchtet mir gerade nicht ein. Sofern ich das Konzept jetzt richtig verinnerlicht habe, benötige ich eigentlich zwei Schleifen: Eine für die Events im User Interface und eine für die Statusänderungen im Modell. Was übersehe ich?
Normalerweise werden Änderungen im Model ja durch das User-Interface getriggert. Die Oberhand behält also auch bei MVC das UI. Umgekehrt muß auch beim nicht getrennten ViewController bei externem Input auf Änderungen in den Daten reagieren.
sohqu hat geschrieben:Zumindest Queue war mir nicht bekannt.
Queues sind eigentlich die einzige Möglichkeit Daten zwischen Threads auszutauschen, wenn man gemeinsam genutzten Speicher verhindern will (was nur zu den von Dir eingangs erwähnten Problemen mit Locks und race-conditions führt).
sohqu hat geschrieben:Die "get_notice()"-Methode hätte ich in der Tat besser "get_status()" genannt.
Das Problem am Namen ist das "get_" am Anfang. Weil eine Funktion, die get_xy heißt erwartungsgemäß etwas an den Aufrufer zurückgibt. In Python sind Funktionen/Methoden auch Objekte, daher ist das explizite Definieren von Schnittstellen-Methoden unnötig. Eine notify-Methode braucht kein Objekt mit einer get_notice-Methode, sondern nur ein Objekt das man aufrufen kann.
Ich glaube Du verwechselst "lose Kopplung" mit zeitlich entkoppelter Ausführung. Ersteres brauchst Du bei einem Model-View-Konzept, zweiteres bei Multi-Threading.
Ein "observer.current_time" darf es erst gar nicht geben, weil das die Daten des Models sind. Und ob die Update-Methode des Views nun get_notify oder draw_view heißt, ist wie oben erwähnt egal, weil das Model zum Benachrichtigen nur irgendeine Methode bekommt.
sohqu hat geschrieben:Die Format-Geschichten finde ich während des Rapid-Prototypings eher störend
Hier zieht das Argument, mach es gleich beim ersten Mal richtig.
BlackJack

@sohqu: `self.model.something` oder `self.model.get_something()` (Vorsicht: triviale Getter und Setter sind nicht „pythonisch“!) ist nicht zu lang. In anderen Sprachen mit implizitem `self`/`this` wäre das auch nur `model.something` oder `model.get_something()`. Das Objekt kennt sich selbst und es kennt seine eigenen Attribute und darf darauf dann auch Attribute abfragen. Würde man das an der Stelle schon ”verbieten”, könnte ein Objekt die eigenen Attribute gar nicht richtig verwenden. Man sollte aufpassen das man nicht all zu tief darüber hinaus durchgreift weil dann das Objekt irgendwann zu viel Wissen über die anderen Objekte benötigt und die Kopplung dadurch höher wird.

Ob der „Train(wreck)“- oder „Wagon“-Stil gut oder schlecht lesbar ist, hängt teilweise vom Geschmack und teilweise vom konkreten Problem ab. Es gibt auch APIs die absichtlich so gestrickt sind. In Python würden mir da beispielsweise spontan `lxml.objectify`, BeautifulSoup, oder SQLAlchemy einfallen.

Beispiel für das ”durchgreifen” in eine XML-Baumstruktur:

Code: Alles auswählen

In [22]: doc = lxml.objectify.parse('test.docbook')

In [23]: doc.getroot().articleinfo.author.email
Out[23]: 'ese@example.com'
Bei diesem Dokument:

Code: Alles auswählen

<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
<article lang="de">
  <articleinfo>
    <title>Das MVC-Entwurfsmuster</title>
    <date>2016-04-29</date>
    <author>
      <firstname>Erich</firstname>
      <othername>S.</othername>
      <surname>Entwickler</surname>
      <email>ese@example.com</email>
    </author>
    <authorinitials>ESE</authorinitials>
    <revhistory>
      <revision>
        <revnumber>0.1</revnumber>
        <date>2016-04-29</date>
        <authorinitials>ESE</authorinitials>
        <revremark>Erste Rohfassung.</revremark>
      </revision>
    </revhistory>
  </articleinfo>
  <abstract id="abstract">
    <simpara>
      Dies soll ein Tutorial zum <emphasis>MVC-Entwurfsmuster</emphasis> 
      sein/werden.
    </simpara>
  </abstract>
  <section id="sec_entwurfsmuster">
    <title>Entwurfsmuster</title>
    <simpara>
      Bis auf die <xref linkend="abstract"/> gibt es hier noch nichts zu sehen.
      <footnote>
        <simpara>Und es wird auch nicht mehr viel dazu kommen. :-)</simpara>
      </footnote>
    </simpara>
    <simpara>
      Wenn da noch etwas dazu kommen sollte, dann müsste man wohl erst einmal
      <indexterm><primary>Entwurfsmuster</primary></indexterm>
      <emphasis>Entwurfsmuster</emphasis> einführen, und dann in einem weiteren
      <link linkend="sec_mvc_entwurfsmuster">Abschnitt über das
      MVC-Entwurfsmuster</link> näher auf dieses Entwurfsmuster eingehen.
    </simpara>
  </section>
  <section id="sec_mvc_entwurfsmuster">
    <title>Das MVC-Entwurfsmuster</title>
    <simpara>
      …
      <indexterm>
        <primary>Entwurfsmuster</primary><secondary>MVC</secondary>
      </indexterm>
      <indexterm>
        <primary>MVC</primary>
      </indexterm>
    </simpara>
  </section>
</article>
Bei SQLAlchemy wird sogar ein Entwurfsmuster verwendet, welches zu so einer API führt: das Builder-Pattern.

Wegen den zwei Schleifen: Die Statusänderung im Model muss in diesem konkreten Fall doch auch von ausserhalb des Models angestossen werden, weil Du die Systemzeit nur ”pollen” kannst. Also kann man das auch in nur einer Schleife abhandeln. Mache ich in meinem Beispiel ja. Bei den meisten GUI-Rahmenwerken kann man sich an dieser Stelle entscheiden ob man über die Ereignisschleife zeitgesteuerte Ereignisse erzeugt, die dann im Model eine Aktualisierung veranlassen. Oder ob man für das Model zum Beispiel einen Thread laufen lässt, der im Falle einer Änderung des Zustands ein Ereignis in die Ereignisschleife einspeist, auf das dann entweder Controller oder View entsprechend reagieren. Für die Umsetzung gibt es hier jetzt aber wieder Unmengen an Variationen. Ob man einen Timer hat der ”Observable” ist, ob man das Observer-Pattern, das Signal/Slot-Konzept, oder gar ein Publisher/Subscriber-Pattern zur Kommunikation verwendet.

Für den Observer braucht man keine eigene Klasse wenn man einfach `__call__()` als API für die Benachrichtigung wählt. Dann kann man da jedes aufrufbare Objekt als Observer verwenden, also Funktionen, Methoden, und andere Typen die `__call__()` implementieren. Mann bräuchte also nur für das Observable eine eigene Klasse wenn man das nicht überall reinschreiben möchte. Und wenn man ein Observable haben möchte das nur einen Observer benachrichtigt, dann reicht ein simpler Callback wie in meinem Beispiel. Und diese Callback-Funktion kann man später auch ganz einfach durch ein Objekt ersetzen bei dem sich mehr als eine Rückruffunktion registrieren kann. Grobes Konzept:

Code: Alles auswählen

class Callbacks(object):

    def __init__(self):
        self.callbacks = list()

    def __call__(self, *args, **kwargs):
        for callback in self.callbacks:
            callback(*args, **kwargs)

    def add(self, callback):
        self.callbacks.append(callback)

    def remove(self, callback):
        self.callbacks.remove(callback)
Entwurfsmuster sind nur abstrakte Muster, dass heisst dort vom Buch abzuweichen ist etwas anderes als von Büchern mit konkreten APIs oder anderen Techniken abzuweichen. Im GoF-Buch steht das sogar als Warnung wenn ich mich richtig erinnere, dass man das nicht sklavisch 1:1 übernehmen soll, sondern sich anschauen soll welche Probleme ein Muster löst, und was der Sinn hinter den Bestandteilen ist. Falls etwas im gegebenen Kontext keinen Sinn macht, dann sollte man das auch nicht so schreiben, nur um einem Beispiel aus einem Buch zu folgen, welches für eine andere Programmiersprache geschrieben wurde, oder das mit einer API zusammenarbeiten muss, bei der die Vorgehensweise nach Buch ”Reibung” erzeugt. Wenn ein Entwurfsmuster ein Problem löst welches man mit Sprachmitteln der gewählten Programmiersprache lösen kann, dann ist ”explizit” sicher nicht besser als ”implizit” die Programmiersprache idiomatisch zu verwenden. Die Sprache idiomatisch zu verwenden, sollte vorrang vor Entwurfsmustern haben die man in anderen Sprachen verwenden muss, weil die keine Sprachmittel haben um das idiomatisch zu lösen.

Es gibt ja beispielsweise das Iterator/Iterable-Entwurfsmuster. Das wird keiner der bei Verstand ist in Python so implementieren wie es für viele andere Sprachen als Entwurfsmuster beschrieben wird. Man kann die Sprache ignorieren und das hier schreiben:

Code: Alles auswählen

import sys


class FibonacciIterator(object):

    def __init__(self):
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def next(self):
        self.a, self.b = self.b, self.a + self.b
        if self.a > sys.maxint:
            raise StopIteration
        return self.a

fibonaccis = FibonacciIterator()
while True:
    try:
        number = next(fibonaccis)
    except StopIteration:
        break
    print(number)
Oder man programmiert in ”echtem” Python und schreibt das hier:

Code: Alles auswählen

def iter_fibonaccis():
    a, b = 0, 1
    while a <= sys.maxint:
        a, b = b, a + b
        yield a

for number in iter_fibonaccis():
    print(number)
Auch in meinem Beispiel von Deinem Programm bleibt die Kopplung lose. Das Model sollte nicht nur möglichst wenig über den View wissen, sondern gar nichts. Trotzdem ist eine `notify()`-Methode auf dem View überflüssig, denn dass das Model nichts von einer `redraw()`-Methode wissen darf, heisst ja nicht das man die nicht von aussen als Rückruffunktion übergeben darf. Also in sofern darf das Model schon die `redraw()`-Methode kennen, nur halt nicht ”statisch”.

Ich finde `str()` und ``+`` und Zeichenkettenliterale komplizierter zu schreiben und vor allem auch zu lesen. Insbesondere wenn man das mal irgendwo im Terminal, in einem Diff oder so, ohne Syntax-Highlighting lesen muss, komme ich gerne mal durcheinander wo eigentlich Zeichenkettenliteral und wo Code anfängt/aufhört und wie das Endprodukt wohl aussehen mag. Klar ist das auch gültiges Python. Das ist aber die erste Iteratorvariante weiter oben auch. ;-)

Welches konkrete Problem willst Du mit dem MVC-Entwurfsmuster eigentlich lösen? Das Entwurfsmuster ist nicht *allgemein* dazu da eine GUI/TUI zu strukturieren! Das ist kein Muss für eine Anwendung mit Benutzerschnittstelle damit man guten, sauberen Code hat. Ich habe so ein bisschen den Eindruck das Du kein Problem hast, welches Du mit einem Entwurfsmuster strukturieren möchtest, sondern das Du das MVC-Entwurfsmuster hast und jetzt mit aller Gewalt das Problem in diese Struktur prügeln willst. Egal ob das passt, oder nicht.
BlackJack

Info am Rande: Das `urwid`-Modul ist ein bisschen besser ausgestattet als `curses` wenn man ein Text-UI schreiben möchte. Zwar nicht so gut wie das was man unter DOS zum Beispiel von Borland gewohnt war, aber immerhin gibt es eine Hauptschleife die sich um Tastatur (und Maus!) kümmert.
Antworten