Architektentagebuch – Sprechen Sie über die praktischen Fähigkeiten, die für die Entwicklung beherrscht werden müssen | JD Cloud Technology Team

I. Einleitung

Obwohl sich die Softwareentwicklung schon immer dem Streben nach effizienten, lesbaren und leicht zu wartenden Funktionen verschrieben hat, sind diese Funktionen wie ein unmögliches Dreieck, das miteinander verflochten ist und ein Auf und Ab aufweist. Ebenso wie Sprachen auf niedriger Ebene (wie Assembler und C-Sprache) eine effiziente Laufleistung aufrechterhalten können, weisen sie jedoch Mängel und Nachteile in Bezug auf Lesbarkeit und Wartbarkeit auf, während Sprachen auf hoher Ebene (wie Java und Python) Mängel und Nachteile aufweisen in Bezug auf Lesbarkeit und Wartbarkeit. Es schneidet in Bezug auf die Leistung gut ab, ist jedoch in Bezug auf die Ausführungseffizienz unzureichend.

Die Vorteile der Sprachökologie auszunutzen und ihre Mängel auszugleichen, war schon immer eine Entwicklungsrichtung von Programmiersprachen.

Verschiedene Programmiersprachen haben unterschiedliche Funktionen und Protokolle. Nehmen wir die Sprache JAVA als Beispiel, um die Wissenspunkte und praktischen Fähigkeiten aufzuschlüsseln, die im Entwicklungsprozess leicht übersehen werden, aber beherrscht werden müssen.

2 Grundlagen

Im Jahr 1999 scheiterte die Mars-Mission der NASA: Bei dieser Mission verwendete die Flugsystemsoftware des Mars Climate Explorer die metrische Einheit Newton zur Berechnung der Triebwerksleistung, während der Richtungskorrekturbetrag und der vom Bodenpersonal eingegebene Schub als Detektorparameter die britische Einheit verwendeten Die Kraft eines Pfunds führte dazu, dass der Detektor in einer falschen Höhe in die Atmosphäre eindrang und schließlich zerfiel.

Dies war ein Unfall, der durch den Konflikt zwischen internationalen Standards (Rinder) und Lokalisierung (Pfund) verursacht wurde. Dies führt zu dem Thema, dass Programme auf Wartbarkeit achten müssen. Da die Softwareproduktion häufig die Zusammenarbeit mehrerer Personen erfordert, ist Wartbarkeit ein wichtiger Bestandteil des kollaborativen Konsenses. Was diesen Aspekt betrifft, sind die beiden Aspekte, die den Menschen am leichtesten einfallen, Benennung und Anmerkung. Lassen Sie uns diese im Folgenden besprechen.

2.1 Über die Benennung

Je nach Lesegewohnheiten muss die variable Benennungsmethode des Programms das Platzproblem zwischen Wörtern überwinden, um verschiedene Wörter miteinander zu verbinden und letztendlich den Effekt zu erzielen, ein neues „Wort“ zu erstellen, das leicht zu lesen ist. Zu den gängigen Benennungsmethoden gehören die folgenden:

  • Schlangenfall : Wird auch als Unterstreichungsbenennung bezeichnet. Verwenden Sie Unterstriche und Kleinbuchstaben, zum Beispiel: my_system;
  • Kamelfall (Kamelfall): Groß- und Kleinschreibung wird anhand des ersten Buchstabens des Wortes beachtet und kann in Groß- und Kleinkamelfall unterteilt werden, z. B.: MySystem, mySystem;
  • Ungarische Nomenklatur (HN-Fall): Attribut + Typ + Beschreibung, wie zum Beispiel: nLength, g_cch , hwnd;
  • Pascal-Fall (Pascal-Fall): Alle Anfangsbuchstaben werden großgeschrieben, was der Nomenklatur des großen Kamelfalls entspricht, wie zum Beispiel: MySystem;
  • Wirbelsäulenfall : Verwenden Sie einen Bindestrich, zum Beispiel: my-system;
  • Großbuchstaben : gemischte Groß-/Kleinschreibung, keine prägnanten Regeln, wie zum Beispiel: mySYSTEM, MYSystem;

Geordnet nach Zielgruppengröße und Beliebtheit sind die Kamel- und die Schlangennomenklatur beliebter, da sie Vorteile in Bezug auf Lesbarkeit und Schreibfreundlichkeit bieten.

2.1.1 Namenswörterbuch

Die Bedeutung erkennen, nachdem man den Namen gesehen hat: Eine gute Namensgebung ist eine Art Kommentar.

Es wird empfohlen, dass sich F&E-Studenten die Namen gängiger Geschäftsszenarien in der Branche merken. Natürlich hat sie bereits jemand für uns zusammengefasst, daher werde ich hier nicht zu sehr auf die Erklärung eingehen. Hier ein Auszug als Referenz:

Benennung der Verwaltungsklasse : Bootstrap, Starter, Prozessor, Manager, Halter, Fabrik, Anbieter, Registrar, Engine, Service, Aufgabe

Benennung der Propagierungsklasse : Kontext, Propagator

Benennung der Callback-Klasse : Handler, Callback, Trigger, Listener, Aware

Benennung der Überwachungsklassen : Metrik, Schätzer, Akkumulator, Tracker

Benennung der Speicherverwaltungsklasse : Allocator, Chunk, Arena, Pool

Benennung der Filtererkennungsklasse : Pipeline, Kette, Filter, Interceptor, Evaluator, Detektor

Benennung von Strukturklassen : Cache, Puffer, Composite, Wrapper, Option, Param, Attribut, Tupel, Aggregator, Iterator, Batch, Limiter

Gängige Benennung von Entwurfsmustern : Strategie, Adapter, Aktion, Befehl, Ereignis, Delegat, Builder, Vorlage, Proxy

Benennung von Parsing-Klassen : Konverter, Resolver, Parser, Customizer, Formatter

Benennung der Netzwerkklassen : Paket, Encoder, Decoder, Codec, Anfrage, Antwort

CRUD-Benennung : Controller, Dienst, Repository

Benennung von Hilfsklassen : Util, Helper

Andere Klassenbenennung : Modus, Typ, Aufrufer, Aufruf, Initialisierer, Zukunft, Versprechen, Selektor, Reporter, Konstanten, Zugriffsmechanismus, Generator

2.1.2 Benennungspraktiken

Was sind die allgemeinen Namensregeln für Projekte ? Verschiedene Sprachen können unterschiedliche Gewohnheiten haben. Nehmen Sie als Beispiel die Namenskonvention für Kamelfälle in der Java-Sprache:

1. Der Projektname muss ausschließlich aus Kleinbuchstaben bestehen.
2. Paketnamen sollten ausschließlich aus Kleinbuchstaben bestehen.
3. Der erste Buchstabe des Klassennamens wird großgeschrieben, und die ersten Buchstaben der verbleibenden konstituierenden Wörter werden der Reihe nach großgeschrieben.
4. Der erste Buchstabe von Variablennamen und Methodennamen muss klein geschrieben sein. Wenn der Name aus mehreren Wörtern besteht, muss der erste Buchstabe jedes Wortes mit Ausnahme des ersten Buchstabens großgeschrieben werden.
5. Konstantennamen sollten ausschließlich in Großbuchstaben geschrieben werden.

Die Spezifikation ist relativ abstrakt. Schauen wir uns zunächst an, was sind die schlechten Namen?

1. Variablenname mit integrierter Verschleierungsfunktion: String zhrmghg = „Extreme Abkürzung“;
2. Bedeutungsloser universeller Variablenname: String a,b,c="Love so und so type";
3. Pinyin-Variablenname mit langer Zeichenfolge: String HuaBuHua = „Archäologischer Typ“;
4. Gemischte Verwendung verschiedener Symbole: String $my_first_name_ = „Es ist zu schwer zu merken“;
5. Verwirrende Groß- und Kleinschreibung, Zahlen und Abkürzungen: String waitRPCResponse1 = „Extrem fehleranfällig“;

Zusätzlich zu den Standardspezifikationen gibt es auch einige tatsächliche Fälle, die uns während des eigentlichen Entwicklungsprozesses stören.

1. Sollten Sie beim Definieren einer Mitgliedsvariablen einen Wrapper-Typ oder einen Basisdatentyp verwenden?

Die Standardwerte von Wrapper-Klassen und Basisdatentypen sind unterschiedlich. Ersteres ist null, und die Standardwerte des letzteren sind je nach Typ unterschiedlich. Aus Sicht der Datengenauigkeit kann der Nullwert der Wrapper-Klasse zusätzliche Informationen darstellen und sie so sicherer machen. Beispielsweise kann dadurch das NPE-Risiko vermieden werden, das durch grundlegende Arten des automatischen Unboxings verursacht wird, und das Risiko einer abnormalen Geschäftslogikverarbeitung. Daher müssen Mitgliedsvariablen Wrapper-Datentypen verwenden und Basisdatentypen werden im Kontext lokaler Variablen verwendet.

2. Warum wird nicht empfohlen, dass Mitgliedsvariablen vom booleschen Typ mit is beginnen?

Es gibt tatsächlich klare Vorschriften für die Definition von Getter/Setter-Methoden in Java Beans. Gemäß der JavaBeans(TM)-Spezifikation muss sein Setter/Getter wie folgt definiert werden, wenn es sich um einen gewöhnlichen Parameter mit dem Namen propertyName handelt:

    public <PropertyType> get<PropertyName>();
    public void set<PropertyName>(<PropertyType> p)

Die boolesche Typvariable propertyName folgt jedoch anderen Benennungsprinzipien:

    public boolean is<PropertyName>();
    public void set<PropertyName>(boolean p)

Aufgrund der Unterschiede in der Art und Weise, wie verschiedene RPC-Frameworks und Objektserialisierungstools Variablen vom Typ Boolean verarbeiten, kann es leicht zu Problemen bei der Codeportabilität kommen. Es gibt Kompatibilitätsprobleme zwischen den gängigsten JSON-Serialisierungsbibliotheken Jackson und Gson. Erstere durchläuft alle Getter-Methoden in der Klasse durch Reflektion und erhält die Eigenschaften des Objekts durch Abfangen des Methodennamens, während letztere direkt Reflektion verwendet und die Eigenschaften darin iteriert Klasse. Um die Auswirkungen dieses Unterschieds auf das Geschäft zu vermeiden, wird empfohlen, nicht alle Mitgliedsvariablen mit zu beginnen, um unvorhersehbare Serialisierungsergebnisse zu verhindern.

3. Sehen Sie, welche Nebenwirkungen die Groß-/Kleinschreibung verursachen kann?

Die JAVA-Sprache selbst unterscheidet zwischen Groß- und Kleinschreibung, aber beim Betrieb von Dateien mit Dateipfaden und Dateinamen wird bei den Dateinamen und -pfaden hier nicht zwischen Groß- und Kleinschreibung unterschieden. Dies liegt daran, dass das Dateisystem nicht zwischen Groß- und Kleinschreibung unterscheidet. Ein typisches Szenario ist, dass Git nicht aktualisiert werden kann, wenn wir eine Codeverwaltungsplattform wie Git verwenden und den Dateinamen in Großbuchstaben im Paketpfad in Kleinbuchstaben ändern. Um unnötige Probleme zu vermeiden, wird empfohlen, im Paket Wörter in Kleinbuchstaben zu verwenden Pfad. Mehrere Wörter werden durch eine Pfadhierarchie definiert.

4. Werden Klassen in verschiedenen JAR-Paketen miteinander in Konflikt geraten?
1. Eine Kategorie besteht darin, dass es mehrere verschiedene Versionen desselben JAR-Pakets gibt. Die Anwendung wählt die falsche Version aus, was dazu führt, dass der JVM die erforderlichen Klassen nicht laden kann oder die falsche Version der Klasse lädt. (Mit Hilfe von Maven-Verwaltungstools lässt sich das Problem relativ einfach lösen.)
2. Der andere Typ besteht darin, dass Klassen mit demselben Klassenpfad in verschiedenen JAR-Paketen erscheinen und dieselbe Klasse in verschiedenen abhängigen JARs erscheint. Aufgrund der Reihenfolge, in der die JARs geladen werden, lädt die JVM die falsche Version der Klasse; (schwieriger zu lösen)

Hier konzentrieren wir uns auf die zweite Situation. Diese Situation tritt wahrscheinlich auf, wenn das System geteilt und neu aufgebaut wird. Das Originalprojekt wird kopiert und dann gelöscht, was dazu führt, dass einige Tools oder Aufzählungsklassen dieselben Pfade und Namen wie die Originale haben. , Wenn sich Drittaufrufer gleichzeitig auf diese beiden Systeme verlassen, kann es leicht passieren, dass künftigen Iterationen eine Falle gestellt wird. Um solche Probleme zu vermeiden, müssen Sie einen eindeutigen Paketpfad für das System erstellen.

Ergänzung: Wenn Sie auf Bibliotheken von Drittanbietern angewiesen sind und ein Klassenkonflikt vorliegt, können Sie den JAR-Paketkonflikt lösen, indem Sie die Bibliothek von Drittanbietern jarjar.jar einführen und den Paketnamen einer der widersprüchlichen JAR-Dateien ändern.

5. Wie kann ein Kompromiss zwischen der Lesbarkeit der Variablenbenennung und den belegten Ressourcen (Speicher, Bandbreite) erzielt werden?

Sie können Objektserialisierungstools als Durchbruch verwenden, indem Sie die gängige Json-Serialisierungsmethode (Jackson) als Beispiel nehmen:

public class SkuKey implements Serializable {
    @JsonProperty(value = "sn")
    @ApiModelProperty(name = "stationNo", value = " 门店编号", required = true)
    private Long stationNo;
    @JsonProperty(value = "si")
    @ApiModelProperty(name = "skuId", value = " 商品编号", required = true)
    private Long skuId;
    // 省略get/set方法
}

Die Funktion der Annotation @JsonProperty besteht darin, die allgemeinen Eigenschaften in JavaBean während der Serialisierung in den angegebenen neuen Namen umzubenennen. Diese Implementierung hat keine Auswirkungen auf die Geschäftsimplementierung. Der ursprüngliche Benennungsvorgang hat weiterhin Vorrang. Er wird nur wirksam, wenn externer RPC Serialisierung und Deserialisierung erfordert. Auf diese Weise wird der Konflikt zwischen Lesbarkeit und Ressourcenbelegung besser gelöst.

6. Sollten wir Klassenobjekte oder Map-Container verwenden, um Eingabe- und Ausgabeparameter für externe Dienste bereitzustellen?

Aus Sicht der Flexibilität sind Kartencontainer stabiler und flexibler. Aus Sicht der Stabilität und Lesbarkeit ist der Kartencontainer eine Blackbox. Sie wissen nicht, was sich darin befindet. Für die Zusammenarbeit benötigen Sie eine detaillierte Hilfsdokumentation. Da die Aktionen zur Dokumentenverwaltung häufig vom technischen Code, dem Mechanismus, getrennt sind Daher ist es schwierig, die Richtigkeit und Aktualität der Informationen sicherzustellen. Daher wird weiterhin empfohlen, Klassenstrukturobjekte zur Verwaltung der eingehenden und ausgehenden Parameterstrukturen zu verwenden.

2.2 Über Kommentare

Kommentare stellen ein wichtiges Kommunikationsmittel zwischen Programmierern und Lesern dar. Sie stellen Erläuterungen und Erläuterungen zum Code dar. Gute Kommentare können die Lesbarkeit der Software verbessern und den Aufwand für die Wartung der Software senken.

2.2.1 Gute Kommentare

Hierarchisch : Kommentieren und erklären Sie jedes mit seinem eigenen Schwerpunkt entsprechend unterschiedlicher Granularitäten wie System, Paket, Klasse, Methode, Codeblock, Codezeile usw.
1. Systemhinweise : Die makroskopischen Funktionen und die Architektur werden über die Datei README.md realisiert.
2. Paketanmerkungen : Die Verantwortungsgrenzen des Moduls werden durch die Paketinfodatei widergespiegelt. Darüber hinaus unterstützt die Datei auch die Deklaration benutzerfreundlicher Klassen und Paketkonstanten und bietet Komfort für auf dem Paket markierte Anmerkungen (Annotation).
3. Klassenanmerkungen : spiegeln hauptsächlich funktionale Verantwortlichkeiten, Versionsunterstützung, Autorenangabe, Anwendungsbeispiele und andere verwandte Informationen wider;
4. Methodenkommentare : Achten Sie auf Eingabeparameter, Ausgabeparameter, Anweisungen zur Ausnahmebehandlung, Beispiele für Verwendungsszenarien und andere verwandte Inhalte.
5. Codeblöcke und Codezeilenkommentare : spiegeln hauptsächlich logische Absichten, Warnungen zum Schließen von Gruben, Planungs-TODO, Verstärkung von Bedenken und andere Details wider;
Es gibt Spezifikationen : Guter Code ist besser als viele Kommentare. Aus dem gleichen Grund sagen wir oft: „Konvention ist wichtiger als Konfiguration“. Es ist eine gute Standardmethode, Bibliotheken von Drittanbietern wie Swagger zu verwenden, um Anmerkungen, also Schnittstellendokumente, zu implementieren.

2.2.2 Schlechte Kommentare

Um Kommentare genau und klar zum Ausdruck zu bringen, ist die Pflege von Kommentaren mit erheblichen Wartungskosten verbunden. Je mehr Kommentare, desto detaillierter, desto besser. Hier sind einige schlechte Anmerkungsszenarien, um das Verständnis zu erleichtern:

1. Redundanz : Wenn der Leser die Bedeutung einer Funktion leicht lesen kann, sind die Kommentare redundant;
2. Falsche Formel : Wenn die Anmerkung unklar oder sogar mehrdeutig ist, ist es besser, sie nicht zu schreiben.
3. Signaturstil : Kommentare wie „Hinzufügen von liuhuiqing 05.08.2023“ verfallen leicht und sind nicht vertrauenswürdig (es gibt keine Garantie dafür, dass jeder jedes Mal auf diese Weise kommentiert), und seine Funktionen können vollständig von Git gesteuert werden Code-Management-Tools zu erreichen;
4. Kommentare in Langform : Codeblöcke werden mit langen Kommentaren vermischt, was nicht nur das Lesen des Codes beeinträchtigt, sondern auch die Wartung erschwert.
5. Nicht-lokale Kommentare : Kommentare sollten am nächsten an der Code-Implementierung platziert werden. Beispielsweise werden die aufgerufenen Methodenkommentare von der Methode selbst verwaltet, und der Aufrufer muss keine detaillierte Erklärung der Methode bereitstellen.
6. Auskommentierter Code : Unbrauchbarer Code sollte gelöscht und nicht auskommentiert werden. Historische Aufzeichnungen werden von Codeverwaltungstools wie Git verwaltet.

2.3 Über Layering

Der Hauptzweck des mehrschichtigen Systemdesigns besteht darin, die Komplexität des Systems durch die Trennung von Belangen zu reduzieren und gleichzeitig die Wiederverwendbarkeit zu verbessern und die Wartungskosten zu senken. Wenn Sie also das Konzept der Schichtung verstehen, wird die Wartbarkeit des Systems weitgehend ein Grundgerüst haben.

2.3.1 Systemschichtung

Bevor die ISO (International Standardization Organization) 1981 das siebenschichtige Netzwerkkommunikationsmodell (Open System Interconnection Reference Model, OSI/RM) formulierte, gab es viele Architekturen in Computernetzwerken, darunter die SNA (System Network Architecture) Structure von IBM und die DECs Die bekannteste digitale Netzwerkarchitektur ist DNA (Digital Network Architecture).

Zuvor basierten die von jedem Hersteller vorgeschlagenen unterschiedlichen Standards auf der eigenen Ausrüstung. Benutzer konnten bei der Produktauswahl nur Produkte desselben Unternehmens verwenden, da verschiedene Unternehmen unterschiedliche Standards haben und möglicherweise unterschiedliche Arbeitsmethoden haben. Dies kann zu Inkompatibilitäten führen zwischen Netzwerkprodukten unterschiedlicher Hersteller. Wenn die Produkte desselben Unternehmens die Bedürfnisse der Benutzer erfüllen können, hängt es davon ab, welches Unternehmen stärker, stärker und benutzerfreundlicher ist. Natürlich werden Benutzer nichts sagen. Das Problem ist, dass ein Unternehmen nicht richtig ist. Alle Produkte zeichnen sich aus. Dies wird sowohl für Hersteller als auch für Anwender zu schmerzhaftem Leid führen. In Analogie zu den aktuellen Ladeschnittstellenprotokollen für Mobiltelefone (Micro-USB-Schnittstelle, Typ-C-Schnittstelle, Lightning-Schnittstelle) können Sie die Bedeutung des Standards tiefgreifend verstehen, wenn Sie immer verschiedene Ladekabel zur Hand haben.

2.3.2 Software-Skalierbarkeit

Software-Skalierbarkeit bezieht sich auf die Fähigkeit eines Softwaresystems, seine ursprüngliche Leistung beizubehalten und sich zu erweitern, um mehr Aufgaben zu unterstützen, wenn es Lastdruck ausgesetzt ist.

Skalierbarkeit kann zwei Aspekte haben: vertikale Skalierbarkeit und horizontale Skalierbarkeit. Vertikale Skalierbarkeit verbessert den Durchsatz des Systems durch Hinzufügen von Ressourcen in derselben Geschäftseinheit, z. B. durch Erhöhen der Anzahl der Server-CPUs, Erhöhen des Serverspeichers usw. Horizontale Skalierbarkeit wird durch das Hinzufügen mehrerer Geschäftsbereichsressourcen erreicht, sodass alle Geschäftsbereiche logisch wie eine Einheit agieren. Zu dieser Methode gehören beispielsweise das verteilte EJB-Komponentenmodell, das Microservice-Komponentenmodell usw.

Softwaresysteme müssen beim Entwurf ein effektives Skalierbarkeitsdesign berücksichtigen, um eine ausreichende Leistungsunterstützung bei Lastdruck sicherzustellen.

Aus Sicht der Skalierbarkeit fällt die Systemschichtung eher in die Kategorie der horizontalen Skalierbarkeit. Bei der Entwicklung von J2EE-Systemen verwenden wir im Allgemeinen eine Schichtarchitektur, die im Allgemeinen in Präsentationsschicht, Geschäftsschicht und Persistenzschicht unterteilt ist. Nachdem die Schichtung eingeführt wurde, wird der Verarbeitungsaufwand für jedes Unternehmen zunehmen, da die Kommunikation zwischen den Schichten zusätzlichen Overhead verursacht.

Da die Schichtung zusätzlichen Aufwand mit sich bringt, warum müssen wir sie schichten?

Dies liegt daran, dass es eine Obergrenze für die Verbesserung der Softwareleistung und des Durchsatzes gibt, indem man sich einfach auf die vertikale Skalierung von Heap-Hardwareressourcen verlässt. Mit zunehmender Systemgröße werden auch die Kosten für die vertikale Skalierung sehr hoch. Wenn die Schichtung übernommen wird, entsteht zwar ein Kommunikationsaufwand zwischen den Schichten, dies wirkt sich jedoch positiv auf die horizontale Skalierbarkeit jeder Schicht aus, und jede Schicht kann unabhängig skaliert werden, ohne andere Schichten zu beeinträchtigen. Das heißt, wenn das System eine größere Zugriffsmenge bewältigen muss, können wir den Systemdurchsatz erhöhen, indem wir mehrere Geschäftsbereichsressourcen hinzufügen.

2.4 Zusammenfassung

In diesem Kapitel geht es hauptsächlich um die Notwendigkeit, im Entwicklungsprozess unter den Aspekten Lesbarkeit und Wartbarkeit einen einheitlichen Konsens über Benennung und Annotation zu erreichen. Zusätzlich zum Konsens muss die Isolierung von Bedenken auch auf Entwurfsebene erfolgen. Dazu gehören die Aufteilung der Systemverantwortung, die Aufteilung der Modulfunktionen, die Konvergenz der Klassenfähigkeiten und die Beziehung zwischen den Entitätsstrukturen – alles muss gut sein geplant.

Drei Übungskapitel

Lassen Sie uns aus diesen klassischen Praxisfällen einige wichtige Qualitätsindikatoren wie Programmskalierbarkeit, Wartbarkeit, Sicherheit und Leistung lernen.

3.1 Klassendefinition

3.1.1 Konstantendefinition

Eine Konstante ist ein fester Wert, der sich während der Programmausführung nicht ändert. Sie können Aufzählungen (Enum) oder Klassen (Class) verwenden, um Konstanten zu definieren.

Wenn Sie eine Gruppe verwandter Konstanten definieren müssen, ist die Verwendung einer Aufzählung besser geeignet. Aufzählungen haben größere Vorteile in Bezug auf Sicherheit und Bedienbarkeit (Unterstützung der Traversierung und Funktionsdefinition).

public enum Color {
 RED, GREEN, BLUE;
}

Wenn Sie nur eine oder mehrere schreibgeschützte Konstanten definieren müssen, ist die Verwendung von Klassenkonstanten prägnanter und bequemer.

public class MyClass {
 public static final int MAX_VALUE = 100;
}

3.1.2 Werkzeuge

Tool-Klassen enthalten normalerweise allgemeine öffentliche Methoden in einem bestimmten nicht geschäftlichen Bereich. Sie erfordern keine unterstützenden Mitgliedsvariablen und werden nur als Tool-Methoden verwendet. Daher ist es am besten, es zu einer statischen Methode zu machen, die keine Instanziierung erfordert und nur zum Abrufen der Definition der Methode und zum Aufrufen verwendet werden kann.

Der Grund, warum die Werkzeugklasse nicht instanziiert wird, besteht darin, Speicherplatz zu sparen, da die Werkzeugklasse statische Methoden bereitstellt, die über die Klasse aufgerufen werden können, und keine Notwendigkeit besteht, das Werkzeugklassenobjekt zu instanziieren.

public abstract class ObjectHelper {
    public static boolean isEmpty(String str) {
        return str == null || str.length() == 0;
    }
}

Um Einschränkungen zu erreichen, die keine instanziierten Objekte erfordern, ist es am besten, beim Definieren der Klasse das Schlüsselwort „abstract“ hinzuzufügen, um die Deklaration zu qualifizieren . Aus diesem Grund werden die meisten Open-Source-Toolklassen wie Spring mit dem Schlüsselwort „abstract“ geändert.

3.1.3 JavaBean

Es gibt zwei gängige Implementierungsmethoden für die JavaBean-Definition: manuelles Schreiben und automatische Generierung.

public class Person {
 private String name;
 private int age;
 
 public Person(String name, int age) {
 this.name = name;
 this.age = age;
 }
 
 public String getName() {
 return name;
 }
 
 public void setName(String name) {
 this.name = name;
 }
 
 public int getAge() {
 return age;
 }
 
 public void setAge(int age) {
 this.age = age;
 }
}

Verwenden Sie das Lombok-Plug-in, um das Schreiben von Java-Code durch Anmerkungen zu verbessern und während der Kompilierung dynamisch Get- und Set-Methoden zu generieren.

import lombok.Data;

@NoArgsConstructor
@Data
@Accessors(chain = true)
public class Person {
    private String name;
    private int age;
}

Das Plug-in-Paket bietet außerdem praktische Funktionen zur Kettenprogrammierung wie @Builder und @Accessors, die die Codierungseffizienz bis zu einem gewissen Grad verbessern können.

3.1.4 Unveränderliche Klassen

Um die Stabilität und Konsistenz ihrer Funktionen und Verhaltensweisen sicherzustellen, sind Klassen in einigen Szenarien so konzipiert, dass sie nicht vererbt und überschrieben werden können.

Um es zu definieren, fügen Sie der Klasse das letzte Schlüsselwort hinzu, Beispiel:

public final class String implements Serializable, Comparable<String>, CharSequence {

}

Im Folgenden sind einige Klassen aufgeführt, die nicht vererbt und überschrieben werden können und in einigen zugrunde liegenden Middleware verwendet werden:

java.lang.String
java.lang.Math
java.lang.Boolean
java.lang.Character
java.util.Date
java.sql.Date
java.lang.System
java.lang.ClassLoader

3.1.5 Anonyme innere Klassen

Anonyme innere Klassen werden normalerweise zur Vereinfachung von Code verwendet. Ihre Definition und Verwendung erfolgt normalerweise an derselben Stelle. Die Verwendungsszenarien sind wie folgt:

1. Direkt als Parameter an die Methode oder den Konstruktor übergeben;
2. Eine anonyme Instanz, die zum Implementieren einer Schnittstelle oder abstrakten Klasse verwendet wird;
public class Example {
    public static void main(String[] args) {
        // 创建一个匿名内部类
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, World!");
            }
        };
        // 调用匿名内部类的方法
        runnable.run();
    }
}

3.1.6 Eine Klasse deklarieren

Eine Deklarationsklasse ist ein Basistyp oder eine Schnittstelle in der Java-Sprache, die zum Definieren des Verhaltens oder der Eigenschaften der Klasse verwendet wird. Einige sind sogar nur eine Deklaration ohne spezifische Methodendefinitionen.

AutoCloseable: Gibt an, dass die Klasse, die diese Schnittstelle implementiert, automatisch geschlossen werden kann, was normalerweise für die Ressourcenverwaltung verwendet wird.
Vergleichbar: Gibt an, dass eine Klasse, die diese Schnittstelle implementiert, mit anderen Objekten verglichen werden kann, die diese Schnittstelle implementieren.
Callable: Gibt an, dass eine Klasse, die diese Schnittstelle implementiert, als Parameter an den Thread-Pool übergeben werden kann und das Ergebnis zurückgibt.
Klonbar: Gibt an, dass Klassen, die diese Schnittstelle implementieren, geklont werden können.
Enum: Gibt an, dass die Klasse, die diese Schnittstelle implementiert, ein Aufzählungstyp ist.
Iterierbar: Gibt an, dass die Klasse, die diese Schnittstelle implementiert, iteriert werden kann.
Ausführbar: Gibt an, dass eine Klasse, die diese Schnittstelle implementiert, als Thread ausgeführt werden kann.
Serialisierbar: Gibt an, dass Klassen, die diese Schnittstelle implementieren, serialisiert und deserialisiert werden können.
Schnittstelle: Gibt an, dass die Klasse, die die Schnittstelle implementiert, eine Schnittstelle ist und Methodendeklarationen enthalten kann.
Annotation: Gibt an, dass die Klasse, die diese Schnittstelle implementiert, eine Annotation ist und zur Metadatenbeschreibung verwendet werden kann.

3.1.7 Rekordklasse

Die Record-Klasse wurde in Java14 in der Vorschau angezeigt und erst in Java17 offiziell veröffentlicht. Gemäß der Beschreibung von JEP395 ist die Record-Klasse ein Träger unveränderlicher Daten, ähnlich wie verschiedene Modell-, DTO-, VO- und andere POJO-Klassen, die heute weit verbreitet sind, aber der Datensatz selbst ist nach der Erstellung nicht mehr zuweisbar. Alle Datensatzklassen erben von java.lang.Record. Die Record-Klasse bietet standardmäßig einen vollständigen Feldkonstruktor, Attributzugriff sowie die Methoden equal, hashcode und toString. Ihre Funktion ist dem Lombok-Plug-in sehr ähnlich .

Definierter Weg

/**
 * 关键定义的类是不可变类
 * 将所有成员变量通过参数的形式定义
 * 默认会生成全部参数的构造方法
 * @param name
 * @param age
 */
public record Person(String name, int age) {
    public Person{
        if(name == null){
            throw new IllegalArgumentException("提供紧凑的方式进行参数校验");
        }
    }
    /**
     * 定义的类中可以定义静态方法
     * @param name
     * @return
     */
    public static Person of(String name) {
        return new Person(name, 18);
    }
}

Verwendung

Person person = new Person("John", 30);
// Person person = Person.of("John");
String name = person.name();
int age = person.age();

Szenen, die verwendet werden sollen

Erstellen Sie über Record ein temporäres Speicherobjekt und sortieren Sie die Person-Array-Objekte nach Alter.

public List<Person> sortPeopleByAge(List<Person> people) {
    
    record Data(Person person, int age){};

    return people.stream()
                .map(person -> new Data(person, computAge(person)))
                .sorted((d1, d2) -> Integer.compare(d2.age(), d1.age()))
                .map(Data::person)
                .collect(toList());
}
public int computAge(Person person) {
    return person.age() - 1;
}

3.1.8 Dichtungstyp

Die in Java 17 eingeführte neue Funktion Sealed Classes dient hauptsächlich dazu, die Vererbung von Klassen einzuschränken. Wir wissen, dass es zwei Hauptarten von Einschränkungen für Klassenvererbungsfunktionen gibt:

1. Final ändert die Klasse so, dass die Klasse nicht vererbt werden kann.
2. Die package-private-Klasse kann steuern, dass sie nur von Klassen unter demselben Paket geerbt werden kann.

Aber offensichtlich sind diese beiden Einschränkungen sehr grob, und versiegelte Klassen ermöglichen eine feinkörnigere Kontrolle der Klassenvererbung.

sealed class SealedClass permits SubClass1, SubClass2 {

}

class SubClass1 extends SealedClass {

}

class SubClass2 extends SealedClass {
   
}

Im obigen Beispiel ist SealedClass eine versiegelte Klasse, die zwei Unterklassen SubClass1 und SubClass2 enthält. In der Definition von SubClass1 und SubClass2 müssen Sie das Schlüsselwort „extends“ verwenden, um von SealedClass zu erben, und das Schlüsselwort „permits“ verwenden, um anzugeben, welche Unterklassen sie erben dürfen. Durch die Verwendung einer versiegelten Klasse können Sie sicherstellen, dass nur Unterklassen, die bestimmte Bedingungen erfüllen, das Protokoll oder die Spezifikation erben oder implementieren können.

3.2 Methodendefinition

3.2.1 Bauweise

Ein Konstruktor ist eine spezielle Methode zum Erstellen und Initialisieren von Objekten. Der Name des Konstruktors muss mit dem Klassennamen identisch sein und keinen Rückgabetyp haben. Beim Erstellen eines Objekts können Sie den Konstruktor mithilfe des Schlüsselworts new aufrufen.

public class MyClass {
    private int myInt;
    private String myString;

    // 构造方法
    public MyClass(int myInt, String myString) {
        this.myInt = myInt;
        this.myString = myString;
    }
}

Ein wichtiges Merkmal der Implementierung des Singleton-Musters besteht darin, dass Benutzer nicht nach Belieben (neue) Objekte erstellen dürfen. Wie erreicht man eine Sicherheitskontrolle? Den Konstruktor als privat zu deklarieren, ist ein wesentlicher Schritt.

3.2.2 Umschreiben der Methode

Das Überschreiben von Methoden bezieht sich auf die Neudefinition einer Methode mit demselben Namen in der übergeordneten Klasse in der Unterklasse. Durch das Überschreiben von Methoden kann eine Unterklasse die Methodenimplementierung in der übergeordneten Klasse überschreiben, um ihr eigenes Verhalten basierend auf den Anforderungen der Unterklasse zu implementieren.

class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myCat = new Cat();
        myCat.makeSound(); // 输出 "Meow"
    }
}

Eines der drei Hauptmerkmale des objektorientierten Polymorphismus ist das Umschreiben von Methoden.

3.2.3 Methodenüberladung

In einer Klasse werden mehrere Methoden mit demselben Namen, aber unterschiedlichen Parameterlisten definiert. Durch das Überladen von Methoden können wir denselben Methodennamen verwenden, um verschiedene Vorgänge auszuführen und basierend auf den an die Methode übergebenen Parametern unterschiedliche Codelogik auszuführen.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        int result1 = calculator.add(2, 3);
        double result2 = calculator.add(2.5, 3.5);
        System.out.println(result1); // 输出 5
        System.out.println(result2); // 输出 6.0
    }
}

3.2.4 Anonyme Methoden

Mit Java 8 wurden Lambda-Ausdrücke eingeführt, mit denen sich Funktionen implementieren lassen, die anonymen Methoden ähneln. Ein Lambda-Ausdruck ist eine anonyme Funktion, die als Parameter an eine Methode übergeben oder direkt als eigenständiger Ausdruck verwendet werden kann.

public static void main(String args[]) {
    List<String> names = Arrays.asList("hello", "world");
    // 使用 Lambda 表达式作为参数传递给 forEach 方法
    names.forEach((String name) -> System.out.println("Name: " + name));

    // 使用 Lambda 表达式作为独立表达式使用
    Predicate<String> nameLengthGreaterThan5 = (String name) -> name.length() > 5;
    boolean isLongName = nameLengthGreaterThan5.test("John");
    System.out.println("Is long name? " + isLongName);
}

3.3 Objektdefinition

3.3.1 Singleton-Objekt

Ein Singleton-Objekt ist ein Objekt, das wiederholt verwendet werden kann, aber nur eine Instanz hat. Es hat folgende Funktionen:

1. Steuern Sie die Ressourcennutzung: Steuern Sie den gleichzeitigen Zugriff auf Ressourcen durch Thread-Synchronisierung.
2. Kontrollieren Sie die Anzahl der generierten Instanzen: um den Zweck der Ressourceneinsparung zu erreichen.
3. Als Kommunikationsmedium verwendet: Das heißt, der Datenaustausch ermöglicht die Kommunikation zwischen zwei unabhängigen Threads oder Prozessen, ohne eine direkte Korrelation herzustellen.

Verwenden Sie beispielsweise eine Aufzählung, um das Singleton-Muster zu implementieren:

public enum Singleton {
 INSTANCE;
 
 public void someMethod() {
 // ...其他代码...
 }
}

3.3.2 Unveränderliche Objekte

Unveränderliche Objekte in Java sind Objekte, deren Zustand nach der Erstellung nicht mehr geändert werden kann. Unveränderliche Objekte sind ein sehr nützliches Objekt, da sie sicherstellen, dass der Zustand des Objekts jederzeit konsistent ist, und so Probleme vermeiden, die durch die Änderung des Objektzustands entstehen. Es gibt mehrere Möglichkeiten, unveränderliche Objekte zu implementieren:

1. Speichern Sie den Status des Objekts in einem unveränderlichen Objekt: String, Integer usw. sind integrierte unveränderliche Objekttypen.
2. Speichern Sie den Zustand des Objekts in einer Endvariablen: Eine Endvariable kann nicht mehr geändert werden, sobald ihr ein Wert zugewiesen wurde.
3. Legen Sie alle Eigenschaften des Objekts als unveränderliche Objekte fest: Dadurch wird sichergestellt, dass das gesamte Objekt unveränderlich ist.

Einige Containerklassenoperationen verfügen auch über entsprechende Wrapper-Klassen, um die Unveränderlichkeit von Containerobjekten zu implementieren, z. B. die Definition unveränderlicher Array-Objekte:

Collections.unmodifiableList(new ArrayList<>());
Wenn ein Objekt in der Domäne als Eingabeparameter übergeben wird, wird es als unveränderliches Objekt definiert. Dies ist für die Aufrechterhaltung der Datenkonsistenz sehr wichtig. Andernfalls wird die Unvorhersehbarkeit von Objektattributänderungen beim Auffinden von Problemen sehr problematisch sein.

3.3.3 Tupelobjekt

Tupel ist ein gängiges Konzept in funktionalen Programmiersprachen. Ein Tupel ist ein unveränderliches Objekt, das mehrere Objekte unterschiedlichen Typs in typsicherer Form speichern kann. Es handelt sich um eine sehr nützliche Datenstruktur, die es Entwicklern ermöglicht, mehrere Datenelemente bequemer und effizienter zu verarbeiten. Allerdings bietet die native Java-Standardbibliothek keine Tupelunterstützung, und wir müssen sie selbst oder mit Hilfe einer Klassenbibliothek eines Drittanbieters implementieren.

Zwei-Tupel-Implementierung

public class Pair<A,B> {
    public final A first;
    public final B second;

    public Pair(A a, B b) {
        this.first = a;
        this.second = b;
    }

    public A getFirst() {
        return first;
    }

    public B getSecond() {
        return second;
    }
}

Triplett-Implementierung

public class Triplet<A,B,C> extends Pair<A,B>{
    public final C third;

    public Triplet(A a, B b, C c) {
        super(a, b);
        this.third = c;
    }

    public C getThird() {
        return third;
    }

    public static void main(String[] args) {
        // 表示姓名,性别,年龄
        Triplet<String,String,Integer> triplet = new Triplet("John","男",18);
        // 获得姓名
        String name = triplet.getFirst();
    }

}

Implementierung mehrerer Gruppen

public class Tuple<E> {
    private final E[] elements;

    public Tuple(E... elements) {
        this.elements = elements;
    }

    public E get(int index) {
        return elements[index];
    }

    public int size() {
        return elements.length;
    }
    public static void main(String[] args) {
        // 表示姓名,性别,年龄
        Tuple<String> tuple = new Tuple<>("John", "男", "18");
        // 获得姓名
        String name = tuple.get(0);
    }
}

Tupel hat hauptsächlich die folgenden Funktionen:

1. Mehrere Datenelemente speichern: Tuple kann mehrere verschiedene Arten von Datenelementen speichern, bei denen es sich um Basistypen, Objekttypen, Arrays usw. handeln kann.

2. Vereinfachen Sie den Code: Tuple kann den Code prägnanter machen und das Schreiben wiederholten Codes reduzieren. Durch Tuple können wir mehrere Variablen in ein Objekt packen und so die Codemenge reduzieren;

3. Verbessern Sie die Lesbarkeit des Codes: Tupel kann die Lesbarkeit des Codes verbessern. Durch Tuple können wir mehrere Variablen in ein Objekt packen und so den Code besser lesbar machen;

4. Unterstützen Sie Funktionen, die mehrere Werte zurückgeben: Tupel kann Funktionen unterstützen, die mehrere Werte zurückgeben. In Java kann eine Funktion nur einen Wert zurückgeben, aber über Tuple können wir mehrere Werte in ein Objekt packen und zurückgeben;

Zu den Klassenbibliotheken von Drittanbietern, die das Tupelkonzept implementieren, gehören neben der Anpassung auch: Google Guava, Apache Commons Lang, JCTools, Vavr usw.

Das Tuple der Google Guava-Bibliothek bietet mehr Funktionalität und wird häufig verwendet. Um beispielsweise die Bedeutung von Tupeln klarer zu machen, stellt Guava das Konzept benannter Tupel bereit. Indem Sie den Tupeln Namen geben, können Sie die Bedeutung jedes Elements klarer ausdrücken. Beispiel:

NamedTuple namedTuple = Tuples.named("person", "name", "age");

3.3.4 Temporäre Objekte

Temporäre Objekte beziehen sich auf Objekte, die während der Programmausführung vorübergehend benötigt werden, aber einen kurzen Lebenszyklus haben. Diese Objekte sind während des Gebrauchs typischerweise nur kurzzeitig vorhanden und erfordern keine langfristige Lagerung oder Wiederverwendung.

Optimierungsvorschläge für temporäre Objekte lauten wie folgt:

1. Versuchen Sie, Objekte wiederzuverwenden. Da das System nicht nur Zeit braucht, um Objekte zu generieren, kann es in Zukunft auch Zeit brauchen, diese Objekte durch Müll zu sammeln und zu verarbeiten. Daher hat die Generierung zu vieler Objekte große Auswirkungen auf die Leistung des Programms. Zu den Strategien zur Wiederverwendung von Objekten gehören: Das Caching von Objekten kann auch für bestimmte Szenarien optimiert werden, z. B. die Verwendung von StringBuffer anstelle des String-Spleißens.
2. Versuchen Sie, lokale Variablen zu verwenden. Die beim Aufruf der Methode übergebenen Parameter und die beim Aufruf erstellten temporären Variablen werden auf dem Stack gespeichert, was schneller ist. Andere Variablen wie statische Variablen, Instanzvariablen usw. werden im Heap erstellt und sind langsamer.
3. Sammeln Sie nach Generation. Die Generations-Garbage-Collection-Strategie basiert auf der Tatsache, dass die Lebenszyklen verschiedener Objekte unterschiedlich sind. Daher können Gegenstände mit unterschiedlichen Lebenszyklen auf unterschiedliche Weise gesammelt werden, um die Recyclingeffizienz zu verbessern;

3.3.5 Walhalla

Als Hochsprache gab es schon immer eine große Leistungslücke zwischen Java und der niedrigeren C-Sprache und der Assemblersprache. Um diese Lücke zu schließen, wurde 2014 das Valhalla-Projekt mit dem Ziel gestartet, flexiblere flache Datentypen in JVM-basierte Sprachen zu integrieren.

Wir alle wissen, dass Java sowohl native Typen als auch Referenztypen unterstützt. Native Datentypen werden als Wert übergeben. Durch Zuweisung und Übergabe von Funktionsparametern wird der Wert kopiert. Nach dem Kopieren besteht keine Korrelation zwischen den beiden Kopien. Referenztypen werden in jedem Fall als Zeiger übergeben, und eine Änderung des Inhalts, auf den der Zeiger zeigt, hat Auswirkungen Alle Referenzen. Valhalla führte Werttypen ein, ein Konzept zwischen primitiven Typen und Referenztypen.

Da die meisten Java-Datenstrukturen in einer Anwendung Objekte sind, können wir Java als eine zeigerintensive Sprache betrachten. Diese zeigerbasierte Objektimplementierung wird verwendet, um die Objektidentifizierung zu ermöglichen, die wiederum für Sprachfunktionen wie Polymorphismus, Veränderlichkeit und Sperren verwendet wird. Standardmäßig gelten diese Eigenschaften für jedes Objekt, unabhängig davon, ob sie tatsächlich benötigt werden. Hier kommen Werttypen ins Spiel.

Das Konzept der Werttypen besteht darin, eine reine Datenaggregation darzustellen, die die Funktionalität regulärer Objekte entfernt. Wir haben also reine Daten, keine Identität. Damit verlieren wir natürlich auch die Funktionalität, die durch Objektidentifikation erreicht werden kann. Da wir keine Objektidentität mehr haben, können wir auf Zeiger verzichten und das allgemeine Speicherlayout von Werttypen ändern. Vergleichen wir das Speicherlayout der Objektreferenz und des Werttyps.



Die Objekt-Header-Informationen werden entfernt und der Werttyp spart 16 Byte Platz im Objekt-Header in einem 64-Bit-Betriebssystem. Gleichzeitig bedeutet dies auch, dass die eindeutige Identität (Identity) und die Initialisierungssicherheit des Objekts aufgegeben werden. Die vorherigen Schlüsselwörter oder Methoden wie wait (), notify (), synchronisiert (obj), System.identityHashCode (obj) usw. ungültig und kann nicht verwendet werden.

Valhalla wird die Leistung erheblich verbessern und undichte Abstraktionen reduzieren:

Leistungsverbesserungen werden durch die Abflachung des Objektdiagramms und das Entfernen der Indirektion erreicht. Dies führt zu einem effizienteren Speicherlayout und weniger Zuweisungen und Garbage Collections.
Bei Verwendung als generischer Typ weisen Grundelemente und Objekte ein ähnlicheres Verhalten auf, was eine bessere Abstraktion bedeutet.

Im September 2023 ist das Valhalla-Projekt noch in Arbeit und es wurde noch keine offizielle Version veröffentlicht. Es lohnt sich, sich auf dieses innovative Projekt zu freuen.

Vier Zusammenfassung

Dieser Artikel fasst den grundlegenden gesunden Menschenverstand zusammen, der häufig im Softwareentwicklungsprozess verwendet wird, und ist in zwei Kapitel unterteilt: Grundlagen und Praxis. Die Grundlagen konzentrieren sich auf die Namenskonventionen von Klassen, Methoden, Variablen und die Kriterien zur Beurteilung der Qualität von Codekommentaren . Im praktischen Kapitel werden gängige technische Konzepte und Implementierungspraktiken auf den drei Ebenen Klassen, Methoden und Objekte analysiert. Ich hoffe, dass dieser gesunde Menschenverstand den Lesern zum Nachdenken und Helfen verhelfen kann.

Autor: JD Retail Emily Liu

Quelle: JD Cloud Developer Community Bitte geben Sie beim Nachdruck die Quelle an

Lei Jun: Die offizielle Version von Xiaomis neuem Betriebssystem ThePaper OS wurde verpackt. Das Popup-Fenster auf der Lotterieseite der Gome App beleidigt seinen Gründer. Ubuntu 23.10 ist offiziell veröffentlicht. Sie können den Freitag genauso gut für ein Upgrade nutzen! Ubuntu 23.10-Release-Folge: Das ISO-Image wurde dringend „zurückgerufen“, da es Hassreden enthielt. Ein 23-jähriger Doktorand behob den 22 Jahre alten „Ghost Bug“ in Firefox. RustDesk Remote Desktop 1.2.3 wurde veröffentlicht, verbessertes Wayland zur Unterstützung von TiDB 7.4 Release: Offiziell kompatibel mit MySQL 8.0. Nach dem Abziehen des Logitech USB-Empfängers stürzte der Linux-Kernel ab. Der Master verwendete Scratch, um den RISC-V-Simulator zu rubbeln, und führte den Linux-Kernel erfolgreich aus. JetBrains startete Writerside, ein Tool zur Erstellung technischer Dokumente.
{{o.name}}
{{m.name}}

Ich denke du magst

Origin my.oschina.net/u/4090830/blog/10120062
Empfohlen
Rangfolge