Klassenaufbau bei größeren Programmen, MVC

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
problembär

Hallo,

bei größeren Programmen versuche ich, Berechnungen und GUI nach dem "Model-View-Controller"-Modell zu trennen. Da ich das ja nirgendwo in einer (Hoch-) Schule gelernt habe, wollte ich mal fragen, ob "man das so macht" oder doch lieber anders.
Also hier erstmal mein "Skeleton", das ich dann jeweils ausbaue, in vier Dateien:

1. "main.py":

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding: iso-8859-1 -*-

import os

import model
import view

class Controller:

    def __init__(self):

        self.osname = os.name
        self.model = model.Model(self)
        self.view = view.View(self, self.model)

    def run(self):
        self.view.showMain()

    def quit(self):
        self.view.mw.destroy()

if __name__ == "__main__":
    application = Controller()
    application.run()
2. "model.py":

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding: iso-8859-1 -*-

import data

class Model:

    def __init__(self, controller):

        self.controller = controller
        self.data = data.ModelData()
3. "view.py":

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding: iso-8859-1 -*-

import Tkinter as tk

import tkFileDialog
import ScrolledText

import data

class View:

    def __init__(self, controller, model):

        self.controller = controller
        self.model = model
        self.resolution = (1024, 768)
        self.data = data.ViewData()
        self.fonts = self.data.getFonts(self.controller.osname)

    def showMain(self):

        self.mw = tk.Tk()
        self.mw.title("Title")
        self.mw.geometry(self.getGeometry( (66, 60) ))
        self.mw.option_add("*font", self.fonts["app"])
        self.mw.bind(sequence = "<Control-q>", func = lambda e: self.controller.quit())
        self.lab = tk.Label(self.mw,
                           text = "Hallo")
        self.lab.pack()
        self.fr1 = tk.Frame(self.mw)
        self.fr1.pack()
        self.btnexit = tk.Button(self.fr1,
                                 text = "Exit",
                                 command = self.controller.quit)

        self.btnexit.pack()

        # self.mw.after(2000, self.showWinsize)
        self.mw.mainloop()

    def showWinsize(self):
        print self.mw.winfo_width()
        print self.mw.winfo_height()

    def getGeometry(self, winsize):

        x = (self.resolution[0] - winsize[0]) // 2
        y = (self.resolution[1] - winsize[1]) // 2

        if x < 0:
            x = 0

        if y < 0:
            y = 0

        return "+" + str(x) + "+" + str(y)
4. "data.py":

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding: iso-8859-1 -*-

class ViewData:

    def getFonts(self, osname):

        fonts = {"posix" : {"app"   : '"Suse Sans" 16',
                            "small" : '"Suse Sans" 12',
                            "text"  : '"Courier" 20' },

                 "nt"    : {"app"   : 'Arial 10',
                            "small" : 'Arial 8',
                            "text"  : '"courier new" 10' }}
        return fonts[osname]


class ModelData:

    def __init__(self):
        pass
Hier sind auch schon ein paar Berechnungen: Ich verwende Tkinter als GUI-Toolkit und möchte, daß die Anwendung unter Windows und Linux direkt läuft (am besten auch MacOS-X: Könnte da bitte jemand ein paar Angaben zu verfügbaren und geeigneten Fonts und Fontgrößen machen). Außerdem möchte ich das Hauptfenster auf dem Bildschirm zentrieren. Dazu brauche ich die Auflösung und die Größe des Fensters mit den jeweils unterschiedlichen Fonts. Das Dumme ist, daß ich diese Größe erst abfragen kann, wenn Tkinter schon aktiv ist, also das Hauptfenster schon dargestellt wird. Na ja, vielleicht kann auch jemand dazu was sagen, aber darum geht es hier nicht hauptsächlich.

Sondern: Das, was zu berechnen ist, fasse ich meist in weiteren Unterklassen von "Model" zusammen und gebe jeweils "Model" der __init__-Funktion der Unterklasse mit. Das heißt, "Model" ist dann der Unterklasse bekannt. Da "Model" auch "Controller" kennt (und "Controller" "View") können Daten aus den Unterklassen über "Model" und "Controller" auch an "View" weitergeleitet werden (und umgekehrt).
Ein Problem ist, ich bin immer versucht, aus der Unterklasse auf diesem Weg direkt auf "View" zuzugreifen.
Und ich bin ebenso versucht, aus "View" heraus, direkt Daten in "Model" oder den Unterklassen zu manipulieren.
Ist das ok, oder soll man immer alles stur über Funktionen in "Controller" und zusätzlich der jeweiligen manipulierten Klasse abwickeln (das wird dann aber manchmal ziemlich aufwendig und lang)?
Darf also auch eine Instanz direkt Werte in einer anderen Instanz verändern oder soll man das vermeiden?

Anders gefragt: Wenn man ein Objekt "flasche" und ein Objekt "person" hätte, dürfte

Code: Alles auswählen

person.trinkt(flasche)
dann auch direkt

Code: Alles auswählen

flasche.istvoll = False
setzen? Oder müßte man immer in "flasche" eine Funktion "wirdgeleert()" haben, die dann von "person.trinkt()" aufgerufen wird und "self.istvoll = False" setzt?

Soweit erstmal. Bin diesmal echt auf eure Antworten gespannt ...

Gruß
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

problembär hat geschrieben:Ein Problem ist, ich bin immer versucht, aus der Unterklasse auf diesem Weg direkt auf "View" zuzugreifen.
Und ich bin ebenso versucht, aus "View" heraus, direkt Daten in "Model" oder den Unterklassen zu manipulieren.
Ist das ok, oder soll man immer alles stur über Funktionen in "Controller" und zusätzlich der jeweiligen manipulierten Klasse abwickeln (das wird dann aber manchmal ziemlich aufwendig und lang)?
Jein. Das Problem ist, dass es ziemlich aufwendig ist, das sauber zu implementieren. Normalerweise sollte das Model nichts vom Controller oder der View wissen. Wenn allerdings nicht vorhat, sich ein vernünftiges Beobachter-Rahmenwerk zu basteln ist das imo die einfachste Lösung.
Darf also auch eine Instanz direkt Werte in einer anderen Instanz verändern oder soll man das vermeiden?
Klar, sonst wären in Python nicht alle Attribute „public“. Wenn es an irgendeiner Stelle nicht sinnvoll ist, kannst du immer noch eine Methode/property benutzen und vor das Attribut ein Unterstrich(was dann soviel bedeutet wie „Finger weg, sonst ab“) machen. Und wenn du, um irgendwas bestimmtes zu erreichen, mehrere Attribute verändern musst, brauchst du eine Methode.
fabi1511
User
Beiträge: 23
Registriert: Donnerstag 25. Juni 2009, 18:59

Das hat jetzt zwar nichts mit Model-View zu tun, aber warum verwendest du Old-Style-Klassen??
BlackJack

Zur Darstellungsfrage: Ich würde da gar nichts machen und darauf Vertrauen, dass der Benutzer sich die Schriften und Grössen eingestellt hat, die am besten zu seiner Hardware und Sehgewohnheiten passen.

Auch vom platzieren des Fensters würde ich Abstand nehmen -- da kann heutzutage zuviel schief gehen. Es gibt mehrere Möglichkeiten wie man mehr als einen Monitor verwalten kann, und solche Konfigurationen sind heute nicht mehr sooo selten. Und wenn mir irgendeine "schlaue" Applikation das Fenster in die Mitte vom virtuellen Desktop setzt, also eine Hälfte auf dem einen Monitor und die Andere auf dem Zweitmonitor angezeigt wird, dann ärgere ich mich.
problembär

Darii hat geschrieben:Normalerweise sollte das Model nichts vom Controller oder der View wissen.
Uii, dann würde ich aber ernste Probleme bekommen. Wüßte gar nicht, wie ich das dann machen sollte.
BlackJack hat geschrieben:Zur Darstellungsfrage: Ich würde da gar nichts machen und darauf Vertrauen, dass der Benutzer sich die Schriften und Grössen eingestellt hat, die am besten zu seiner Hardware und Sehgewohnheiten passen.
AFAICS übernimmt Tkinter kaum Voreinstellungen des WindowManagers.
Beim Textfeld liest "Word für Windows" z.B. Fonts aus "normal.dot". Könnte sein, daß es sonst auch Fontangaben aus der Windows-Registrierdatenbank liest (wo man sonst mit Regedit beigeht ...).
KDE-Programme übernehmen Einstellungen die im KDE-Kontrollzentrum eingestellt wurden, gnome-Programme die aus dem "gnome-control-center".
Tkinter aber anscheinend nicht. Wenn man da nichts selbst setzt, hat die Oberfläche winzige Schriften. Das führt dann oft dazu, daß Leute sagen, Tkinter-Programme seien "häßlich" (sind sie so auch). Die Mehrheit der Benutzer kann und will auch nichts selbst im Programmcode einstellen. Die sagen dann nur, das ist ein häßliches Programm und löschen es. Ist schon ein Problem, wobei ich dabei natürlich offen für andere / bessere Lösungen bin.

Bald nochmal mehr zum Hauptthema Klassen ...

Gruß
BlackJack

@problembär: Das "Textfeld" von Word ist kein "normaler" GUI-Bestandteil. Die Schriftgrössen dort sind nicht für Monitore mit den unterschiedlichsten Grössen und Auflösungen, sondern absolute Grössen für Ausdrucke auf Papier in der Vorlage festgelegt. Wie die am Bildschirm angezeigt werden, wird zusätzlich durch die Vergrösserungsstufe beeinflusst, die der Benutzer wählt. Eine Option die er für den Rest der GUI (Menüs, Dialogtexte, …) nicht zusätzlich zur Wahl der Schriftgrösse hat.

Bei mir hatte noch keine Tk-Anwendung winzige Schrift. Was wahrscheinlich bedeutet, dass ich mich bei Deinen Anwendungen über zu grosse Schrift beschweren würde. :-)
fhoech
User
Beiträge: 143
Registriert: Montag 9. April 2007, 18:26

Die Mehrheit der Benutzer kann und will auch nichts selbst im Programmcode einstellen.
Sagt ja auch keiner :) Ich würde nur keine Fontvorgaben machen. Ich benutze selbst Tkinter nicht, aber die meisten GUI-Toolkits übernehmen automatisch die systemeigenen Vorgaben für UI-Elemente (was meiner Erfahrung nach z.B. mit wxPython unter Mac/Linux/Windows problemlos funktioniert), ich nehme an, bei Tkinter ist es ähnlich? Ausserdem siehts komisch aus, wenn eine Applikation (z.B.) Arial verwendet, während alle andren Programme sich in der Regel an (System-)Themevorgaben halten.
Und wenn mir irgendeine "schlaue" Applikation das Fenster in die Mitte vom virtuellen Desktop setzt, also eine Hälfte auf dem einen Monitor und die Andere auf dem Zweitmonitor angezeigt wird, dann ärgere ich mich.
Allerdings, oder wenn die Titelzeile unter der Taskleiste klebt (v.a. unter Windows vergessen die Leute scheinbar oft, das die Position der Taskleiste anpassbar ist, und ich hab sie halt gern grundsätzlich oben), oder das Fenster größer ist als der nutzbare Bildschirmbereich, und/oder sich nicht in der Größe anpassen lässt, etc... :)

Zur Fensterpositionierung/-größe: Die letzte Fensterposition könnte sich das Programm idealerweise merken (Konfigurationsdatei o.ä.) und beim nächsten Start wiederherstellen. Danach sollte das Programm überprüfen, ob das Fenster sich in einem sichtbaren Bildschirmbereich befindet und wenn nicht, die Position entsprechend anpassen (kann ja sein, dass sich die Bildschirmkonfiguration geändert hat, oder dort wo sich das Fenster vorher befand, jetzt ein OS-eigenes UI-Element ist, z.B. eine Taskleiste). Dann eine ebensolche Überprüfung auf den verfügbaren/nutzbaren Platz (der i.d.R. nicht der Bildschirmauflösung entspricht, da es ja UI-Elemente des Betriebssystems wie Taskleisten etc. gibt, dafür bringen die GUI-Toolkits Methoden mit) und wenn nötig Anpassung der Fenstergröße an diesen (z.B. das Fenster zuerst so anpassen, das alle UI-Elemente hineinpassen - dazu haben die GUI-Toolkits auch Methoden, es braucht also nicht kümmern, wieviel Platz die Elemente tatsächlich beanspruchen - und dann ggf. Höhe und/oder Breite anpassen, so dass das Fenster nicht größer ist als der nutzbare Bildschirmbereich, Scrollbalken zeigen wenn nötig. Erst dann das Fenster zeigen, so bekommen Nutzer keine "springenden" Fenster zu Gesicht).
Pekh
User
Beiträge: 482
Registriert: Donnerstag 22. Mai 2008, 09:09

problembär hat geschrieben:
Darii hat geschrieben:Normalerweise sollte das Model nichts vom Controller oder der View wissen.
Uii, dann würde ich aber ernste Probleme bekommen. Wüßte gar nicht, wie ich das dann machen sollte.
Das Modell kümmert sich ausschließlich um die Daten. Gesteuert, gefüttert und ausgelesen wird es vom Controller. Wenn die Oberfläche Daten benötigt, klopt sie beim Controller an (Events!) und dieser schreibt sie dann an die richtige Stelle bzw. ruft eine Methode der Oberfläche auf und übergibt die geforderten Daten.

Richtig interessant wird es aber dann, wenn man der reinen Lehre folgt und die Oberfläche auch keine Kenntnis des Controllers hat. Irgendwohin müssen die Events ja geschickt werden. Und da kommen dann die Beobachter ins Spiel. Diese zu implementieren kann aber ganz schön ausarten, weshalb man hier Testbarkeit und Sauberkeit gegen Aufwand abwägen muß. Bei kleineren Tools lohnt sich der Aufwand der Trennung in der Regel eher weniger.
problembär

fhoech hat geschrieben:Ausserdem siehts komisch aus, wenn eine Applikation (z.B.) Arial verwendet, während alle andren Programme sich in der Regel an (System-)Themevorgaben halten.
Stimmt schon, aber Tkinter-Oberflächen sehen oft etwas untypisch aus. Auch die typische Menüleiste muß man erst recht umständlich zusammenbasteln, sonst hat man keine. 100%ig kriegt man also das Look&Feel des OS sowieso nicht hin. Wenn der Benutzer sich nur einigermaßen zuhause fühlt, ist es eigentlich ok.
fhoech hat geschrieben:Zur Fensterpositionierung/-größe: Die letzte Fensterposition könnte sich das Programm idealerweise merken (Konfigurationsdatei o.ä.) und beim nächsten Start wiederherstellen ... Erst dann das Fenster zeigen, so bekommen Nutzer keine "springenden" Fenster zu Gesicht).
Das wäre schon nicht schlecht. Hab' das auch mal gemacht mit einer Konfigurationsdatei, ging, war aber recht umständlich zu prüfen, ob beim Laden eine gültige vorliegt (oder ob jemand drin rumgeschrieben hat). Aber im Prinzip machbar.
Mit der Fenstergröße hatte ich sonst auch mal ein kleines Willkommenfenster gemacht, bevor das Hauptfenster geöffnet wird (um mit Tk.winfo_....() ) benötigte Werte zu messen). So ein Vorabfenster/Logo haben ja auch andere Programme beim Laden, wenn auch wohl zu anderen Zwecken.
Pekh hat geschrieben:Das Modell kümmert sich ausschließlich um die Daten. Gesteuert, gefüttert und ausgelesen wird es vom Controller. Wenn die Oberfläche Daten benötigt, klopt sie beim Controller an (Events!) und dieser schreibt sie dann an die richtige Stelle bzw. ruft eine Methode der Oberfläche auf und übergibt die geforderten Daten.

Richtig interessant wird es aber dann, wenn man der reinen Lehre folgt und die Oberfläche auch keine Kenntnis des Controllers hat. Irgendwohin müssen die Events ja geschickt werden. Und da kommen dann die Beobachter ins Spiel.
Oha, das klingt ja schon ganz schön anders als ich mir das gedacht hatte (s.o.).
Kennt ihr irgendein Code-Beispiel, an dem man mal sehen könnte, wie das umgesetzt wird?

Gruß
Benutzeravatar
Klip
User
Beiträge: 98
Registriert: Donnerstag 10. August 2006, 20:39

Hallo,

ich habe dir mal ein Minimalbeispiel zusammengeklaubt.

Code: Alles auswählen

class Model(object):
    def __init__(self):
        self._args = ""
        self.observers = []
        
    def register_observer(self, observer):
        self.observers.append(observer)
        
    def notify(self):
        [observer.update() for observer in self.observers]
        
    @property
    def args(self):
        return self._args
    
    @args.setter
    def args(self, value):
        self._args = value
        self.notify()
    
    
class GUI(object):
    def __init__(self):
        pass
    
    def show(self, data):
        print data
        
        
class Controller(object):
    def __init__(self, model, view):
        self.model = model
        self.view = view
        
        self.model.register_observer(self)
        
    def update(self):
        self.view.show(self.model.args)
        
        
model = Model()
gui = GUI()
ctrl = Controller(model, gui)

model.args = "new data"
model.args = "even more new data"
Wie du siehst kennt der Controller beide, die anderen sind jeweils unwissend, kennen also nur sich selbst.
Der Controller registriert sich des Weiteren selbstständig von außen als Beobachter von Model, ohne dass irgendwo in Model selbst der Code dafür stehen müsste.

Als Zusatzlektüre hier noch das Observer-Pattern. Wenn du runterscrollst findest du da auch ein Python-Codebeispiel zu dem Pattern.

Ich hoffe das hilft dir beim Verständnis.

Beste Grüße,

Klip
BlackJack

@Klip: Eine "list comprehension" nur um eine Zeile zu sparen und damit eine unnötige Liste zu erstellen ist IMHO unschön.

Dieses Listener registrieren und später benachrichtigen in jede Klasse zu schreiben ist unter Java zum Beispiel üblich, und da nervt's mich auch. Das kann man auch in eine Klasse verpacken und immer wieder verwenden (ungetestet):

Code: Alles auswählen

class Event(object):
    def __init__(self, callbacks=()):
        self.callbacks = set(callbacks)
    
    def __call__(self, *args, **wargs):
        for callback in self.callbacks:
            callback(*args, **kwargs)
    
    def register(self, callback):
        self.callbacks.add(callback)
    
    def remove(self, callback):
        self.callbacks.discard(callback)
So kann man die API der Beteiligten so konzipieren, dass man dort einfach *ein* "callable" als Rückruffunktion hinterlegen kann. Das kann man dann auch machen wenn man wirklich nur eine einfache Funktion da übergeben möchte. Oder man kann eben so ein `Event`-Objekt verwenden und damit mehrere Rückruffunktionen hinterlegen.

Auf jeden Fall würde ich vermeiden so etwas wie "update" hart vorzuschreiben. Funktionen und Methoden sind in Python Bürger erster Klasse -- die kann man auch als Argumente in der Gegend herum reichen.
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

Pekh hat geschrieben:Richtig interessant wird es aber dann, wenn man der reinen Lehre folgt und die Oberfläche auch keine Kenntnis des Controllers hat. Irgendwohin müssen die Events ja geschickt werden.
Das ist wirklich wenig sinnvoll. Ich weiß auch nicht wo du die „reine Lehre“ her hast, aber ursprünglich kannten sich View und Controller gegenseitig. Das ist ja eigentlich auch der Sinn von MVC. Da Events zu verwenden, ist vermutlich nur ein Hilfsmittel das ganze bei statischer Typisierung halbwegs sauber hinzubekommen, bei Python kann man da getrost drauf verzichten und gleich entsprechende Methoden des Controllers aufrufen.
Pekh
User
Beiträge: 482
Registriert: Donnerstag 22. Mai 2008, 09:09

Ertappt :oops:

Ich verwende in der Regel MVP, hatte in letzter Zeit aber zunehmend Schwierigkeiten es begrifflich von MVC abzugrenzen. Hab den Stoff jetzt nachgeholt ;)
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Pekh hat geschrieben:Ich verwende in der Regel MVP, hatte in letzter Zeit aber zunehmend Schwierigkeiten es begrifflich von MVC abzugrenzen.
Sowohl MVC als auch MVP wurden gerade durch Webprogrammierung stark in ihrer ursprünglichen Bedeutung verbogen. Ein gutes Paper über MVP ist IMHO dies: http://www.object-arts.com/papers/TwistingTheTriad.PDF Und eine schöne Übersicht bietet Martin Fowler: http://martinfowler.com/eaaDev/uiArchs.html wo er bereits das MVP von DolphinSmalltalk (das aus dem ApplicationModel-Pattern von VisualWorks und der ursprünglichen Arbeit von Taligent (einem Versuch von IBM und Apple ein neues OS zu bauen) abgeleitet wurde und wo das VisualWorks-Dingens von dem ursprünglichen MVC weiterentwickelt wurde) als neue Variante namens "Supervising Controller" bezeichnet.

Stefan
problembär

Hallo,

@sma: Danke für das Dokument

http://www.object-arts.com/papers/TwistingTheTriad.PDF

Freut mich zu lesen, daß die auch Schwierigkeiten damit hatten, zu bestimmen, welche Klasse welche (direkt oder indirekt) beeinflussen soll (von Model nur über Controller oder auch von Model direkt zu View):
For example, let's say one wants to explicitly change the colour of one or more views dependent on some conditions in the application model. The correct way to do this in MVC would be to trigger some sort of event, passing the colour along with it. Behaviour would then have to be coded in the view to "hang off" this event and to apply the colour change whenever the event was triggered. This is a rather circuitous route to achieving this simple functionality and typically it would be avoided by taking a shortcut and using #componentAt: to look up a particular named view from the application model and to apply the colour change to the view directly. However, any direct access of a view like this breaks the MVC dictum that the model should know nothing about the views to which it is connected.
Auch sehr hilfreich waren die Angaben, welche Klasse von welcher wissen soll:
Generally, a view and controller are directly linked together (usually by an instance variable pointing from one to the other) but a view/controller pair is only indirectly linked to the model. By indirect, I mean that an Observer relationship is set up so that the view/controller pair knows about the existence of the model but not vice versa.
In MVP scheint die "Unkenntnis" von Model sogar noch stärker zu sein.
Trotzdem zeigen die Grafiken im Dokument, daß Model "Benachrichtigungen" ("notifications") an View sendet.
Stellt sich mir also die Frage, wie man solche "Benachrichtigungen" erzeugt, wenn Model weder Controller/Presenter noch View kennt.
(Vielleicht finde ich eine Antwort im zweiten Dokument, das ich noch lesen muß :oops: ...).

Gruß
Antworten