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:

  1. 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.

  2. View (Ansicht): In den Klassen des Views findet die Darstellung der Anwendung sowie die Interaktionen Darstellung der Benutzerinteraktionen statt.

  3. 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()}")