Nicht exportierte Typen in Go: Wo alles begann
Wenn man von klassischen objektorientierten Sprachen wie Java oder PHP in die Welt von Go einsteigt, sind viele Entwickler an die Verwendung von Klassen und die automatische Objektinitialisierung beim Aufruf von Konstruktoren gewöhnt.
Probleme mit dem Export privater Typen
Das Hauptproblem beim Exportieren privater Typen besteht darin, dass dies das Prinzip der Kapselung verletzt.
Allein der Begriff "nicht exportierte Struktur" selbst besagt: Sie ist für die interne Verwendung im Paket bestimmt. Die wird benötigt, damit die Codekonsumenten NUR mit exportierten Typen arbeiten. Das ist die Kapselung, die es Ihnen ermöglicht, Dinge zu verbergen, auf die von außen nicht zugegriffen werden sollte! Es handelt sich um etwas Komplexes, das sich mit neuen Versionen des Pakets ändern kann. Daher ist es dasselbe, als würde man versuchen, etwas, das als "nicht essbar" gekennzeichnet ist - essbar zu machen. Es ist, als würde man Nägel mit einem Presslufthammer einschlagen. Wenn ein Programmierer so etwas tut, versteht er einfach nicht, warum es notwendig ist.
Schreiben von Strukturen, die keine Initialisierung erfordern.
Beispiele: sync.Mutex, time.Time, sync.WaitGroup und andere.
Bei der Standardinitialisierung funktionieren sie korrekt. Benannte Konstruktoren können für die Initialisierung in den gewünschten Zustand verwendet werden: time.Now(), time.Parse(). Dies ist der ideale Ansatz! Allerdings kann dies nicht in allen Fällen erreicht werden.
Ein komplexer Domänendienst erfordert möglicherweise eine tiefere Initialisierung.
Zum Beispiel Dependency Injection (Config, Logger, Repo, NetClient usw.). In diesem Fall werden ein benannter Konstruktor und eine Struktur mit privaten Feldern erstellt, in die bei der Grundinitialisierung nichts geschrieben werden kann. In diesem Fall werden Abhängigkeiten, Konfigurationen und andere Dinge im Konstruktor überprüft, der einen Fehler zurückgeben kann.
NewDomainService(c Config, d Dependencies) (*DomainService, error)
Solche Dienste haben in der Signatur immer Methoden, die eine domänenspezifische Funktion ausführen:
- Start(ctx context.Context) error
- Process(ctx context.Context) error
- Execute(ctx context.Context) error
- .....
Wenn der Dienst nicht ordnungsgemäß initialisiert wurde, gibt er einen Fehler zurück. In 99% der Fälle können solche Dienste ohnehin verschiedene Fehler aus dutzenden von Gründen zurückgeben. Das Argument „Ich bin zu faul, Fehler zu beheben“ klingt also nicht gut.
Fehler behandeln ist der Go-Ansatz
Das wird mit zwei Zeilen erledigt, die vom IDE automatisch generiert werden. Und genau das ermöglicht es, sofort die Schwachstellen des eigenen Codes zu erkennen, mögliche Fehler zu identifizieren und darüber nachzudenken, was damit zu tun ist. Und dabei wird nichts Schlimmes passieren.
Sie wird vom Verbraucher verarbeitet, und im Falle einer fehlerhaften Initialisierung wird der Programmierer den Fehler auf der Ebene der Tests seines Moduls sehen, sofern er sie natürlich schreibt. Im schlimmsten Fall wird alles beim ersten Testlauf der Anwendung zusammenbrechen.
Wenn jedoch ein solches Problem (mein Dienst wurde nicht initialisiert) zu "irgendwelchen schrecklichen kritischen Dingen" führt, wie zum Beispiel das Löschen der Datenbank, das Formatieren der Festplatte oder das Übertragen von Bitcoins an eine nicht existierende Brieftasche, dann haben Sie Probleme mit der Dienstentwicklung! Das Problem liegt bei Ihnen, nicht bei denen, die Ihren Code verwenden! Denn so etwas sollte niemals passieren!
Schutz vor unerwarteten Problemen mit "naiven Verbrauchern" - erwarte Probleme
Und zwar:
- Sie werden nicht in der Lage sein, eine ordentliche Anwendungsarchitektur aufzubauen. Denn Sie können diesen privaten Typ einfach nicht in den oberen Schichten verwenden. Zum Beispiel, wenn es sich um ein Repository handelt oder um einen anderen Domänendienst, muss er im Verbraucher-Interface beschrieben werden. Aber das können Sie nicht tun, weil es ein privater Typ ist! Sie werden einfach nicht in der Lage sein, etwas außerhalb seines Pakets zu typisieren! Daher wird DI nicht funktionieren.
- Wenn Sie es in einem Interface-Typ eines Verbrauchers einbauen wollen, benötigen Sie Folgendes:
- ServiceLocator oder ApplicationRegistry - dies ist ein Top-Level-Architekturobjekt, das Verweise auf die Hauptdienste speichert. Es muss die Initialisierung dieses Objekts kennen und speichern
- IoC - hier müssen Sie eine Implementierung für das Interface binden, das automatisch beim Erstellen von Objekten injiziert wird. Ob Sie dies manuell tun, Wire oder FX verwenden, Sie müssen trotzdem beispielsweise einen FX-Show-Konstruktor für diesen Typ schreiben und ihn dann an die entsprechenden Interfaces binden. All dies ist mit privaten Typen unmöglich. - Sie können die GoDoc-Dokumentation nicht verwenden, weil private Typen dort nicht für die externe Verwendung beschrieben werden, was absolut logisch ist!
- Dies widerspricht der Idiomatik der Sprache und verwirrt andere Entwickler mit einem ungewöhnlichen und falschen Ansatz.
Anstelle einer Schlussfolgerung
Das Exportieren privater Typen in Go mag auf den ersten Blick praktisch erscheinen, aber in der Praxis kann dies zu ernsthaften Problemen und Schwierigkeiten bei der Entwicklung führen. Verwenden Sie private Typen nur für die interne Verwendung in Ihren Paketen und halten Sie sich an den idiomatischen Ansatz beim Schreiben von Go-Code.
Wenn Sie Beratung zur Go-Entwicklung benötigen oder Ihr Anwendungsprogramm verbessern möchten, zögern Sie nicht, uns zu kontaktieren. Wir helfen Ihnen gerne bei der Suche nach den besten Lösungen für Ihre Anforderungen.