Einige echte praktische Beispiele zum Unterrichten objektorientierter Konzepte und Programmierung (in Python)
Ich habe meinen Gymnasiasten beigebracht, einige einfache Python-Skripte zu schreiben, wobei ich einige Ideen aus "Automate the Boring Stuff with Python" übernommen habe , zB das Ändern von Dateinamen in einem Ordner in ein bestimmtes Namensmuster.
Mein nächstes Ziel ist es, ihnen objektorientierte Konzepte beizubringen und ihnen, wenn möglich, einige "echte" Beispiele zum Ausprobieren zu geben. Aber die meisten Materialien, die ich finde (die meisten Top-Suchergebnisse von Google) über die OO-Programmierung (nicht nur Python) sind mit schlechten Beispielen beladen, wenn ich das so sagen darf. Ich mag es wirklich nicht, Auto/Fahrzeug oder Tier/Hund zu benutzen, um ihnen das OO-Konzept und OOP beizubringen.
A. es ist langweilig (Sie kennen High-School-Kinder)
B. es hat keinen wirklichen Nutzen.
C. wie ein Kommentar sagte "sie sind fatal fehlerhaft." (Aber wenn Sie das Tierbeispiel wirklich gerne verwenden möchten, überprüfen Sie dieses "Objektorientiertes Design" )
Ich habe vor, pathlib zu verwenden (übrigens haben sie bereits eine grundlegende Vorstellung vom Unterschied zwischen Windows und UNIX)

GUI-Entwicklung könnte ein weiteres gutes Beispiel sein, um OOP beizubringen, aber ich möchte ihnen noch keine GUI beibringen.
Irgendein Vorschlag, OO mit dem "echten" Beispiel einzuführen?
Übrigens, ich bin kein starker Verfechter der objektorientierten Programmierung. Aber die pathlib.Path
Klasse ist gerade im Vergleich zu os.path eine nützliche Abstraktion, auch wenn die Oberstufenschüler das kaum zu schätzen wissen.
---- aktualisieren ----
Ich finde, dass das Python-Logging-Modul ein weiteres gutes Beispiel ist, aber es scheint für unerfahrene Programmierer zu kompliziert zu sein.
Bei der Verwendung des Logging-Moduls verwenden wir hauptsächlich 3 Objekte, Logger, Handler, Formatter . Logger ist die Fassade , wiehttps://docs.python.org/3/howto/logging.html#loggers sagte,
Logger-Objekte haben eine dreifache Aufgabe. Erstens stellen sie dem Anwendungscode mehrere Methoden zur Verfügung, damit Anwendungen zur Laufzeit Nachrichten protokollieren können. Zweitens bestimmen Protokollierungsobjekte, auf welche Protokollnachrichten basierend auf dem Schweregrad (der Standardfilterfunktion) oder den Filterobjekten reagiert werden soll. Drittens geben Logger-Objekte relevante Log-Nachrichten an alle interessierten Log-Handler weiter.
Handler ist ein gutes Beispiel für die Verwendung von Vererbung https://docs.python.org/3/howto/logging.html#useful-handlers und Handler verwenden Formatierer, die die Komplexität von LogRecord verbergen, um die Protokollnachricht zu formatieren.
Zusätzlich zu diesen Klassen gibt es eine Reihe von Funktionen auf Modulebene zur komfortablen Verwendung.
Insgesamt ist es ein gutes Beispiel für die Verwendung von Komposition und Aggregation.
Aber ich bin kein Experte für das Python-Logging-Modul, ich hoffe, jemand, der mit dem Python-Logging-Modul vertraut ist, kann meine Frage beantworten.
--- Update 2 ----
Ich fand, dass Alan Kay die Frage beantwortete: „Was sind die fünf Merkmale des objektorientierten Paradigmas, die Sie für eine gute Software-Engineering-Praxis für wichtig halten? “ mit diesen Worten schwingen meiner Meinung nach mit der Antwort, die Buffy gab, mit. Aber wie diese Ideen an Gymnasiasten oder unerfahrene Programmierer vermittelt werden können, ist eine weitere Herausforderung.
ein „Teil“-Konstrukt, das sein Inneres vor seinem Äußeren schützen kann und umgekehrt
ein „Kommunikations“-Konstrukt, das Interaktionen vermitteln und mit Abhängigkeiten umgehen kann
ein „System“-Konstrukt, das eine Kombination von Teilen und Kommunikation ist, die rekursiv in einen Teil passen kann, und alles wird auf diese Weise gemacht
die „Nachrichten“, die kommuniziert werden, beziehen sich auch auf die Systeme
das System, das hergestellt wird, wird mit den gleichen Arten von Systemen hergestellt ...
Antworten
Ich erwarte, dies ein paar Mal zu aktualisieren und hoffe, irgendwann eine lange Antwort zu haben.
Aber lassen Sie mich zunächst einige Dinge erklären, die Ihnen vielleicht bereits bekannt sind, anderen Lesern jedoch möglicherweise nicht.
Erstens geht es bei der OO-Programmierung im Grunde nicht um Vererbung, und zu viele Bücher und Autoren verstehen das nicht. Darüber hinaus verwenden sie Vererbung auf schreckliche Weise, die Software schwer zu verstehen und zu warten macht.
Beachten Sie zum Beispiel, dass die Hierarchie der biologischen Welt im Linnaeus-Stil fast ausschließlich eine von "Schnittstellen" und nicht einmal "abstrakten Klassen" ist, geschweige denn konkreten Klassen. Es gibt beispielsweise keine instanziierten "Säugetiere". Erst am Ausgang der Hierarchie existieren die Dinge tatsächlich, anders als als Ideen. Es gibt natürlich eine gewisse genetische Kontinuität.
Zweitens gibt es einige Prinzipien, die Sie beim Schreiben und Lehren von OO-Design leiten können, aber sie erfordern Disziplin. Und ein Teil dieser Disziplin besteht darin, die Situationen zu kontrollieren, in denen Sie die Regeln "brechen".
Mein erster Rat für jeden, der ein OO-Programmierer werden möchte, ist, nicht in Vererbung, sondern in Komposition zu denken. Komplexe Dinge (Objekte) setzen sich aus anderen Dingen (Objekten) zusammen, die etwas einfacher sind als das enthaltende Objekt und ihm einige wesentliche Dienste leisten. Wenn Sie Klassen schreiben, in denen alle (oder sogar die meisten) Ihrer Instanzvariablen Sprachprimitive sind, dann verstehen Sie es nicht wirklich. Und wenn diese Instanzvariablen (Objekte oder primitive) viele Getter und Setter haben, dann betreiben Sie überhaupt keine OO-Programmierung.
Tatsächlich erfordert das Begehen der obigen Fehler, dass der Programmierer alle Details an allen Stellen des Programms im Auge behält, an denen OO entworfen wurde, um Entscheidungen zu erfassen, damit sie nicht erneut "überprüft" werden müssen. Stellen Sie es ein und vergessen Sie es sozusagen.
Wenn Sie also ein Automobil bauen möchten, betrachten Sie es nicht als Unterklasse von Fahrzeugen (was Ihnen nichts bringt: eine persönliche Drohne und ein Schlachtschiff sind beides Fahrzeuge). Denken Sie daran, anstatt aus verschiedenen Teilen zusammengesetzt zu sein: Motor, Getriebe, Bedienelemente, Unterbringung usw. Und diese Teile selbst bestehen aus Teilen. Ein Motor hat Zünder und Kolben, Auspuff usw. Viele davon bestehen auch aus Teilen. Nur auf der untersten, einfachsten Ebene bauen Sie mit Primitiven.
Zwei der Prinzipien, die es wert sind, im Hinterkopf zu behalten und fast immer zu befolgen, sind das Liskov-Substitutionsprinzip und das Demeter-Gesetz .
Die erste schlägt vor, dass Sie, wenn Sie eine Klasse mit einer Unterklasse erweitern, nicht auch die öffentliche Schnittstelle der Unterklasse erweitern. Dann sind alle Unterklassenobjekte ersetzbar und unterscheiden sich im Verhalten, aber nicht in der Schnittstelle. Demeter hingegen zwingt Sie dazu, expliziteren Code zu schreiben, der dem Leser Zusammenhänge klar macht. Natürlich zwingt es Sie auch, mehr Namen einzuführen, und wenn es sich um Namen handelt, ist Ihr Code klarer.
Das Liskov-Prinzip ist natürlich eines der Elemente von SOLID, die Sie auch in Ihr Denken einbeziehen müssen.
In meiner eigenen Programmierung bin ich Liskov sehr treu und definiere auch Interfaces für die meisten Dinge, bevor ich Klassen schreibe. In der Hitze des Gefechts bin ich Demeter weniger treu. Aber dann muss ich manchmal auch diese abcd-Kaskaden entwirren, um herauszufinden, was ich wirklich meine.
Ein Ziel beim Schreiben von OO-Code ist es, nur sehr kurze Methoden mit einem Minimum an Struktur zu schreiben. Mit anderen Worten, ich versuche, die zyklomatische Komplexität zu minimieren . Nach der vierten Anweisung einer Methode oder wenn die Komplexitätsstufe drei erreicht, beginnen meine Handflächen zu jucken. Damit komme ich nicht immer durch, aber es ist ein Ziel. Die Lösung besteht darin, Komplexität gnadenlos zu refaktorisieren. Zerlegen Sie natürlich Methoden, aber auch "Teile", indem Sie neue Klassen erstellen, um die Komplexität zu bewältigen. Auch wenn viele dieser Klassen Singletons sind, wird der Code normalerweise verbessert und das Ziel von Anfang an im Auge behalten, bedeutet, dass der Refactoring-Schritt weniger erforderlich ist.
Entwurfsmuster sind Werkzeuge, die Sie in den meisten OO-Sprachen benötigen, um ein effektiver Programmierer zu sein. Besonders hilfreich sind Strategy, Decorator, Observer und Iterator. Die meisten davon werden tatsächlich verwendet, um die verschiedenen Java-Bibliotheken zu erstellen.
Um nun zur eigentlichen Frage zu kommen, die hier gestellt wurde. Beachten Sie jedoch, dass viel Lernen stattfinden kann, auch wenn die Schüler das Projekt nie in der vorgesehenen Zeit fertigstellen. Ein agiler Ansatz (Extreme Programming, sagen wir mit Ihnen als "Kunde") an die Entwicklung lässt ihnen einige Funktionen, auch wenn nicht alle Spezifikationen umgesetzt werden.
Dungeon-Spiel
Erstellen Sie ein textbasiertes Dungeon-Spiel. Die Hauptobjekte sind Charaktere (Menschen), Orte und Dinge. Die Orte sind in einer Art Karte, Labyrinth oder Gitter organisiert. Dinge passieren, wenn Charaktere Orte betreten. Charaktere finden und tragen Dinge. Die Dinge haben Aktionen, je nach Art der Dinge. Ein "Zauber" ist ein "Ding", dessen Handlung vom Kontext abhängen kann. Ein "Transporter"-Objekt kann in verschiedenen Räumen unterschiedlich funktionieren (Strategieobjekte).
Beachten Sie, dass das klassische Brettspiel Chutes (oder Snakes) and Ladders eine vereinfachte Version davon ist. Eine textbasierte Version vermeidet viel Komplexität.
Taschenrechner
Ein Taschenrechner hat Teile wie Tasten und das Display. Weniger sichtbar sind der interne Speicher, eventuell ein Stack. Sogar die Operationen können Objekte sein. Das Verhalten der Tasten ändert sich je nach Stand der Berechnung (Strategiemuster). Es ist tatsächlich möglich, einen einfachen Taschenrechner ohne eine einzige IF-Anweisung zu erstellen.
Abstrakter Computer mit einer Assemblersprache
Eine stapelbasierte Computerprozessorsimulation ist ziemlich einfach. Es kann Akkumulatoren und dergleichen geben, aber ein einzelner Stapel, auf dem alle Operationen ausgeführt werden, ist einfach und vollständig. Die Operationen können Objekte (Teile) sein. Ein Vorteil dieses Beispiels ist, dass die meisten der notwendigen Methoden sehr kurz sein können. Ein Programm kann mit einem Java Scanner-Objekt gelesen werden. Es braucht mindestens einen Programmzähler und eventuell einen Rahmenzeiger, wenn die Sprache Unterprogramme unterstützen soll.
Haftnotizen (habe ich noch nicht probiert)
Eine Anwendung, mit der Benutzer Notizen und Querverweise erstellen und organisieren können. Einige Klassen können Notizen, Schlüsselwörter, Verbindungen, Listen sein.
Gefahr
Ein Simulator für das Jeopardy-Spiel aus dem Fernsehen. Kategorien, Antworten, Fragen, Teams, Ergebnisse.
(Vielleicht bald wieder zurück).
Ich fand, dass eine Python-Klasse so einfach zu schreiben ist (für "alltägliche" Aufgaben), dass sie ganz natürlich aus dem Wunsch heraus wachsen kann, Code in der Entwicklung zu vereinfachen und umzugestalten. Es handelt sich um einen pragmatischen Bottom-up-Ansatz; Sie erwarteten, einen einfachen Code für einen bestimmten Zweck zu hacken, dann wächst er ein wenig und Sie stellen fest, dass Sie mehrere Funktionen haben, die "Strukturen" handhaben, die Tupel oder Arrays oder sogar Globals waren. Plötzlich sehen Sie das Licht und erstellen eine Klasse, und die Codegröße wird durch 2 oder mehr geteilt und ist so viel einfacher .
Dies ist eine einfache Möglichkeit, für OOP zu argumentieren, indem man einen "flachen" konkreten vorhandenen Code nimmt und nach der möglichen Abstraktion sucht, die Daten aus den Funktionsparametern in die Klasse verschiebt und verwendet self
.
Dinge wie Vererbung können auf ähnliche Weise fast natürlich entdeckt werden; Sie verwenden eine vorhandene Klasse, die nicht ganz das tut, was Sie wollen, und Sie müssen sie ändern. Anstatt sie zu kopieren und zu ändern, unterteilen Sie eine Methode und ändern oder fügen eine Methode hinzu.
Für konkrete Beispiele können Sie sich die Computerhardware selbst ansehen. Auf einer niedrigen Ebene werden Register oft in mehrere unterschiedliche Funktionen unterteilt. Wenn Sie ein Feature-Bit auf 1 setzen möchten, müssen Sie es um 20 Bit nach links verschieben, den aktuellen Wert des Registers lesen, die Bits 0 bis 5 ausblenden, da sie "1 zum Löschen schreiben" sind und so weiter. Versuchen Sie, einen seriellen Port 16550 uart zu emulieren; es ist gut für die Seele. Und wenn Sie MicroPython verwenden, das auf einem echten Mikroprozessor läuft, können Sie natürlich Ihren Code wahrscheinlich sogar ausprobieren.
Mein bevorzugtes Beispiel sowohl für die Datenmodellierung als auch für OOP (die sich in beiden Fällen um eine Art Normalisierung drehen) ist ein Videoverleih . Es ist vielleicht ein sehr veraltetes Beispiel, Sie können es gerne in eine Bibliothek oder einen Verleih für andere Dinge ändern, aber ich finde, dass das Beispiel der Videothek sowohl die Feinheiten von OOP als auch der Datennormalisierung hervorhebt, während es gleichzeitig sehr einfach ist Kontext zu erfassen.
Das Hauptziel besteht darin, ein Diagramm mit drei Tabellen/Klassen aufzubauen: Customer
, Video
und Rental
(das ist die Kreuztabelle zwischen Kunden und Videos).
Der Rest dieser Antwort sind nur Tipps, wie Sie bestimmte OOP-Grundlagen mit dem vorliegenden Beispiel in Verbindung bringen können.
Warum Objekte?
Nun, wie würden Sie Ihre Videodaten speichern, wenn Sie drei Dinge für eine Vermietung verfolgen möchten: den Kundennamen, die Adresse, den Videonamen und das voraussichtliche Rückgabedatum.
Bitten Sie die Schüler, ein sehr einfaches Programm zu schreiben, das Ihnen sagt, dass Alice Antz gemietet hat, Bob Bee Movie gemietet hat und Charlie Autos gemietet hat. Lassen Sie sie sich auf eine wiederverwendbare PrintRentalInfo
Methode verlassen, aber erlauben Sie ihnen, die Methodenparameter nach eigenem Ermessen zu definieren.
Schüler, die OOP noch nicht gesehen haben, verwenden vier verschiedene Arrays und verlassen sich darauf, dass ein Video in allen vier Arrays auf demselben Index zu finden ist. Erklären Sie ihnen, dass es nicht einfach ist, mit einer "Tüte" mit Kundennamen, einer "Tüte" mit Adressen, einer "Tüte" mit Videonamen und einer "Tüte" mit Rücksendedaten zu arbeiten. Schlagen Sie vor, dass es sinnvoller wäre, statt einer "Tasche" pro Datenfeld eine "Tasche" pro Anmietung zu erstellen.
Erstellen Sie die Rental
Klasse mit den vier Eigenschaften. Erstellen Sie dieselbe Anwendung wie zuvor, verwenden Sie jedoch OOP. Dies zeigt ihnen die Objektinitialisierung, wie verschiedene Objekte die gleiche Struktur, aber individuell einzigartigen Inhalt haben und wie Sie ein Objekt weitergeben können (im Gegensatz zu mehreren Methodenparametern primitiver Typen).
Heben Sie wirklich hervor, wie einfach es ist, dass Sie diese Miet-"Tasche" von Methode zu Methode bewegen und alle relevanten Informationen zusammenhalten können.
Warum mehr als eine Klasse?
Ein vierter Kunde erscheint. Sie heißt auch Alice. Sie haben hier ein Problem, weil Sie jetzt nicht mehr wissen können , welche Alice welche Videos ausgeliehen hat, und Sie möchten nicht die falsche Alice bestrafen.
Außerdem hat uns die ursprüngliche Alice angerufen, um uns mitzuteilen, dass sich ihre Adresse geändert hat. Weisen Sie auf die Schwierigkeit hin, alle Anmietungen durchgehen zu müssen und herauszufinden, dass Sie "old_address" nicht einfach blind in "new_address" ändern können, da möglicherweise andere Kunden an derselben Adresse wohnen, die nicht mit Alice umgezogen sind. Auch auf den Namen kann man sich nicht verlassen, da die vierte Alice auch schon einige Mieten gemacht hat.
Wenn die Schüler immer wieder protestieren, dass Sie dies aufgrund der Kombination von Name und Adresse tun könnten, was passiert, wenn diese beiden Alices unter derselben Adresse wohnen und nur eine von ihnen umzieht.
Schlagen Sie den Schülern vor, dass es sehr sinnvoll wäre, wenn wir eine separate Liste aller unserer Kunden und ihrer Adressen hätten, damit wir sie nicht nur anhand ihres Namens unterscheiden und die Daten einer Person leicht ändern können.
Ziel: Erstellen Sie eine Customer
Klasse und ändern Sie sie Rental
so, dass sie eine Customer
Eigenschaft enthält, im Gegensatz zu Namens- / Adresseigenschaften.
Fokus: sehr hervorheben, wie Sie zwei verschiedene Kundenobjekte haben können, auch wenn ihre Namen und Adressen gleich sind.
Extra: Sie können den gleichen Ansatz zum Erstellen der Video
Klasse verwenden, um die spezifischen Videos zu verfolgen, die Sie haben. Überlassen Sie dies vielleicht den Schülern als Übung, da dies weitgehend dasselbe ist wie zuvor.
Ab diesem Zeitpunkt können Sie die Geschäftslogik basierend auf dem, was Sie präsentieren möchten, erweitern.
- Vererbung - Vielleicht vermietet der Laden
Video
undGame
, aber Sie möchten trotzdemRental
in der Lage sein, eine Verknüpfung zu einem von ihnen herzustellen (mit der BasisklasseRentableObject
) - Schnittstelle - Sie können dasselbe Beispiel als Vererbung verwenden.
- Datennormalisierung - Wie würden wir sowohl die verfügbaren Filme als auch die einzelnen physischen Kassetten verfolgen (wir könnten mehrere desselben Films haben)? Wie könnte man Bußgelder nachverfolgen, die ein Kunde angefallen ist und welche er bereits bezahlt hat?
- Datentransformation - Unser Chef möchte, dass wir einen wöchentlichen Bericht über alle getätigten Anmietungen, zurückgegebenen Anmietungen, angefallenen Bußgelder und Bußgelder von früher, die noch nicht bezahlt wurden , drucken .
- Referenz vs. Wert - Fügen Sie a
Price
zuVideo
und hinzuRental
. Präsentieren Sie, wie Sierental.Price
basierend auf festlegenvideo.Price
, aber beivideo.Price
späteren Änderungenrental.Price
unverändert bleiben. Wiederholen Sie nun die gleiche Übung mit einem Referenzobjekt (zB Änderung des Kundennamens).
Ich finde, dass der Kontext für dieses Beispiel sehr leicht zu verstehen ist und viele Erweiterungsmöglichkeiten bietet. Dies könnte zu einem langfristigen Projekt werden, das Sie ständig erweitern. Dies kann eine wertvolle Lektion sein, wenn Sie Ihren Schülern beibringen möchten, wie sie mit sich ändernden Anforderungen umgehen und die Vorteile sauberer Codierung oder Wartung und Legacy-Entwicklung nutzen.