Klassendesign: Was ist gut?



Gepostet von Denis Tsyplakov , Solution Architect, DataArt

Im Laufe der Jahre habe ich festgestellt, dass Programmierer von Zeit zu Zeit dieselben Fehler wiederholen. Leider helfen Bücher über theoretische Aspekte der Entwicklung nicht, sie zu vermeiden: Bücher haben normalerweise keine konkreten praktischen Ratschläge. Und ich denke sogar warum ...

Die erste Empfehlung, die mir zum Beispiel beim Protokollieren oder beim Klassendesign einfällt, ist sehr einfach: "Machen Sie keinen völligen Schwachsinn." Aber die Erfahrung zeigt, dass es definitiv nicht genug ist. Nur die Gestaltung von Klassen in diesem Fall ist ein gutes Beispiel - ein ewiger Kopfschmerz, der sich aus der Tatsache ergibt, dass jeder dieses Thema auf seine eigene Weise betrachtet. Aus diesem Grund habe ich beschlossen, grundlegende Tipps in einem Artikel zusammenzufassen, um eine Reihe typischer Probleme zu vermeiden und vor allem Ihre Kollegen vor ihnen zu bewahren. Wenn Ihnen einige Prinzipien banal erscheinen (weil sie wirklich banal sind!) - nun, dann haben sie sich bereits in Ihrem Subkortex niedergelassen, und Ihr Team kann beglückwünscht werden.

Ich werde eine Reservierung machen, in der Tat werden wir uns nur der Einfachheit halber auf den Unterricht konzentrieren. Fast das Gleiche gilt für die Funktionen oder andere Bausteine ​​der Anwendung.
Wenn die Anwendung funktioniert und die Aufgabe ausführt, ist ihr Design gut. Oder nicht? Hängt von der objektiven Funktion der Anwendung ab; Was für eine mobile Anwendung gut geeignet ist, die einmal auf der Messe gezeigt werden muss, ist möglicherweise nicht für die Handelsplattform geeignet, die eine Bank seit Jahren entwickelt. Bis zu einem gewissen Grad kann die Antwort auf diese Frage als SOLID- Prinzip bezeichnet werden, ist aber zu allgemein - ich möchte einige spezifischere Anweisungen, auf die in einem Gespräch mit Kollegen Bezug genommen werden kann.

Zielanwendung


Da es keine universelle Antwort geben kann, schlage ich vor, den Anwendungsbereich einzuschränken. Nehmen wir an, wir schreiben eine Standard-Geschäftsanwendung, die Anforderungen über HTTP oder eine andere Schnittstelle akzeptiert, eine Logik darüber implementiert und dann entweder eine Anforderung an den nächsten Dienst in der Kette sendet oder die empfangenen Daten irgendwo speichert. Nehmen wir der Einfachheit halber an, dass wir das Spring IoC Framework verwenden, da es mittlerweile weit verbreitet ist und der Rest der Frameworks ihm ziemlich ähnlich ist. Was können wir über eine solche Anwendung sagen?

  • Die Zeit, die der Prozessor für die Verarbeitung einer Anforderung benötigt, ist wichtig, aber nicht kritisch - eine Erhöhung des Wetters um 0,1% wird dies nicht tun.
  • Es stehen uns keine Terabyte Speicher zur Verfügung. Wenn die Anwendung jedoch zusätzliche 50 bis 100 KByte benötigt, ist dies keine Katastrophe.
  • Je kürzer die Startzeit, desto besser. Es gibt jedoch keinen grundlegenden Unterschied zwischen 6 Sekunden und 5,9 Sekunden.

Optimierungskriterien


Was ist uns in diesem Fall wichtig?

Der Projektcode wird wahrscheinlich mehrere, möglicherweise mehr als zehn Jahre lang vom Unternehmen verwendet.

Der Code wird zu unterschiedlichen Zeiten von mehreren Entwicklern geändert, die sich nicht auskennen.
Es ist möglich, dass Entwickler in einigen Jahren die neue LibXYZ-Bibliothek oder das FrABC-Framework verwenden möchten.

Irgendwann kann ein Teil des Codes oder das gesamte Projekt mit der Codebasis eines anderen Projekts zusammengeführt werden.

Inmitten von Managern wird allgemein angenommen, dass solche Probleme mithilfe von Dokumentation gelöst werden. Die Dokumentation ist natürlich gut und nützlich, da sie zu Beginn der Arbeit an einem Projekt so großartig ist, dass fünf offene Tickets an Ihnen hängen. Der Projektmanager fragt Sie, wie Sie Fortschritte gemacht haben, und Sie müssen etwa 150 lesen (und sich daran erinnern) Textseiten, die weit entfernt von brillanten Schriftstellern geschrieben wurden. Natürlich hatten Sie ein paar Tage oder sogar ein paar Wochen Zeit, um in das Projekt einzusteigen, aber wenn Sie einfache Arithmetik verwenden, einerseits 5.000.000 Byte Code, andererseits beispielsweise 50 Arbeitsstunden. Es stellt sich heraus, dass im Durchschnitt 100 KB Code pro Stunde injiziert werden mussten. Und hier hängt alles sehr stark von der Qualität des Codes ab. Wenn es sauber ist: einfach zu montieren, gut strukturiert und vorhersehbar, scheint das Eingießen in ein Projekt ein merklich weniger schmerzhafter Prozess zu sein. Nicht die letzte Rolle spielt dabei das Klassendesign. Nicht der letzte.

Was wir vom Klassendesign wollen


Aus all dem können viele interessante Schlussfolgerungen über die allgemeine Architektur, den Technologie-Stack, den Entwicklungsprozess usw. gezogen werden. Aber von Anfang an haben wir uns entschlossen, über Klassendesign zu sprechen. Lassen Sie uns sehen, welche nützlichen Dinge wir aus dem lernen können, was zuvor dazu gesagt wurde.

  • Ich möchte, dass ein Entwickler, der mit dem Anwendungscode nicht gründlich vertraut ist, verstehen kann, was diese Klasse tut, wenn er sich eine Klasse ansieht. Und umgekehrt - wenn ich eine funktionale oder nicht funktionale Anforderung betrachte, könnte ich schnell erraten, wo sich die Anwendung in den dafür verantwortlichen Klassen befindet. Nun, es ist wünschenswert, dass die Implementierung der Anforderungen nicht über die gesamte Anwendung verteilt ist, sondern sich auf eine Klasse oder eine kompakte Gruppe von Klassen konzentriert. Lassen Sie mich anhand eines Beispiels erklären, welche Art von Antimuster ich meine. Angenommen, wir müssen überprüfen, ob 10 Anforderungen eines bestimmten Typs nur von Benutzern ausgeführt werden können, die mehr als 20 Punkte in ihrem Konto haben (unabhängig davon, was dies bedeutet). Eine schlechte Möglichkeit, eine solche Anforderung zu implementieren, besteht darin, zu Beginn jeder Anforderung eine Prüfung einzufügen. Dann wird die Logik auf 10 Methoden in verschiedenen Controllern verteilt. Eine gute Möglichkeit besteht darin, einen Filter oder WebRequestInterceptor zu erstellen und alles an einem Ort zu überprüfen.
  • Ich möchte, dass die Änderungen in einer Klasse, die sich nicht auf den Klassenvertrag auswirken, sich nicht auf andere Klassen auswirken (zumindest seien wir realistisch!). Mit anderen Worten, ich möchte die Implementierung eines Klassenvertrags zusammenfassen.
  • Ich möchte, dass es möglich ist, beim Ändern des Klassenvertrags durch die Anrufkette zu gehen und Verwendungen zu finden und die Klassen zu finden, die von dieser Änderung betroffen sind. Das heißt, ich möchte, dass Klassen keine indirekten Abhängigkeiten haben.
  • Wenn möglich, würde ich gerne sehen, dass Anforderungsverarbeitungsprozesse, die aus mehreren einstufigen Schritten bestehen, nicht durch den Code mehrerer Klassen verschmiert werden, sondern auf derselben Ebene beschrieben werden. Es ist sehr gut, wenn der Code, der einen solchen Verarbeitungsprozess beschreibt, auf einen Bildschirm innerhalb einer Methode mit einem eindeutigen Namen passt. Zum Beispiel müssen wir alle Wörter in einer Zeile finden, für jedes Wort einen Drittanbieter anrufen, eine Beschreibung des Wortes abrufen, die Beschreibung formatieren und die Ergebnisse in der Datenbank speichern. Dies ist eine Abfolge von Aktionen in 4 Schritten. Es ist sehr praktisch, den Code zu verstehen und seine Logik zu ändern, wenn es eine Methode gibt, bei der diese Schritte nacheinander ausgeführt werden.
  • Ich möchte wirklich, dass die gleichen Dinge im Code auf die gleiche Weise implementiert werden. Wenn wir beispielsweise sofort vom Controller aus auf die Datenbank zugreifen, ist es besser, dies überall zu tun (obwohl ich ein solches Design nicht als bewährte Methode bezeichnen würde). Und wenn wir bereits die Ebenen der Services und Repositorys eingegeben haben, ist es besser, die Datenbank nicht direkt vom Controller aus zu kontaktieren.
  • Ich möchte, dass die Anzahl der Klassen / Schnittstellen, die nicht direkt für funktionale und nicht funktionale Anforderungen verantwortlich sind, nicht sehr groß ist. Die Arbeit mit einem Projekt, in dem es für jede Klasse zwei Schnittstellen mit Logik gibt, eine komplexe Hierarchie der Vererbung von fünf Klassen, einer Klassenfabrik und einer abstrakten Klassenfabrik, ist ziemlich schwierig.

Praktische Empfehlungen


Nachdem wir Wünsche formuliert haben, können wir spezifische Schritte skizzieren, mit denen wir unsere Ziele erreichen können.

Statische Methoden


Zum Aufwärmen beginne ich mit einer relativ einfachen Regel. Sie sollten keine statischen Methoden erstellen, es sei denn, sie sind für den Betrieb einer der verwendeten Bibliotheken erforderlich (z. B. müssen Sie einen Serializer für einen Datentyp erstellen).

Grundsätzlich ist die Verwendung statischer Methoden nichts Falsches. Wenn das Verhalten einer Methode vollständig von ihren Parametern abhängt, warum nicht wirklich statisch machen? Sie müssen jedoch berücksichtigen, dass wir Spring IoC verwenden, mit dem die Komponenten unserer Anwendung gebunden werden. Spring IoC befasst sich mit den Konzepten der Bohnen und ihrem Umfang. Dieser Ansatz kann mit statischen Methoden gemischt werden, die in Klassen gruppiert sind. Es kann jedoch sehr schwierig sein, eine solche Anwendung zu verstehen und sogar etwas darin zu ändern (wenn Sie beispielsweise einen globalen Parameter an eine Methode oder Klasse übergeben müssen).

Gleichzeitig bieten statische Methoden im Vergleich zu IoC-Bins einen sehr unbedeutenden Vorteil in der Geschwindigkeit des Methodenaufrufs. Und damit enden vielleicht die Vorteile.

Wenn Sie keine Geschäftsfunktion erstellen, die eine große Anzahl ultraschneller Aufrufe zwischen verschiedenen Klassen erfordert, ist es besser, keine statischen Methoden zu verwenden.

Hier kann der Leser fragen: "Aber was ist mit den Klassen StringUtils und IOUtils?" In der Java-Welt hat sich in der Tat eine Tradition entwickelt: Hilfsfunktionen für die Arbeit mit Strings und Input-Output-Streams in statische Methoden zu integrieren und unter dem Dach von SomethingUtils-Klassen zu sammeln. Aber diese Tradition scheint mir eher moosig. Wenn Sie es befolgen, wird natürlich kein großer Schaden erwartet - alle Java-Programmierer sind daran gewöhnt. Aber eine solche rituelle Handlung hat keinen Sinn. Einerseits, warum nicht die StringUtils-Bean machen, andererseits, wenn Sie die Bean und alle Hilfsmethoden nicht statisch machen, lassen Sie uns bereits die statischen Dachklassen StockTradingUtils und BlockChainUtils machen. Es ist schwierig, Logik in statische Methoden einzufügen, einen Rand zu zeichnen und anzuhalten. Ich rate Ihnen, nicht zu beginnen.

Vergessen Sie nicht, dass mit Java 11 viele der Hilfsmethoden, die die Entwickler jahrzehntelang von Projekt zu Projekt geführt hatten, entweder Teil der Standardbibliothek wurden oder beispielsweise in Google Guava zu Bibliotheken zusammengeführt wurden.

Atomic, kompakter Klassenvertrag


Es gibt eine einfache Regel, die für die Entwicklung eines Softwaresystems gilt. Wenn Sie sich eine Klasse ansehen, sollten Sie in der Lage sein, schnell und kompakt zu erklären, was diese Klasse tut, ohne auf lange Ausgrabungen zurückgreifen zu müssen. Wenn es unmöglich ist, die Erklärung in einen Absatz einzufügen (es ist jedoch nicht notwendig, in einem Satz ausgedrückt), könnte es sich lohnen, über diese Klasse nachzudenken und sie in mehrere Atomklassen zu unterteilen. Zum Beispiel die Klasse "Sucht nach Textdateien auf einer Festplatte und zählt die Anzahl der Buchstaben Z in jeder von ihnen" - ein guter Kandidat für die Zerlegung "sucht auf einer Festplatte" + "zählt die Anzahl der Buchstaben".

Machen Sie andererseits keine zu kleinen Klassen, von denen jede für eine Aktion ausgelegt ist. Aber wie groß sollte die Klasse dann sein? Die Grundregeln lauten wie folgt:

  • Idealerweise, wenn der Klassenvertrag mit der Beschreibung der Geschäftsfunktion (oder Unterfunktion, je nachdem, wie die Anforderungen angeordnet sind) übereinstimmt. Dies ist nicht immer möglich: Wenn der Versuch, diese Regel einzuhalten, zur Erstellung eines umständlichen, nicht offensichtlichen Codes führt, ist es besser, die Klasse in kleinere Teile zu unterteilen.
  • Eine gute Messgröße für die Beurteilung der Qualität eines Klassenvertrags ist das Verhältnis seiner intrinsischen Komplexität zur Komplexität des Vertrags. Zum Beispiel kann ein sehr guter (wenn auch fantastischer) Klassenvertrag folgendermaßen aussehen: „Die Klasse hat eine Methode, die am Eingang eine Zeile mit einer Beschreibung des Themas in russischer Sprache erhält und als Ergebnis eine Qualitätsgeschichte oder sogar eine Geschichte zu einem bestimmten Thema verfasst.“ Hier ist der Vertrag einfach und allgemein verständlich. Die Implementierung ist äußerst komplex, aber die Komplexität ist in der Klasse verborgen.

Warum ist diese Regel wichtig?

  • Erstens ist die Fähigkeit, sich selbst klar zu erklären, was jede der Klassen tut, immer nützlich. Leider kann dies bei weitem nicht jeder Projektentwickler tun. Man kann oft so etwas hören wie: „Nun, das ist so ein Wrapper über die Path-Klasse, die wir irgendwie gemacht haben und manchmal anstelle von Path verwenden. Sie hat auch eine Methode, die alle File.separator-Pfade verdoppeln kann. Wir benötigen diese Methode, um Berichte in der Cloud zu speichern, und aus irgendeinem Grund ist sie in der Path-Klasse gelandet. "
  • Das menschliche Gehirn kann gleichzeitig mit nicht mehr als fünf bis zehn Objekten arbeiten. Die meisten Menschen haben nicht mehr als sieben. Wenn ein Entwickler mit mehr als sieben Objekten arbeiten muss, um ein Problem zu lösen, wird er entweder etwas übersehen oder gezwungen sein, mehrere Objekte unter einem logischen „Dach“ zu packen. Und wenn Sie es noch packen müssen, warum tun Sie es nicht sofort bewusst und geben Sie diesem Regenschirm einen aussagekräftigen Namen und einen klaren Vertrag.

Wie kann man überprüfen, ob alles granular genug ist? Bitten Sie einen Kollegen, Ihnen 5 (fünf) Minuten Zeit zu geben. Nehmen Sie den Teil der Anwendung, an dem Sie gerade arbeiten. Erklären Sie für jede Klasse einem Kollegen, was genau diese Klasse tut. Wenn Sie nicht in 5 Minuten passen oder ein Kollege nicht verstehen kann, warum diese oder jene Klasse benötigt wird, sollten Sie vielleicht etwas ändern. Nun, oder nicht, um das Experiment mit einem anderen Kollegen zu ändern und erneut durchzuführen.

Klassenabhängigkeiten


Angenommen, wir müssen verknüpfte Textabschnitte mit einer Länge von mehr als 100 Byte für eine in ein ZIP-Archiv gepackte PDF-Datei auswählen und in der Datenbank speichern. Ein beliebtes Antimuster sieht in solchen Fällen folgendermaßen aus:

  • Es gibt eine Klasse, die ein ZIP-Archiv öffnet, darin nach einer PDF-Datei sucht und diese als InputStream zurückgibt.
  • Diese Klasse enthält einen Link zu einer Klasse, die in PDF-Textabschnitten sucht.
  • Die Klasse, die mit PDF arbeitet, hat wiederum einen Link zu der Klasse, die Daten in der Datenbank speichert.

Einerseits sieht alles logisch aus: empfangene Daten, die direkt als nächste Klasse in der Kette bezeichnet werden. Gleichzeitig mischen sich die Verträge der Klasse an der Spitze der Kette in die Verträge und Abhängigkeiten aller Klassen, die in der Kette dahinter stehen. Es ist viel korrekter, diese Klassen atomar und unabhängig voneinander zu machen und eine weitere Klasse zu erstellen, die die Verarbeitungslogik tatsächlich implementiert, indem diese drei Klassen miteinander verbunden werden.

Wie man es nicht macht:



Was ist hier falsch? Die Klasse, die mit ZIP-Dateien arbeitet, übergibt Daten an die Klasse, die das PDF verarbeitet, und die Daten wiederum an die Klasse, die mit der Datenbank arbeitet. Dies bedeutet, dass die Klasse, die mit ZIP arbeitet, aus irgendeinem Grund von den Klassen abhängt, die mit der Datenbank arbeiten. Außerdem ist die Verarbeitungslogik auf drei Klassen verteilt, und um sie zu verstehen, müssen wir alle drei Klassen durchgehen. Was ist, wenn Sie Textabschnitte aus PDF benötigen, um über einen REST-Aufruf an einen Drittanbieter weitergeleitet zu werden? Sie müssen die Klasse ändern, die mit PDF funktioniert, und sie auch mit REST zeichnen.

Wie es geht:



Hier haben wir vier Klassen:

  • Eine Klasse, die nur mit einem ZIP-Archiv funktioniert und eine Liste von PDF-Dateien zurückgibt (man kann argumentieren - das Zurückgeben von Dateien ist schlecht - sie sind groß und brechen die Anwendung. Aber lassen Sie uns das Wort "return" in diesem Fall im weitesten Sinne lesen. Zum Beispiel gibt es Stream von InputStream zurück )
  • Die zweite Klasse ist für die Arbeit mit PDF verantwortlich.
  • Die dritte Klasse weiß nichts und kann nichts anderes tun, als Absätze in der Datenbank zu speichern.
  • Und die vierte Klasse, die buchstäblich aus mehreren Codezeilen besteht, enthält die gesamte Geschäftslogik, die auf einen Bildschirm passt.

Ich betone noch einmal, dass es 2019 in Java mindestens zwei gute (und etwas weniger) gibt
gute) Möglichkeiten, keine Dateien zu übertragen und eine vollständige Liste aller Absätze als Objekte im Speicher. Das:

  1. Java Stream API
  2. Rückrufe Das heißt, eine Klasse mit einer Geschäftsfunktion überträgt Daten nicht direkt, sondern sagt ZIP Extractor: Hier ist ein Rückruf für Sie, suchen Sie nach PDF-Dateien in einer ZIP-Datei, erstellen Sie einen InputStream für jede Datei und rufen Sie den übertragenen Rückruf damit auf.

Implizites Verhalten


Wenn wir nicht versuchen, ein völlig neues Problem zu lösen, das zuvor von niemandem gelöst wurde, sondern etwas tun, das andere Entwickler bereits mehrere hundert (oder hunderttausende) Mal getan haben, haben alle Teammitglieder einige Erwartungen hinsichtlich der zyklomatischen Komplexität und Ressourcenintensität der Lösung . Wenn wir beispielsweise alle Wörter, die mit dem Buchstaben z beginnen, in einer Datei finden müssen, ist dies ein sequentielles, einmaliges Lesen der Datei in Blöcken von der Festplatte. Das heißt, wenn Sie sich auf https://gist.github.com/jboner/2841832 konzentrieren, dauert ein solcher Vorgang mehrere Mikrosekunden pro 1 MB, möglicherweise mehrere zehn oder sogar hundert Mikrosekunden, je nach Programmierumgebung und Systemlast überhaupt keine Sekunde. Es werden mehrere zehn Kilobyte Speicher benötigt (wir lassen die Frage weg, was wir mit den Ergebnissen machen, dies ist das Anliegen einer anderen Klasse), und der Code wird höchstwahrscheinlich etwa einen Bildschirm belegen. Gleichzeitig erwarten wir, dass keine anderen Systemressourcen verwendet werden. Das heißt, der Code erstellt keine Threads, schreibt keine Daten auf die Festplatte, sendet keine Pakete über das Netzwerk und speichert keine Daten in der Datenbank.

Dies ist die übliche Erwartung eines Methodenaufrufs:

zWordFinder.findZWords(inputStream). ... 

Wenn der Code Ihrer Klasse diese Anforderungen aus einem vernünftigen Grund nicht erfüllt, z. B. um ein Wort in z und nicht in z zu klassifizieren, müssen Sie jedes Mal die REST-Methode aufrufen (ich weiß nicht, warum dies erforderlich sein könnte, aber stellen wir uns das vor). Es ist notwendig, sehr sorgfältig in den Klassenvertrag zu schreiben, und es ist sehr gut, wenn der Name der Methode anzeigt, dass die Methode irgendwo ausgeführt wird, um sie zu konsultieren.

Wenn Sie keinen vernünftigen Grund für implizites Verhalten haben, schreiben Sie die Klasse neu.

Wie kann man die Erwartungen an die Komplexität und Ressourcenintensität der Methode verstehen? Sie müssen auf eine dieser einfachen Möglichkeiten zurückgreifen:

  1. Gewinnen Sie mit Erfahrung einen ziemlich weiten Horizont.
  2. Fragen Sie einen Kollegen - das geht immer.
  3. Sprechen Sie vor Beginn der Entwicklung mit den Teammitgliedern über den Implementierungsplan.
  4. Um sich die Frage zu stellen: "Aber verwende ich bei dieser Methode nicht zu viele redundante Ressourcen?" Das reicht normalerweise aus.

Sie müssen sich auch nicht auf die Optimierung einlassen - das Einsparen von 100 Byte bei Verwendung durch die Klasse 100.000 ist für die meisten Anwendungen nicht sehr sinnvoll.

Diese Regel öffnet ein Fenster in die Welt des Overengineering und verbirgt Antworten auf Fragen wie „Warum sollten Sie nicht einen Monat damit verbringen, 10 Byte Speicher in einer Anwendung zu sparen, die 10 GB benötigt, um zu funktionieren?“. Aber ich werde dieses Thema hier nicht weiterentwickeln. Sie verdient einen separaten Artikel.

Implizite Methodennamen


In der Java-Programmierung gibt es derzeit mehrere implizite Konventionen bezüglich Klassennamen und deren Verhalten. Es gibt nicht viele von ihnen, aber es ist besser, sie nicht zu brechen. Ich werde versuchen, diejenigen aufzulisten, die mir in den Sinn kommen:

  • Konstruktor - Erstellt eine Instanz der Klasse, kann einige ziemlich verzweigte Datenstrukturen erstellen, funktioniert jedoch nicht mit der Datenbank, schreibt nicht auf die Festplatte und sendet keine Daten über das Netzwerk (ich sage, der integrierte Logger kann dies alles, aber dies ist eine andere Geschichte in jedem Fall liegt es am Gewissen des Protokollierungskonfigurators).
  • Getter - getSomething () - gibt eine Art Speicherstruktur aus den Tiefen des Objekts zurück. Auch hier schreibt es nicht auf die Festplatte, führt keine komplexen Berechnungen durch, sendet keine Daten über das Netzwerk, funktioniert nicht mit der Datenbank (außer wenn dies ein verzögertes ORM-Feld ist und dies nur einer der Gründe ist, warum verzögerte Felder mit großer Sorgfalt verwendet werden sollten). .
  • Setter - setSomething (Something Something) - legt den Wert der Datenstruktur fest, führt keine komplexen Berechnungen durch, sendet keine Daten über das Netzwerk und funktioniert nicht mit der Datenbank. In der Regel wird vom Setter nicht erwartet, dass er das Verhalten oder den Verbrauch wesentlicher Rechenressourcen impliziert.
  • equals () und hashcode () - es wird überhaupt nichts erwartet, außer einfachen Berechnungen und Vergleichen in einer Menge, die linear von der Größe der Datenstruktur abhängt. Das heißt, wenn wir Hashcode für ein Objekt aus drei primitiven Feldern aufrufen, wird erwartet, dass N * 3 einfache Rechenanweisungen ausgeführt werden.
  • toSomething () - Es wird auch erwartet, dass diese Methode einen Datentyp in einen anderen konvertiert und für die Konvertierung nur eine Speichergröße benötigt, die mit der Größe der Strukturen und der Prozessorzeit vergleichbar ist, die linear von der Größe der Strukturen abhängt. Hierbei ist zu beachten, dass die Typkonvertierung nicht immer linear erfolgen kann. Beispielsweise kann die Konvertierung eines Pixelbilds in ein SVG-Format eine nicht triviale Aktion sein. In diesem Fall ist es jedoch besser, die Methode anders zu benennen. Zum Beispiel sieht der Name computeAndConvertToSVG ​​() etwas umständlich aus, deutet jedoch sofort darauf hin, dass einige wichtige Berechnungen im Inneren stattfinden.

Ich werde ein Beispiel geben. Ich habe kürzlich ein Anwendungsaudit durchgeführt. Durch die Logik der Arbeit weiß ich, dass die Anwendung irgendwo im Code die RabbitMQ-Warteschlange abonniert. Ich gehe den Code durch - ich kann diesen Ort nicht finden. Ich bin direkt auf der Suche nach einem Appell an Kaninchen, ich fange an zu klettern, ich gehe zu dem Ort im Geschäftsfluss, an dem das Abonnement tatsächlich stattfindet - ich fange an zu schwören. Wie es im Code aussieht:

  1. Die Methode service.getQueueListener (tickerName) wird aufgerufen - das zurückgegebene Ergebnis wird ignoriert. Dies könnte eine Warnung sein, aber ein solcher Code, bei dem die Ergebnisse der Methode ignoriert werden, ist nicht der einzige in der Anwendung.
  2. Im Inneren wird tickerName auf null geprüft und eine andere Methode getQueueListenerByName (tickerName) aufgerufen.
  3. Darin wird eine Instanz der QueueListener-Klasse mit dem Namen des Tickers aus dem Hash entnommen (falls dies nicht der Fall ist, wird sie erstellt), und die Methode getSubscription () wird darauf aufgerufen.
  4. Und jetzt, innerhalb der Methode getSubscription (), findet das Abonnement tatsächlich statt. Und es passiert irgendwo in der Mitte einer Methode von der Größe von drei Bildschirmen.

Ich werde es Ihnen ehrlich sagen - ohne die gesamte Kette zu durchlaufen und ohne ein aufmerksames Dutzend Code-Bildschirme zu lesen, war es unrealistisch zu erraten, wo sich das Abonnement befindet. Wenn die Methode subscribeToQueueByTicker (tickerName) heißen würde, würde ich viel Zeit sparen.

Dienstprogrammklassen


Es gibt ein wundervolles Buch Design Patterns: Elements of Reusable Object-Oriented Software (1994), das oft als GOF (Gang of Four, nach Anzahl der Autoren) bezeichnet wird. Der Vorteil dieses Buches besteht hauptsächlich darin, dass Entwickler aus verschiedenen Ländern eine einzige Sprache zur Beschreibung von Klassenentwurfsmustern erhalten. Anstelle von "Die Klasse existiert garantiert nur in einer Instanz und hat einen statischen Zugriffspunkt" können wir jetzt "Singleton" sagen. Das gleiche Buch verursachte zerbrechlichen Köpfen spürbaren Schaden. Dieser Schaden wird durch ein Zitat aus einem der Foren gut beschrieben. "Kollegen, ich muss einen Webshop erstellen und mir sagen, mit welchen Vorlagen ich beginnen soll." Mit anderen Worten, einige Programmierer neigen dazu, Entwurfsmuster zu missbrauchen, und wo immer Sie mit einer Klasse umgehen können, erstellen sie manchmal fünf oder sechs gleichzeitig - nur für den Fall, "für mehr Flexibilität".

Wie kann man entscheiden, ob man eine abstrakte Klassenfabrik (oder ein anderes Muster, das komplizierter als eine Schnittstelle ist) benötigt oder nicht? Es gibt einige einfache Überlegungen:

  1. Wenn Sie im Frühjahr eine Bewerbung schreiben, werden 99% der Zeit nicht benötigt. Der Frühling bietet Ihnen übergeordnete Bausteine, verwenden Sie diese. Das Maximum, das Sie möglicherweise nützlich finden, ist eine abstrakte Klasse.
  2. Wenn Punkt 1 Ihnen immer noch keine klare Antwort gab, denken Sie daran, dass jede Vorlage +1000 Punkte für die Komplexität der Anwendung beträgt. Analysieren Sie sorgfältig, ob die Vorteile der Verwendung der Vorlage den Schaden überwiegen. Denken Sie an eine Metapher und denken Sie daran, dass jedes Arzneimittel nicht nur heilt, sondern auch leicht schadet. Trinken Sie nicht alle Pillen auf einmal.


Ein gutes Beispiel dafür, wie Sie es nicht brauchen, finden Sie hier .

Fazit


Zusammenfassend möchte ich darauf hinweisen, dass ich die grundlegendsten Empfehlungen aufgelistet habe. Ich würde sie nicht einmal in Form eines Artikels ausmachen - sie sind so offensichtlich. Aber im letzten Jahr bin ich zu oft auf Anwendungen gestoßen, bei denen viele dieser Empfehlungen verletzt wurden. Schreiben wir einfachen Code, der leicht zu lesen und zu warten ist.

Source: https://habr.com/ru/post/de443756/


All Articles