JavaRush /Java-Blog /Random-DE /Regeln zum Schreiben von Code: von der Erstellung eines S...

Regeln zum Schreiben von Code: von der Erstellung eines Systems bis zur Arbeit mit Objekten

Veröffentlicht in der Gruppe Random-DE
Guten Tag zusammen, heute möchte ich mit Ihnen über das richtige Schreiben von Code sprechen. Als ich zum ersten Mal mit dem Programmieren begann, stand nirgendwo eindeutig geschrieben, dass man so schreiben kann, und wenn man so schreibt, werde ich dich finden und…. Infolgedessen hatte ich viele Fragen im Kopf: Wie schreibe ich richtig, welche Grundsätze sollten in diesem oder jenem Abschnitt des Programms befolgt werden usw. Regeln zum Schreiben von Code: von der Erstellung eines Systems bis zur Arbeit mit Objekten – 1Nun, nicht jeder möchte sofort in Bücher wie Clean Code eintauchen, da darin viel geschrieben steht, aber zunächst wenig klar ist. Und wenn Sie mit dem Lesen fertig sind, können Sie jegliche Lust am Programmieren vertreiben. Basierend auf dem oben Gesagten möchte ich Ihnen heute einen kleinen Leitfaden (eine Reihe kleiner Empfehlungen) zum Schreiben von Code auf höherer Ebene zur Verfügung stellen. In diesem Artikel gehen wir auf die grundlegenden Regeln und Konzepte ein, die sich auf die Erstellung eines Systems und die Arbeit mit Schnittstellen, Klassen und Objekten beziehen. Das Lesen dieses Materials wird nicht viel Zeit in Anspruch nehmen und hoffentlich keine Langeweile aufkommen lassen. Ich gehe von oben nach unten vor, also von der allgemeinen Struktur der Anwendung zu spezifischeren Details. Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 2

System

Die allgemein wünschenswerten Eigenschaften des Systems sind:
  • minimale Komplexität – zu komplizierte Projekte sollten vermieden werden. Die Hauptsache ist Einfachheit und Klarheit (am besten = einfach);
  • einfache Wartung – beim Erstellen einer Anwendung müssen Sie bedenken, dass diese unterstützt werden muss (auch wenn Sie es nicht sind), daher sollte der Code klar und offensichtlich sein;
  • schwache Kopplung ist die minimale Anzahl von Verbindungen zwischen verschiedenen Teilen des Programms (maximale Nutzung von OOP-Prinzipien);
  • Wiederverwendbarkeit – Entwurf eines Systems mit der Fähigkeit, seine Fragmente in anderen Anwendungen wiederzuverwenden;
  • Portabilität – das System muss leicht an eine andere Umgebung angepasst werden können;
  • einzelner Stil – Entwurf eines Systems in einem einzigen Stil in seinen verschiedenen Fragmenten;
  • Erweiterbarkeit (Skalierbarkeit) – Verbesserung des Systems, ohne seine Grundstruktur zu stören (wenn Sie ein Fragment hinzufügen oder ändern, sollte dies keine Auswirkungen auf den Rest haben).
Es ist praktisch unmöglich, eine Anwendung zu erstellen, die keine Änderungen erfordert, ohne Funktionalität hinzuzufügen. Wir müssen ständig neue Elemente einführen, damit unsere Idee mit der Zeit gehen kann. Und hier kommt die Skalierbarkeit ins Spiel . Skalierbarkeit bedeutet im Wesentlichen, die Anwendung zu erweitern, neue Funktionen hinzuzufügen und mit mehr Ressourcen (oder, anders gesagt, mit mehr Last) zu arbeiten. Das heißt, wir müssen einige Regeln einhalten, z. B. die Reduzierung der Systemkopplung durch Erhöhung der Modularität, damit es einfacher ist, neue Logik hinzuzufügen.

Phasen des Systementwurfs

  1. Softwaresystem – Entwerfen einer Anwendung in allgemeiner Form.
  2. Aufteilung in Subsysteme/Pakete – Definition logisch trennbarer Teile und Definition der Interaktionsregeln zwischen ihnen.
  3. Unterteilung von Subsystemen in Klassen – Unterteilung von Teilen des Systems in bestimmte Klassen und Schnittstellen sowie Definition der Interaktion zwischen ihnen.
  4. Die Unterteilung von Klassen in Methoden ist eine vollständige Definition der notwendigen Methoden für eine Klasse, basierend auf der Aufgabe dieser Klasse. Methodendesign – detaillierte Definition der Funktionalität einzelner Methoden.
Normalerweise sind normale Entwickler für das Design verantwortlich und der Anwendungsarchitekt ist für die oben beschriebenen Punkte verantwortlich.

Hauptprinzipien und Konzepte des Systemdesigns

Idiom für verzögerte Initialisierung Eine Anwendung verbringt keine Zeit damit, ein Objekt zu erstellen, bis es verwendet wird, was den Initialisierungsprozess beschleunigt und die Belastung des Garbage Collectors verringert. Allerdings sollte man damit nicht zu weit gehen, da dies zu einer Verletzung der Modularität führen kann. Es könnte sich lohnen, alle Entwurfsschritte auf einen bestimmten Teil zu verlagern, zum Beispiel main, oder in eine Klasse, die wie eine Fabrik funktioniert . Einer der Aspekte guten Codes ist das Fehlen häufig wiederholter Standardcodes. In der Regel wird solcher Code in einer separaten Klasse abgelegt, damit er zum richtigen Zeitpunkt aufgerufen werden kann. AOP Separat möchte ich die aspektorientierte Programmierung erwähnen . Dabei handelt es sich um Programmierung durch die Einführung einer End-to-End-Logik, d. h. sich wiederholender Code wird in Klassen – Aspekte – eingeteilt und aufgerufen, wenn bestimmte Bedingungen erreicht sind. Zum Beispiel beim Zugriff auf eine Methode mit einem bestimmten Namen oder beim Zugriff auf eine Variable eines bestimmten Typs. Manchmal können Aspekte verwirrend sein, da nicht sofort klar ist, woher der Code aufgerufen wird. Dennoch handelt es sich um eine sehr nützliche Funktionalität. Insbesondere beim Caching oder Logging: Wir fügen diese Funktionalität hinzu, ohne den regulären Klassen zusätzliche Logik hinzuzufügen. Mehr über OAP können Sie hier lesen . 4 Regeln für die Gestaltung einfacher Architektur nach Kent Beck
  1. Ausdruckskraft – die Notwendigkeit eines klar ausgedrückten Zwecks der Klasse, wird durch korrekte Benennung, geringe Größe und Einhaltung des Prinzips der Einzelverantwortung erreicht (wir werden weiter unten näher darauf eingehen).
  2. Ein Minimum an Klassen und Methoden – Ihr Wunsch, die Klassen so klein und unidirektional wie möglich aufzuteilen, kann zu weit gehen (Antimuster – Shotgunning). Dieses Prinzip erfordert, das System kompakt zu halten und nicht zu weit zu gehen und für jeden Niesen eine Klasse zu schaffen.
  3. Fehlende Duplizierung – zusätzlicher Code, der verwirrend ist, ist ein Zeichen für schlechtes Systemdesign und wird an einen separaten Ort verschoben.
  4. Ausführung aller Tests – ein System, das alle Tests bestanden hat, wird kontrolliert, da jede Änderung zum Scheitern der Tests führen kann, was uns zeigen kann, dass eine Änderung in der internen Logik der Methode auch zu einer Änderung des erwarteten Verhaltens geführt hat .
SOLID Bei der Gestaltung eines Systems lohnt es sich, die bekannten Prinzipien von SOLID zu berücksichtigen: S – Einzelverantwortung – das Prinzip der Einzelverantwortung; O – offen-geschlossen – Prinzip der Offenheit/Geschlossenheit; L – Liskov-Substitution – Barbara Liskovs Substitutionsprinzip; I – Schnittstellentrennung – das Prinzip der Schnittstellentrennung; D – Abhängigkeitsumkehr – Prinzip der Abhängigkeitsumkehr; Wir gehen nicht näher auf die einzelnen Prinzipien ein (dies würde den Rahmen dieses Artikels sprengen, aber Sie können hier mehr darüber erfahren).

Schnittstelle

Einer der vielleicht wichtigsten Schritte bei der Erstellung einer geeigneten Klasse ist die Erstellung einer geeigneten Schnittstelle, die eine gute Abstraktion darstellt, die die Implementierungsdetails der Klasse verbirgt, und gleichzeitig eine Gruppe von Methoden darstellt, die eindeutig miteinander konsistent sind . Werfen wir einen genaueren Blick auf eines der SOLID-Prinzipien – Schnittstellentrennung : Clients (Klassen) sollten keine unnötigen Methoden implementieren, die sie nicht verwenden. Das heißt, wenn es darum geht, Schnittstellen mit einer minimalen Anzahl von Methoden zu erstellen, die darauf abzielen, die einzige Aufgabe dieser Schnittstelle auszuführen (für mich ist sie der Einzelverantwortung sehr ähnlich ), ist es besser, ein paar kleinere zu erstellen statt einer aufgeblähten Schnittstelle. Glücklicherweise kann eine Klasse mehr als eine Schnittstelle implementieren, wie es bei der Vererbung der Fall ist. Sie müssen auch auf die korrekte Benennung von Schnittstellen achten: Der Name sollte ihre Aufgabe so genau wie möglich widerspiegeln. Und je kürzer es ist, desto weniger Verwirrung wird es natürlich verursachen. Auf der Schnittstellenebene werden normalerweise Kommentare zur Dokumentation geschrieben , die uns wiederum dabei helfen, detailliert zu beschreiben, was die Methode tun soll, welche Argumente sie akzeptiert und was sie zurückgibt.

Klasse

Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 3Schauen wir uns die interne Organisation des Unterrichts an. Oder besser gesagt, einige Ansichten und Regeln, die beim Erstellen von Klassen befolgt werden sollten. Normalerweise sollte eine Klasse mit einer Liste von Variablen beginnen, die in einer bestimmten Reihenfolge angeordnet sind:
  1. öffentliche statische Konstanten;
  2. private statische Konstanten;
  3. private Instanzvariablen.
Als nächstes folgen verschiedene Konstruktoren in der Reihenfolge von weniger zu mehr Argumenten. Ihnen folgen Methoden von offenerem Zugriff bis hin zu den geschlossensten: Ganz unten stehen in der Regel private Methoden, die die Implementierung einiger Funktionen verbergen, die wir einschränken möchten.

Klassengröße

Jetzt möchte ich über die Klassengröße sprechen. Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 4Erinnern wir uns an eines der Prinzipien von SOLID – Einzelverantwortung . Einzelverantwortung – das Prinzip der Einzelverantwortung. Es besagt, dass jedes Objekt nur ein Ziel (Verantwortung) hat und die Logik aller seiner Methoden darauf abzielt, dieses sicherzustellen. Das heißt, auf dieser Grundlage sollten wir große, aufgeblähte Klassen vermeiden (die ihrer Natur nach ein Antimuster sind – „göttliches Objekt“), und wenn wir viele Methoden unterschiedlicher, heterogener Logik in einer Klasse haben, müssen wir nachdenken über die Aufteilung in ein paar logische Teile (Klassen). Dies wiederum verbessert die Lesbarkeit des Codes, da wir nicht viel Zeit benötigen, um den Zweck einer Methode zu verstehen, wenn wir den ungefähren Zweck einer bestimmten Klasse kennen. Sie müssen auch den Klassennamen im Auge behalten : Er sollte die darin enthaltene Logik widerspiegeln. Nehmen wir an, wenn wir eine Klasse haben, deren Name mehr als 20 Wörter enthält, müssen wir über eine Umgestaltung nachdenken. Jede Klasse mit Selbstachtung sollte nicht so viele interne Variablen haben. Tatsächlich arbeitet jede Methode mit einer oder mehreren davon, was zu einer stärkeren Kopplung innerhalb der Klasse führt (was genau das sein sollte, da die Klasse ein einziges Ganzes sein sollte). Infolgedessen führt die Erhöhung der Kohärenz einer Klasse zu einer Verringerung dieser als solcher, und natürlich erhöht sich unsere Anzahl an Klassen. Für manche ist das ärgerlich; sie müssen mehr zum Unterricht gehen, um zu sehen, wie eine bestimmte große Aufgabe funktioniert. Unter anderem ist jede Klasse ein kleines Modul, das minimal mit den anderen verbunden sein sollte. Diese Isolation reduziert die Anzahl der Änderungen, die wir vornehmen müssen, wenn wir einer Klasse zusätzliche Logik hinzufügen.

Objekte

Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 5

Verkapselung

Hier werden wir zunächst über eines der Prinzipien der OOP- Kapselung sprechen . Das Verbergen der Implementierung läuft also nicht darauf hinaus, eine Methodenschicht zwischen Variablen zu erstellen (den Zugriff durch einzelne Methoden, Getter und Setter gedankenlos einzuschränken, was nicht gut ist, da der Sinn der Kapselung verloren geht). Das Ausblenden des Zugriffs zielt darauf ab, Abstraktionen zu bilden, das heißt, die Klasse stellt gemeinsame konkrete Methoden bereit, mit denen wir mit unseren Daten arbeiten. Aber der Benutzer muss nicht genau wissen, wie wir mit diesen Daten arbeiten – es funktioniert, und das ist in Ordnung.

Gesetz von Demeter

Sie können auch das Demeter-Gesetz in Betracht ziehen: Es handelt sich um eine kleine Reihe von Regeln, die dabei helfen, die Komplexität auf Klassen- und Methodenebene zu verwalten. Nehmen wir also an, wir haben ein Objekt Carund es hat eine Methode - move(Object arg1, Object arg2). Nach dem Gesetz von Demeter ist diese Methode auf den Aufruf beschränkt:
  • Methoden des Objekts selbst Car(mit anderen Worten dieses);
  • Methoden von Objekten, die in erstellt wurden move;
  • Methoden übergebener Objekte als Argumente - arg1, arg2;
  • Methoden interner Objekte Car(dasselbe).
Mit anderen Worten, das Demeter-Gesetz ist so etwas wie eine Kinderregel – mit Freunden kann man reden, mit Fremden aber nicht .

Datenstruktur

Eine Datenstruktur ist eine Sammlung zusammengehöriger Elemente. Betrachtet man ein Objekt als Datenstruktur, handelt es sich um eine Menge von Datenelementen, die von Methoden verarbeitet werden, deren Existenz implizit impliziert ist. Das heißt, es handelt sich um ein Objekt, dessen Zweck darin besteht, gespeicherte Daten zu speichern und zu verarbeiten (verarbeiten). Der Hauptunterschied zu einem regulären Objekt besteht darin, dass ein Objekt eine Reihe von Methoden ist, die mit Datenelementen arbeiten, deren Existenz impliziert ist. Verstehst du? In einem regulären Objekt sind die Methoden der Hauptaspekt, und interne Variablen zielen auf deren korrekte Funktion ab, aber in einer Datenstruktur ist es umgekehrt: Methoden unterstützen und helfen bei der Arbeit mit gespeicherten Elementen, die hier die wichtigsten sind. Eine Art von Datenstruktur ist Data Transfer Object (DTO) . Dies ist eine Klasse mit öffentlichen Variablen und keinen Methoden (oder nur Lese-/Schreibmethoden), die Daten beim Arbeiten mit Datenbanken, beim Parsen von Nachrichten von Sockets usw. übergeben. Typischerweise werden Daten in solchen Objekten nicht lange gespeichert und sind es auch fast sofort in die Entität umgewandelt, mit der unsere Anwendung arbeitet. Eine Entität wiederum ist ebenfalls eine Datenstruktur, ihr Zweck besteht jedoch darin, an der Geschäftslogik auf verschiedenen Ebenen der Anwendung teilzunehmen, während das DTO darin besteht, Daten zur/von der Anwendung zu transportieren. Beispiel-DTO:
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Alles scheint klar, aber hier erfahren wir etwas über die Existenz von Hybriden. Hybride sind Objekte, die Methoden zur Verarbeitung wichtiger Logik sowie zum Speichern interner Elemente und Zugriffsmethoden (get/set) auf diese enthalten. Solche Objekte sind chaotisch und erschweren das Hinzufügen neuer Methoden. Sie sollten sie nicht verwenden, da nicht klar ist, wozu sie gedacht sind – zum Speichern von Elementen oder zum Ausführen irgendeiner Logik. Über mögliche Objekttypen können Sie hier nachlesen .

Prinzipien zum Erstellen von Variablen

Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 6Denken wir ein wenig über Variablen nach, oder besser gesagt, darüber, nach welchen Prinzipien sie erstellt werden könnten:
  1. Idealerweise sollten Sie eine Variable unmittelbar vor der Verwendung deklarieren und initialisieren (anstatt sie zu erstellen und zu vergessen).
  2. Deklarieren Sie Variablen nach Möglichkeit als endgültig, um zu verhindern, dass sich ihr Wert nach der Initialisierung ändert.
  3. Vergessen Sie nicht die Zählervariablen (normalerweise verwenden wir sie in einer Art Schleife for, das heißt, wir dürfen nicht vergessen, sie zurückzusetzen, da dies sonst unsere gesamte Logik zerstören kann).
  4. Sie sollten versuchen, Variablen im Konstruktor zu initialisieren.
  5. new SomeObject()Wenn Sie zwischen der Verwendung eines Objekts mit oder ohne Referenz ( ) wählen können , wählen Sie ohne ( ), da dieses Objekt nach seiner Verwendung bei der nächsten Garbage Collection gelöscht wird und keine Ressourcen verschwendet.
  6. Machen Sie die Lebensdauer von Variablen so kurz wie möglich (die Distanz zwischen der Erstellung einer Variablen und dem letzten Zugriff).
  7. Initialisieren Sie Variablen, die in einer Schleife verwendet werden, unmittelbar vor der Schleife und nicht am Anfang der Methode, die die Schleife enthält.
  8. Beginnen Sie immer mit dem engsten Bereich und erweitern Sie ihn nur bei Bedarf (Sie sollten versuchen, die Variable so lokal wie möglich zu gestalten).
  9. Verwenden Sie jede Variable nur für einen Zweck.
  10. Vermeiden Sie Variablen mit versteckter Bedeutung (die Variable ist zwischen zwei Aufgaben hin- und hergerissen, was bedeutet, dass ihr Typ nicht zur Lösung einer davon geeignet ist).
Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 7

Methoden

Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 8Kommen wir direkt zur Implementierung unserer Logik, nämlich zu den Methoden.
  1. Die erste Regel ist Kompaktheit. Idealerweise sollte eine Methode 20 Zeilen nicht überschreiten. Wenn also beispielsweise eine öffentliche Methode erheblich „anwächst“, müssen Sie darüber nachdenken, die getrennte Logik in private Methoden zu verschieben.

  2. Die zweite Regel besagt, dass Blöcke in Befehlen if, usw. nicht stark verschachtelt sein sollten: elseDieswhile verringert die Lesbarkeit des Codes erheblich. Idealerweise sollte die Verschachtelung nicht mehr als zwei Blöcke betragen {}.

    Außerdem empfiehlt es sich, den Code in diesen Blöcken kompakt und einfach zu gestalten.

  3. Die dritte Regel besagt, dass eine Methode nur eine Operation ausführen darf. Das heißt, wenn eine Methode eine komplexe, vielfältige Logik ausführt, unterteilen wir sie in Untermethoden. Dadurch ist die Methode selbst eine Fassade, deren Zweck darin besteht, alle anderen Operationen in der richtigen Reihenfolge aufzurufen.

    Was aber, wenn die Operation zu einfach erscheint, um eine separate Methode zu erstellen? Ja, manchmal scheint es, als würde man Spatzen aus einer Kanone schießen, aber kleine Methoden bieten eine Reihe von Vorteilen:

    • einfacheres Lesen des Codes;
    • Methoden werden im Laufe der Entwicklung tendenziell komplexer, und wenn die Methode ursprünglich einfach war, wird es etwas einfacher sein, ihre Funktionalität zu komplizieren;
    • Implementierungsdetails verbergen;
    • Erleichterung der Wiederverwendung von Code;
    • höhere Codezuverlässigkeit.
  4. Als Abwärtsregel gilt , dass der Code von oben nach unten gelesen werden sollte: Je niedriger, desto größer die Logiktiefe und umgekehrt, je höher, desto abstrakter die Methoden. Beispielsweise sind Schalterbefehle ziemlich unkompakt und unerwünscht. Wenn Sie jedoch nicht auf die Verwendung eines Schalters verzichten können, sollten Sie versuchen, ihn so weit wie möglich nach unten zu verschieben, in die Methoden der untersten Ebene.

  5. Methodenargumente – wie viele sind ideal? Im Idealfall gibt es überhaupt keine)) Aber passiert das wirklich? Allerdings sollte man versuchen, möglichst wenige davon zu haben, denn je weniger es sind, desto einfacher lässt sich diese Methode anwenden und desto einfacher lässt sich sie testen. Versuchen Sie im Zweifelsfall, alle Szenarien für die Verwendung einer Methode mit einer großen Anzahl von Eingabeargumenten zu erraten.

  6. Separat möchte ich Methoden hervorheben, die ein boolesches Flag als Eingabeargument haben , da dies natürlich impliziert, dass diese Methode mehr als eine Operation implementiert (wenn wahr, dann eine, falsch – eine andere). Wie ich oben geschrieben habe, ist das nicht gut und sollte nach Möglichkeit vermieden werden.

  7. Wenn eine Methode eine große Anzahl eingehender Argumente hat (der Extremwert ist 7, aber Sie sollten nach 2-3 darüber nachdenken), müssen Sie einige Argumente in einem separaten Objekt gruppieren.

  8. Wenn mehrere ähnliche (überladene) Methoden vorhanden sind , müssen ähnliche Parameter in derselben Reihenfolge übergeben werden: Dies verbessert die Lesbarkeit und Benutzerfreundlichkeit.

  9. Wenn Sie Parameter an eine Methode übergeben, müssen Sie sicherstellen, dass sie alle verwendet werden. Wofür dient sonst das Argument? Schneiden Sie es aus der Schnittstelle heraus und fertig.

  10. try/catchEs sieht von Natur aus nicht sehr schön aus, daher wäre es ein guter Schritt, es in eine separate Zwischenmethode (Methode zur Behandlung von Ausnahmen) zu verschieben:

    public void exceptionHandling(SomeObject obj) {
        try {
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Ich habe oben über das Wiederholen von Code gesprochen, aber ich füge es hier hinzu: Wenn wir mehrere Methoden mit sich wiederholenden Teilen des Codes haben, müssen wir ihn in eine separate Methode verschieben, was die Kompaktheit sowohl der Methode als auch des Codes erhöht Klasse. Und vergessen Sie nicht die richtigen Namen. Die Details zur korrekten Benennung von Klassen, Schnittstellen, Methoden und Variablen erzähle ich Ihnen im nächsten Teil des Artikels. Und das ist alles für uns heute. Regeln zum Schreiben von Code: vom Erstellen eines Systems bis zum Arbeiten mit Objekten - 9Kodexregeln: Die Macht der richtigen Benennung, guter und schlechter Kommentare
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION