Ein kurzer Überblick über objektorientiertes Software-Design

Demonstriert durch die Implementierung der Klassen eines Rollenspiels

Einführung

Die meisten modernen Programmiersprachen unterstützen und fördern die objektorientierte Programmierung (OOP). Auch wenn wir in letzter Zeit eine leichte Abkehr davon zu sehen scheinen, da die Leute anfangen, Sprachen zu verwenden, die nicht stark von OOP beeinflusst sind (wie Go, Rust, Elixier, Ulme, Scala), haben die meisten immer noch Objekte. Die hier beschriebenen Entwurfsprinzipien gelten auch für Nicht-OOP-Sprachen.

Um erfolgreich klaren, qualitativ hochwertigen, wartbaren und erweiterbaren Code zu schreiben, müssen Sie über Designprinzipien Bescheid wissen, die sich über Jahrzehnte Erfahrung als effektiv erwiesen haben.

Offenlegung: Das Beispiel, das wir durchgehen werden, wird in Python sein. Beispiele dienen dazu, einen Punkt zu beweisen, und können auf andere, offensichtliche Weise schlampig sein.

Objekttypen

Da wir unseren Code um Objekte modellieren werden, wäre es nützlich, zwischen ihren unterschiedlichen Verantwortlichkeiten und Variationen zu unterscheiden.

Es gibt drei Arten von Objekten:

1. Entitätsobjekt

Dieses Objekt entspricht im Allgemeinen einer realen Entität im Problemraum. Angenommen, wir erstellen ein Rollenspiel (RPG), ein Entitätsobjekt wäre unsere einfache HeroKlasse:

Diese Objekte enthalten im Allgemeinen Eigenschaften über sich selbst (wie healthoder mana) und können durch bestimmte Regeln geändert werden.

2. Steuerobjekt

Steuerobjekte (manchmal auch als Managerobjekte bezeichnet ) sind für die Koordination anderer Objekte verantwortlich. Dies sind Objekte, die steuernund nutzen Sie andere Objekte. Ein gutes Beispiel in unserer RPG-Analogie wäre die FightKlasse, die zwei Helden kontrolliert und sie zum Kampf bringt.

Das Einkapseln der Logik für einen Kampf in einer solchen Klasse bietet Ihnen mehrere Vorteile: Einer davon ist die einfache Erweiterbarkeit der Aktion. Sie können dem Helden sehr leicht einen NPC-Typ (Non-Player Character) übergeben, um zu kämpfen, vorausgesetzt, er enthält dieselbe API. Sie können die Klasse auch sehr einfach erben und einige der Funktionen überschreiben, um Ihre Anforderungen zu erfüllen.

3. Grenzobjekt

Dies sind Objekte, die an der Grenze Ihres Systems sitzen. Jedes Objekt, das Eingaben von einem anderen System entgegennimmt oder Ausgaben für dieses erzeugt - unabhängig davon, ob es sich bei diesem System um einen Benutzer, das Internet oder eine Datenbank handelt -, kann als Grenzobjekt klassifiziert werden.

Diese Grenzobjekte sind für die Übersetzung von Informationen in und aus unserem System verantwortlich. In einem Beispiel, in dem wir Benutzerbefehle verwenden, benötigen wir das Grenzobjekt, um eine Tastatureingabe (wie eine Leertaste) in ein erkennbares Domänenereignis (z. B. einen Zeichensprung) zu übersetzen.

Bonus: Wertobjekt

Wertobjekte stellen einen einfachen Wert in Ihrer Domäne dar. Sie sind unveränderlich und haben keine Identität.

Wenn wir sie in unser Spiel integrieren würden, würde eine Moneyoder eine DamageKlasse gut passen. Mit diesen Objekten können wir leicht verwandte Funktionen leicht unterscheiden, finden und debuggen, während der naive Ansatz der Verwendung eines primitiven Typs - eines Arrays von Ganzzahlen oder einer Ganzzahl - dies nicht tut.

Sie können als Unterkategorie von klassifiziert werden EntityObjekte.

Wichtige Gestaltungsprinzipien

Designprinzipien sind Regeln im Software-Design, die sich im Laufe der Jahre als wertvoll erwiesen haben. Wenn Sie diese strikt befolgen, können Sie sicherstellen, dass Ihre Software von erstklassiger Qualität ist.

Abstraktion

Abstraktion ist die Idee, ein Konzept in einem bestimmten Kontext auf das Wesentliche zu vereinfachen. Sie können das Konzept besser verstehen, indem Sie es auf eine vereinfachte Version reduzieren.

Die obigen Beispiele veranschaulichen die Abstraktion - sehen Sie sich an, wie die FightKlasse strukturiert ist. Die Art und Weise, wie Sie es verwenden, ist so einfach wie möglich - Sie geben ihm zwei Helden als Argumente für die Instanziierung und rufen die fight()Methode auf. Nicht mehr, nicht weniger.

Die Abstraktion in Ihrem Code sollte der Regel der geringsten Überraschung folgen. Ihre Abstraktion sollte niemanden mit unnötigem und nicht verwandtem Verhalten / Eigenschaften überraschen. Mit anderen Worten - es sollte intuitiv sein.

Beachten Sie, dass unsere Hero#take_damage()Funktion nichts Unerwartetes bewirkt, wie das Löschen unseres Charakters beim Tod. Aber wir können erwarten, dass es unseren Charakter tötet, wenn seine Gesundheit unter Null geht.

Verkapselung

Kapselung kann als Einsetzen von etwas in eine Kapsel betrachtet werden - Sie begrenzen die Exposition gegenüber der Außenwelt. In der Software hilft die Einschränkung des Zugriffs auf innere Objekte und Eigenschaften bei der Datenintegrität.

Die Kapselung Blackbox-Boxen sind eine innere Logik und erleichtern die Verwaltung Ihrer Klassen, da Sie wissen, welcher Teil von anderen Systemen verwendet wird und welcher nicht. Dies bedeutet, dass Sie die innere Logik leicht überarbeiten können, während Sie die öffentlichen Teile beibehalten, und sicherstellen können, dass Sie nichts kaputt gemacht haben. Als Nebeneffekt wird das Arbeiten mit der gekapselten Funktionalität von außen einfacher, da Sie weniger darüber nachdenken müssen.

In den meisten Sprachen erfolgt dies über die sogenannten Zugriffsmodifikatoren (privat, geschützt usw.). Python ist nicht das beste Beispiel dafür, da es keine expliziten Modifikatoren gibt, die in die Laufzeit integriert sind, aber wir verwenden Konventionen, um dies zu umgehen. Das _Präfix der Variablen / Methoden kennzeichnet sie als privat.

Stellen Sie sich zum Beispiel vor, wir ändern unsere Fight#_run_attackMethode, um eine boolesche Variable zurückzugeben, die angibt, ob der Kampf beendet ist, anstatt eine Ausnahme auszulösen. Wir werden wissen, dass der einzige Code, den wir möglicherweise gebrochen haben, innerhalb der FightKlasse liegt, weil wir die Methode privat gemacht haben.

Denken Sie daran, dass Code häufiger geändert als neu geschrieben wird. Die Möglichkeit, Ihren Code mit möglichst klaren und geringen Auswirkungen zu ändern, ist die Flexibilität, die Sie als Entwickler wünschen.

Zersetzung

Zerlegung ist die Aufteilung eines Objekts in mehrere separate kleinere Teile. Diese Teile sind leichter zu verstehen, zu warten und zu programmieren.

Stellen Sie sich vor, wir wollten mehr RPG-Funktionen wie Buffs, Inventar, Ausrüstung und Charakterattribute zusätzlich zu unseren Hero:

Ich nehme an, Sie können feststellen, dass dieser Code ziemlich chaotisch wird. Unser HeroObjekt macht zu viel auf einmal und dieser Code wird dadurch ziemlich spröde.

Zum Beispiel ist ein Ausdauerpunkt 5 Gesundheit wert. Wenn wir dies in Zukunft ändern möchten, damit es 6 Gesundheit wert ist, müssen wir die Implementierung an mehreren Stellen ändern.

Die Antwort besteht darin, das HeroObjekt in mehrere kleinere Objekte zu zerlegen, die jeweils einen Teil der Funktionalität umfassen.

Jetzt, nach unserem Held Objekt Funktionalität in Zersetzung HeroAttributes, HeroInventory, HeroEquipmentund HeroBuffObjekte, zukünftige Funktionen Hinzufügen einfacher sein wird, mehr gekapselt und besser abstrahiert. Sie können feststellen, dass unser Code viel sauberer und klarer ist, was er tut.

Es gibt drei Arten von Zerlegungsbeziehungen:

  • Verband- Definiert eine lose Beziehung zwischen zwei Komponenten. Beide Komponenten sind nicht voneinander abhängig, können aber zusammenarbeiten.

Beispiel:Hero und ein ZoneObjekt.

  • Aggregation - Definiert eine schwache Beziehung zwischen einem Ganzen und seinen Teilen. Wird als schwach angesehen, da die Teile ohne das Ganze existieren können.

Beispiel:HeroInventory und Item.

A HeroInventorykann viele haben Itemsund Itemkann zu jedem gehören HeroInventory(z. B. Handelsgegenstände).

  • Komposition - Eine starke Beziehung, in der das Ganze und der Teil nicht ohne einander existieren können. Die Teile können nicht geteilt werden, da das Ganze von genau diesen Teilen abhängt.

Beispiel:Hero und HeroAttributes.

Dies sind die Attribute des Helden - Sie können seinen Besitzer nicht ändern.

Verallgemeinerung

Verallgemeinerung könnte das wichtigste Entwurfsprinzip sein - es ist der Prozess, gemeinsame Merkmale zu extrahieren und an einem Ort zu kombinieren. Wir alle kennen das Konzept von Funktionen und Klassenvererbung - beides ist eine Art Verallgemeinerung.

Ein Vergleich könnte die Dinge klären: Während die Abstraktion die Komplexität reduziert, indem unnötige Details ausgeblendet werden, reduziert die Generalisierung die Komplexität, indem mehrere Entitäten, die ähnliche Funktionen ausführen, durch ein einziges Konstrukt ersetzt werden.

In diesem Beispiel haben wir die Funktionalität unserer Common- Heround NPC Class-Klasse in einen gemeinsamen Vorfahren namens verallgemeinert Entity. Dies wird immer durch Vererbung erreicht.

Anstatt dass unsere NPCund HeroKlassen alle Methoden zweimal implementieren und gegen das DRY-Prinzip verstoßen, haben wir hier die Komplexität reduziert, indem wir ihre gemeinsame Funktionalität in eine Basisklasse verschoben haben.

Als Warnung - übertreiben Sie die Vererbung nicht. Viele erfahrene Leute empfehlen Ihnen, die Komposition der Vererbung vorzuziehen.

Vererbung wird oft von Amateurprogrammierern missbraucht, wahrscheinlich weil es eine der ersten OOP-Techniken ist, die sie aufgrund ihrer Einfachheit verstehen.

Komposition

Komposition ist das Prinzip, mehrere Objekte zu einem komplexeren zu kombinieren. Praktisch gesagt - es werden Instanzen von Objekten erstellt und deren Funktionalität verwendet, anstatt sie direkt zu erben.

Ein Objekt, das Komposition verwendet, kann als zusammengesetztes Objekt bezeichnet werden . Es ist wichtig, dass dieses Komposit einfacher ist als die Summe seiner Kollegen. Wenn wir mehrere Klassen zu einer kombinieren, möchten wir den Abstraktionsgrad erhöhen und das Objekt einfacher machen.

Die API des zusammengesetzten Objekts muss seine inneren Komponenten und die Interaktionen zwischen ihnen verbergen. Stellen Sie sich eine mechanische Uhr vor, sie hat drei Zeiger zum Anzeigen der Uhrzeit und einen Knopf zum Einstellen - enthält jedoch intern Dutzende beweglicher und voneinander abhängiger Teile.

Wie bereits erwähnt, wird die Komposition der Vererbung vorgezogen. Dies bedeutet, dass Sie sich bemühen sollten, allgemeine Funktionen in ein separates Objekt zu verschieben, das dann von Klassen verwendet wird, anstatt es in einer von Ihnen geerbten Basisklasse zu speichern.

Lassen Sie uns ein mögliches Problem mit der Übererbung von Funktionen veranschaulichen:

Wir haben unserem Spiel gerade Bewegung hinzugefügt.

Anstatt den Code zu duplizieren, haben wir, wie wir gelernt haben, die Generalisierung verwendet, um die move_rightund move_left-Funktionen in die EntityKlasse einzufügen.

Okay, was wäre, wenn wir Reittiere in das Spiel einführen wollten?

Reittiere müssten sich ebenfalls nach links und rechts bewegen, könnten aber nicht angreifen. Kommen Sie und denken Sie darüber nach - sie haben vielleicht nicht einmal Gesundheit!

Ich weiß, was Ihre Lösung ist:

Verschieben Sie die moveLogik einfach in eine separate MoveableEntityoder MoveableObjectKlasse, die nur diese Funktionalität hat. Die MountKlasse kann das dann erben.

Was machen wir dann, wenn wir Reittiere wollen, die gesund sind, aber nicht angreifen können? Mehr Aufteilung in Unterklassen? Ich hoffe, Sie können sehen, wie unsere Klassenhierarchie komplex wird, obwohl unsere Geschäftslogik noch recht einfach ist.

Ein etwas besserer Ansatz wäre, die Bewegungslogik in eine MovementKlasse (oder einen besseren Namen) zu abstrahieren und sie in den Klassen zu instanziieren, die sie möglicherweise benötigen. Dadurch wird die Funktionalität gut verpackt und für alle Arten von Objekten wiederverwendbar, die nicht darauf beschränkt sind Entity.

Hurra, Komposition!

Haftungsausschluss für kritisches Denken

Obwohl diese Gestaltungsprinzipien durch jahrzehntelange Erfahrung entstanden sind, ist es dennoch äußerst wichtig, dass Sie kritisch denken können, bevor Sie blind ein Prinzip auf Ihren Code anwenden.

Wie alle Dinge kann zu viel eine schlechte Sache sein. Manchmal können Prinzipien zu weit gehen, man kann zu klug mit ihnen umgehen und am Ende etwas finden, mit dem man tatsächlich schwerer arbeiten kann.

Als Ingenieur besteht Ihre Hauptaufgabe darin, den besten Ansatz für Ihre spezielle Situation kritisch zu bewerten und nicht blindlings willkürliche Regeln zu befolgen und anzuwenden.

Zusammenhalt, Kopplung und Trennung von Anliegen

Zusammenhalt

Kohäsion repräsentiert die Klarheit der Verantwortlichkeiten innerhalb eines Moduls oder mit anderen Worten - seine Komplexität.

Wenn Ihre Klasse eine Aufgabe und nichts anderes ausführt oder einen klaren Zweck hat - diese Klasse hat einen hohen Zusammenhalt . Auf der anderen Seite, wenn es etwas unklar ist, was es tut oder mehr als einen Zweck hat - es hat einen geringen Zusammenhalt .

Sie möchten, dass Ihre Klassen einen hohen Zusammenhalt haben. Sie sollten nur eine Verantwortung haben, und wenn Sie feststellen, dass sie mehr haben, ist es möglicherweise an der Zeit, diese aufzuteilen.

Kupplung

Die Kopplung erfasst die Komplexität zwischen dem Verbinden verschiedener Klassen. Sie möchten, dass Ihre Klassen so wenig und so einfach wie möglich mit anderen Klassen verbunden sind, damit Sie sie bei zukünftigen Ereignissen austauschen können (z. B. beim Ändern von Webframeworks). Das Ziel ist eine lose Kopplung .

In vielen Sprachen wird dies durch die häufige Verwendung von Schnittstellen erreicht - sie abstrahieren die spezifische Klasse, die die Logik verarbeitet, und stellen eine Art Adapterschicht dar, in die sich jede Klasse einstecken kann.

Trennung von Bedenken

Separation of Concerns (SoC) ist die Idee, dass ein Softwaresystem in Teile aufgeteilt werden muss, deren Funktionalität sich nicht überschneidet. Oder wie der Name schon sagt - Anliegen - Ein allgemeiner Begriff für alles, was eine Lösung für ein Problem bietet - muss an verschiedenen Stellen getrennt werden.

Eine Webseite ist ein gutes Beispiel dafür. Die drei Ebenen (Information, Präsentation und Verhalten) sind in drei Bereiche unterteilt (HTML, CSS bzw. JavaScript).

Wenn Sie sich das RPG- HeroBeispiel noch einmal ansehen, werden Sie feststellen, dass es am Anfang viele Bedenken hatte (Buffs anwenden, Angriffsschaden berechnen, Inventar verwalten, Gegenstände ausrüsten, Attribute verwalten). Wir trennten uns diese Bedenken durch Zersetzung in mehr zusammenhängende Klassen , die abstrakte und kapseln ihre Details. Unsere HeroKlasse fungiert jetzt als zusammengesetztes Objekt und ist viel einfacher als zuvor.

Auszahlen

Das Anwenden solcher Prinzipien könnte für einen so kleinen Code übermäßig kompliziert aussehen. Die Wahrheit ist, dass es ein Muss für jedes Softwareprojekt ist, das Sie in Zukunft entwickeln und warten möchten. Das Schreiben eines solchen Codes hat zu Beginn etwas Aufwand, zahlt sich aber auf lange Sicht mehrmals aus.

Diese Prinzipien stellen sicher, dass unser System mehr ist:

  • Erweiterbar : Hohe Kohäsion erleichtert die Implementierung neuer Module ohne Rücksicht auf nicht verwandte Funktionen. Geringe Kopplung bedeutet, dass ein neues Modul weniger Material zum Anschließen hat, daher ist es einfacher zu implementieren.
  • Wartbar : Eine niedrige Kopplung stellt sicher, dass eine Änderung in einem Modul im Allgemeinen keine Auswirkungen auf andere hat. Eine hohe Kohäsion stellt sicher, dass bei einer Änderung der Systemanforderungen so wenig Klassen wie möglich geändert werden müssen.
  • Wiederverwendbar : Hohe Kohäsion stellt sicher, dass die Funktionalität eines Moduls vollständig und klar definiert ist. Durch die geringe Kopplung ist das Modul weniger vom Rest des Systems abhängig, was die Wiederverwendung in anderer Software erleichtert.

Zusammenfassung

Wir haben zunächst einige grundlegende übergeordnete Objekttypen (Entität, Grenze und Steuerung) eingeführt.

Wir haben dann Schlüsselprinzipien bei der Strukturierung dieser Objekte gelernt (Abstraktion, Generalisierung, Zusammensetzung, Zerlegung und Verkapselung).

Im Anschluss haben wir zwei Softwarequalitätsmetriken (Kopplung und Kohäsion) eingeführt und uns über die Vorteile der Anwendung dieser Prinzipien informiert.

Ich hoffe, dieser Artikel bietet einen hilfreichen Überblick über einige Gestaltungsprinzipien. Wenn Sie sich in diesem Bereich weiterbilden möchten, sind hier einige Ressourcen, die ich empfehlen würde.

Weitere Lesungen

Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software - Das wohl einflussreichste Buch auf diesem Gebiet. In seinen Beispielen etwas veraltet (C ++ 98), aber die Muster und Ideen bleiben sehr relevant.

Wachsende objektorientierte Software, die von Tests geleitet wird - Ein großartiges Buch, das zeigt, wie die in diesem Artikel (und mehr) beschriebenen Prinzipien durch die Arbeit an einem Projekt praktisch angewendet werden können.

Effektives Software-Design - Ein erstklassiger Blog, der viel mehr als nur Einblicke in das Design bietet.

Spezialisierung auf Softwaredesign und Architektur - Eine großartige Reihe von 4 Videokursen, in denen Sie während der gesamten Anwendung eines Projekts, das alle vier Kurse umfasst, effektives Design lernen.

Wenn diese Übersicht für Sie informativ war, geben Sie ihr bitte die Anzahl der Klatschen, die Sie für verdient halten, damit mehr Menschen darüber stolpern und Wert daraus ziehen können.