4.10 Dies und Das 2
Model - View - Controler (MVC-Konzept)
Das Model-View-Controller (MVC)-Konzept ist ein bewährtes Entwurfsmuster in der Softwareentwicklung, das Klassen in drei Gruppen unterteilt:
-
Model (Modell): Das Model ist für die Datenhaltung zuständig. Hierin finden sich Klassen zum Speichern und Abrufen der Daten. Da das Model unabhängig von der Verarbeitung der Daten (Controler) und der Ansicht der Daten (View) ist, kann man in einer guten Programmierung die Datenhaltung ändern, ohne dass dies einen Einfluss auf die Datenverarbeitung bzw. die Darstellung der Daten hat.
-
View (Ansicht): In den Klassen des Views findet die Darstellung der Anwendung sowie die Interaktionen Darstellung der Benutzerinteraktionen statt.
-
Controller (Steuerung): Der Controller fungiert als Vermittler zwischen Model und View. Er verarbeitet Benutzereingaben, aktualisiert das Modell entsprechend und sorgt dafür, dass die Ansicht die aktuellen Daten anzeigt.
Durch diese Trennung der Verantwortlichkeiten fördert das MVC-Muster eine klare Strukturierung des Codes, erleichtert die Wartung und ermöglicht eine parallele Entwicklung der einzelnen Komponenten. In Python wird dieses Muster häufig in Web-Frameworks wie Django verwendet, um eine saubere und skalierbare Anwendungsarchitektur zu gewährleisten.
Guter Stil
Hier wird die Textlänge geprüft. Es ist eine sehr schlanke Lösung einer Praktikumsaufgabe
| # Textlänge prüfen
# 27.11.2024, Schicke Lösung aus dem Praktikum
from machine import Pin, SoftI2C
from ssd1306 import SSD1306_I2C # Import der TimHanewich SSD1306 I2C-Klasse
# Initialisierung der I2C-Pins
i2c = SoftI2C(scl=Pin(17), sda=Pin(16))
# Initialisierung des OLED-Displays mit expliziter I2C-Adresse
oled = SSD1306_I2C(128, 64, i2c, addr=0x3C)
class Eingabe:
def __init__(self, maxLang):
self.maxLang = maxLang
def pruefeText(self, text):
if len(text) <= self.maxLang:
return text
else:
return "Text ist zu lang!"
# Hauptprogramm
eingabeText = Eingabe(16)
text = input("Gib deinen Text ein: ")
anzeigeText = eingabeText.pruefeText(text)
oled.fill(0)
oled.text(anzeigeText, 0, 20)
oled.show()
|
MVC-Konzept
Zu welchem Bereich des MVC-Konzepts gehört hier die Klasse "Eingabe"?
Gut abgesichert
Es ist empfehlenswert, die Variablenübergabe an eine Methode auf ihren Typ zu überprüfen oder den Typ festzulegen.
Nehmen wir das bereits bekannte Beispiel und ändern es entsprechend ab, indem wir alle übergebenen Parameter entweder überprüfen oder konvertieren.
Hier das Originalbeispiel
| # LED-Steuerung mit Funktionen und Klassen
# J. Thomaschewski, 04.09.2024
from machine import Pin
from time import sleep
class LEDController:
def __init__(self, pinNumber):
self.led = Pin(pinNumber, Pin.OUT)
def blink(self, times, delay):
for _ in range(times):
self.led.on()
sleep(delay)
self.led.off()
sleep(delay)
# Erstellen eines LED-Controllers
ledController = LEDController('LED')
# LED blinken lassen
ledController.blink(5, 0.5)
|
Hier ist ein verbessertes und abgesichertes Beispiel. So werden die Methoden der Klasse vor "Störungen von außen" geschützt.
| # "Abgesicherte" LED-Steuerung mit Funktionen und Klassen
# J. Thomaschewski, 27.11.2024
from machine import Pin
from time import sleep
class LEDController:
def __init__(self, pinValue):
try:
if isinstance(pinValue, int):
# Externe LED: pin ist die GPIO-Pin-Nummer
self.led = Pin(pinValue, Pin.OUT)
elif pinValue == 'LED':
# Onboard-LED: String 'LED'
self.led = Pin('LED', Pin.OUT)
else:
raise TypeError("pinValue muss ein int oder der String 'LED' sein.")
except Exception as e:
print(f"Fehler bei der Initialisierung: {e}")
self.led = None # Setze led auf None, da die Initialisierung fehlgeschlagen ist
def blink(self, times, delay):
if self.led is None:
print("LED wurde nicht korrekt initialisiert.")
return
try:
if not isinstance(times, int):
raise TypeError("times muss vom Typ int sein.")
if not isinstance(delay, (int, float)):
raise TypeError("delay muss vom Typ int oder float sein.")
for _ in range(times):
self.led.on()
sleep(delay)
self.led.off()
sleep(delay)
except Exception as e:
print(f"Fehler beim Blinken: {e}")
# Hauptprogramm
ledController = LEDController('LED')
ledController.blink(5, 0.5) # schreiben Sie mal (5.5, 0.5)
|
Und plötzlich haben wir sehr viel Sourcecode, aber auch einen sehr guten Sourcecode.
Klassenattribute und Methoden aufrufen
Hier das Originalbeispiel
| # LED-Steuerung mit Funktionen und Klassen
# J. Thomaschewski, 04.09.2024
from machine import Pin
from time import sleep
class LEDController:
def __init__(self, pinNumber):
self.led = Pin(pinNumber, Pin.OUT)
def blink(self, times, delay):
for _ in range(times):
self.led.on()
sleep(delay)
self.led.off()
sleep(delay)
# Erstellen eines LED-Controllers
ledController = LEDController('LED')
# LED blinken lassen
ledController.blink(5, 0.5)
|
Wir wissen, dass wir die Methode blink()
aus dem Hauptprogramm aufrufen können und zwar (abstrakt geschrieben) mit objektName.blink(wert1, wert1)
.
Wir können aber auch Klassenattribute auslesen und sogar neu überschreiben. Schauen wir uns dies mal an:
| # LED-Steuerung mit Funktionen und Klassen
# J. Thomaschewski, 27.11.2024
from machine import Pin
from time import sleep
class LEDController:
def __init__(self, pinNumber):
self.led = Pin(pinNumber, Pin.OUT)
def blink(self, times, delay):
for _ in range(times):
self.led.on()
sleep(delay)
self.led.off()
sleep(delay)
# Erstellen eines LED-Controllers
ledController = LEDController('LED')
# Auslesen des Klassen-Attributs
print(ledController.led)
# Noch schlimmer: ich kann das Klassenattribut sogar überschreiben
# und damit von außen alles kaput machen. Dazu die folgenden Zeilen auskommentieren!
#ledController.led = Pin(16, Pin.OUT)
#print(ledController.led)
# LED blinken lassen
ledController.blink(5, 0.5)
|
Und selbst unser gut abgesicherte Script mit try - except
bekommt man damit kaputt. Aber man "darf" in Python gemäß Konvention nicht auf Klassen-Attribute aus dem Hauptprogramm zugreifen.
Bei Java, C++, PHP etc. "kann" man darauf nicht zugreifen (sofern die Klasse gut programmiert wurde). Wenn man auf ein "privates" Klassenattribut in Java, C++, PHP etc. zugreift, dann gibt dies eine Fehlermeldung.
Klassen-Attribute in Python
Unterstrich (_): Ein Attributname, der mit einem einzelnen Unterstrich beginnt, z. B. _meinAttribut, signalisiert, dass es sich um ein geschütztes Attribut handelt, auf das nicht direkt von außen zugegriffen werden sollte. Diese Konvention dient hauptsächlich als Hinweis für Entwickler:innen und wird nicht vom Interpreter erzwungen.
Nerd-Wissen zwei Unterstriche
Ein Attributnamezwei Unterstrichen, z. B. __meinAttribut, führt zu Namensveränderung (Name Mangling). Der Interpreter ändert den Namen des Attributs intern, um Kollisionen in Unterklassen zu vermeiden. Dies erschwert den direkten Zugriff von außen, verhindert ihn jedoch nicht vollständig.
Getter und Setter
In der objektorientierten Programmierung ist es oft vorteilhaft, Attribute einer Klasse nicht direkt zugänglich zu machen, sondern den Zugriff über sogenannte Getter- und Setter-Methoden zu steuern. Dies bietet mehrere Vorteile:
-
Datenvalidierung: Bevor ein Attribut ein Wert zugewiesen wird, kann überprüft werden, ob dieser Wert gültig ist und den erwarteten Kriterien entspricht.
-
Kontrollierter Zugriff: Öffentliche Attribute können von überall gelesen und geschrieben werden. Oftmals möchte man jedoch den schreibenden Zugriff einschränken oder an bestimmte Bedingungen knüpfen.
-
Erweiterbarkeit: Durch die Verwendung von Getter- und Setter-Methoden kann später zusätzliche Funktionalität hinzugefügt werden, wie z.B. das Beobachter-Pattern, um Änderungen an Eigenschaften an andere Programmteile weiterzuleiten.
Im folgenden Python-Beispiel wird eine Klasse Student
definiert, die diese Prinzipien anwendet.
| # Klasse Student mit "privaten Attributen" und Hauptprogramm
# J. Thomaschewski, 27.11.2024
class Student:
def __init__(self, name: str, matrNumber: int, phoneNumber: str):
self._name = name
self._matrNumber = matrNumber
self._phoneNumber = phoneNumber
def getMatrNumber(self) -> int:
return self._matrNumber
def getPhoneNumber(self) -> str:
return self._phoneNumber
def setPhoneNumber(self, newPhoneNumber: str) -> None:
# Hier könnten Validierungen des neuen Werts erfolgen
self._phoneNumber = newPhoneNumber
def returnNameAndMatrikel(self) -> str:
return(f"{self._name} hat die Matr.-Nr.: {self._matrNumber}")
# Hauptprogramm
# Erstellen eines Student-Objekts
student = Student("Marie", 7203456, "0491-23403")
# Auslesen der Matrikelnummer
matrikelnummer = student.getMatrNumber()
print(f"Matrikelnummer: {matrikelnummer}")
# Auslesen der Telefonnummer
telefonnummer = student.getPhoneNumber()
print(f"Telefonnummer: {telefonnummer}")
# Setzen einer neuen Telefonnummer
neueTelefonnummer = "01234 567890"
student.setPhoneNumber(neueTelefonnummer)
print(f"Neue Telefonnummer: {student.getPhoneNumber()}")
print(student.returnNameAndMatrikel())
|
Hier ist einige neu, was wir nun im Detail besprechen müssen.
Arbeiten mit zwei Klassen - völlig unabhängig von einander
| class Horse:
def __init__(self, horseType: str):
self._horseType = horseType
def ride(self, location: str) -> str:
return f"Ich habe ein {self._horseType} und reite {location}"
class Student:
def __init__(self, name: str, matrNumber: int):
self._name = name
self._matrNumber = matrNumber
self._phoneNumber = "keine Angabe"
def getName(self) -> str:
return self._name
def getMatriculationNumber(self) -> int:
return self._matrNumber
def setPhoneNumber(self, newPhoneNumber: str) -> None:
self._phoneNumber = newPhoneNumber
def getPhoneNumber(self) -> str:
return self._phoneNumber
# Hauptprogramm
annaHorse = Horse("Deutsches Reitpony")
annaStudent = Student("Anna", 7205555)
# Beispielausgabe
print(annaHorse.ride("im Park"))
print(f"Name des Studenten: {annaStudent.getName()}")
print(f"Matrikelnummer: {annaStudent.getMatriculationNumber()}")
print(f"Telefonnummer vor Änderung: {annaStudent.getPhoneNumber()}")
annaStudent.setPhoneNumber("12345 67890")
print(f"Telefonnummer nach Änderung: {annaStudent.getPhoneNumber()}")
|