Wann sollten Sie Erweiterungsmethoden für Ihre eigenen Klassen schreiben?

8

Ich habe kürzlich eine Codebasis gesehen, in der eine Datenklasse Addressirgendwo und dann an einem anderen Ort definiert wurde:

fun Address.toAnschrift() = let { address ->
    Anschrift().apply {
        // mapping code here...
    }
}

Ich fand es verwirrend, diese Methode nicht direkt an der Adresse zu haben. Gibt es etablierte Muster oder Best Practices für die Verwendung von Erweiterungsmethoden? Oder muss das Buch darüber noch geschrieben werden?

Beachten Sie, dass ich trotz der Kotlin-Syntax in meinem Beispiel an allgemeinen Best Practices interessiert bin, da diese auch für C # oder andere Sprachen mit diesen Funktionen gelten würden.

Robert Jack Will
quelle
4
Da "Anschrift" eine deutsche Version von "Adresse" ist, scheint dies eine länderspezifische Konvertierung eines länderneutralen Adresstyps zu sein. Es ist sinnvoll, den länderspezifischen Code an einem anderen Ort aufzubewahren. Dies ist keine Antwort auf Ihre eigentliche Frage, aber es erklärt, warum sie es in diesem Beispiel getan haben .
R. Schmitz
1
»Mit Erweiterungsmethoden können Sie vorhandenen Typen Methoden" hinzufügen ", ohne einen neuen abgeleiteten Typ zu erstellen, den ursprünglichen Typ neu zu kompilieren oder auf andere Weise zu ändern . « Docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… . »Für eine von Ihnen implementierte Klassenbibliothek sollten Sie keine Erweiterungsmethoden verwenden ... Wenn Sie einer Bibliothek, für die Sie den Quellcode besitzen, wichtige Funktionen hinzufügen möchten, sollten Sie die Standardrichtlinien von .NET Framework für die Assemblyversionierung befolgen.« tl; dr Wenn Sie den Code besitzen, sind keine Erweiterungsmethoden erforderlich.
Thomas Junk

Antworten:

15

Erweiterungsmethoden bieten nichts, was auf andere Weise nicht möglich ist. Sie sind syntaktischer Zucker, um die Entwicklung zu verbessern, ohne einen tatsächlichen technischen Nutzen zu bieten.


Erweiterungsmethoden sind relativ neu. Standards und Konventionen werden normalerweise von der Meinung der Bevölkerung festgelegt (plus langjähriger Erfahrung darüber, was letztendlich eine schlechte Idee ist), und ich glaube nicht, dass wir an dem Punkt angelangt sind, an dem wir eine endgültige Linie ziehen können, der alle zustimmen.

Ich kann jedoch mehrere Argumente sehen, die auf Dingen basieren, die ich von Kollegen gehört habe.

1. Sie ersetzen normalerweise statische Methoden.

Erweiterungsmethoden basieren am häufigsten auf Dingen, die sonst statische Methoden gewesen wären, nicht auf Klassenmethoden. Sie sehen aus wie Klassenmethoden, sind es aber nicht wirklich.

Erweiterungsmethoden sollten einfach nicht als Alternative für Klassenmethoden betrachtet werden. sondern um ansonsten syntaktisch hässlichen statischen Methoden ein "Klassenverfahren" zu verleihen .

2. Wenn Ihre beabsichtigte Methode spezifisch für ein konsumierendes Projekt ist, jedoch nicht für die Quellbibliothek.

Nur weil Sie sowohl die Bibliothek als auch den Verbraucher entwickeln, bedeutet dies nicht, dass Sie bereit sind, die Logik dort zu platzieren, wo sie passt.

Zum Beispiel:

  • Sie haben eine PersonDtoKlasse aus Ihrem DataLayerProjekt.
  • Ihr WebUIProjekt möchte a PersonDtoin a konvertieren können PersonViewModel.

Sie können (und möchten) diese Konvertierungsmethode nicht zu Ihrem DataLayerProjekt hinzufügen . Da diese Methode auf das WebUIProjekt beschränkt ist, sollte sie sich in diesem Projekt befinden.

Mit einer Erweiterungsmethode können Sie diese Methode innerhalb Ihres Projekts (und möglicher Konsumenten Ihres Projekts) global zugänglich machen, ohne dass die DataLayerBibliothek sie implementieren muss.

3. Wenn Sie der Meinung sind, dass Datenklassen nur Daten enthalten sollten.

Beispielsweise Personenthält Ihre Klasse die Eigenschaften für eine Person. Sie möchten jedoch einige Methoden haben, mit denen einige Daten formatiert werden können:

public class Person
{
    //The properties...

    public SecurityQuestionAnswers GetSecurityAnswers()
    {
        return new SecurityQuestionAnswers()
        {
            MothersMaidenName = this.Mother.MaidenName,
            FirstPetsName = this.Pets.OrderbyDescending(x => x.Date).FirstOrDefault()?.Name
        };
    }
}

Ich habe viele Mitarbeiter gesehen, die die Idee hassen, Daten und Logik in einer Klasse zu mischen. Ich bin mit einfachen Dingen wie dem Formatieren von Daten nicht ganz einverstanden, aber ich gebe zu, dass es sich im obigen Beispiel schmutzig anfühlt Person, wenn man sich darauf verlassen muss SecurityQuestionAnswers.

Durch das Einfügen dieser Methode in eine Erweiterungsmethode wird verhindert, dass die ansonsten reine Datenklasse beschmutzt wird.

Beachten Sie, dass dieses Argument stilistisch ist. Dies ähnelt Teilklassen. Dies hat wenig bis gar keinen technischen Vorteil, aber Sie können Code in mehrere Dateien aufteilen, wenn Sie ihn für sauberer halten (z. B. wenn viele Benutzer die Klasse verwenden, sich aber nicht um Ihre zusätzlichen benutzerdefinierten Methoden kümmern).

4. Hilfsmethoden

In den meisten Projekten neige ich dazu, Helferklassen zu haben. Hierbei handelt es sich um statische Klassen, die normalerweise eine Sammlung von Methoden zur einfachen Formatierung bereitstellen.

Beispielsweise hatte eine Firma, bei der ich arbeitete, ein bestimmtes Datums- / Uhrzeitformat, das sie verwenden wollten. Anstatt die Formatzeichenfolge überall einzufügen oder die Formatzeichenfolge zu einer globalen Variablen zu machen, habe ich mich für eine DateTime-Erweiterungsmethode entschieden:

public static string ToCompanyFormat(this DateTime dt)
{
    return dt.ToString("...");
} 

Ist das besser als eine normale statische Hilfsmethode? Ich glaube schon. Es bereinigt die Syntax. Anstatt

DateTimeHelper.ToCompanyFormat(myObj.CreatedOn);

Ich könnte:

myObj.CreatedOn.ToCompanyFormat();

Dies ähnelt einer fließenden Version derselben Syntax. Ich mag es mehr, obwohl es keinen technischen Vorteil hat, wenn man übereinander steht.


Alle diese Argumente sind subjektiv. Keiner von ihnen deckt einen Fall ab, der sonst niemals abgedeckt werden könnte.

  • Jede Erweiterungsmethode kann problemlos in eine normale statische Methode umgeschrieben werden.
  • Code kann mithilfe von Teilklassen in Dateien getrennt werden, auch wenn keine Erweiterungsmethoden vorhanden waren.

Andererseits können wir Ihre Behauptung aufgreifen, dass sie niemals extrem notwendig sind:

  • Warum brauchen wir Klassenmethoden, wenn wir immer statische Methoden mit einem Namen erstellen können, der eindeutig angibt, dass es sich um eine bestimmte Klasse handelt?

Die Antwort auf diese und Ihre Frage bleibt dieselbe:

  • Schönere Syntax
  • Verbesserte Lesbarkeit und weniger Code-Pedanterie (Code wird in weniger Zeichen auf den Punkt gebracht)
  • Intellisense liefert kontextbezogene Ergebnisse (Erweiterungsmethoden werden nur für den richtigen Typ angezeigt. Statische Klassen können überall verwendet werden).

Eine besondere Erwähnung:

  • Erweiterungsmethoden ermöglichen eine Verkettung des Codierungsstils. Dies ist in letzter Zeit populärer geworden, im Gegensatz zu der Vanille-Methoden-Wrapping-Syntax. Ähnliche syntaktische Einstellungen finden Sie in der Methodensyntax von LINQ.
  • Während die Methodenverkettung auch mithilfe von Klassenmethoden durchgeführt werden kann; Beachten Sie, dass dies für statische Methoden nicht ganz gilt. Erweiterungsmethoden basieren am häufigsten auf Dingen, die sonst statische Methoden gewesen wären, nicht auf Klassenmethoden.
Flater
quelle
5
Punkt 2 ist derjenige, den ich am überzeugendsten finde, und der Grund, warum ich das selbst getan habe. Manchmal haben Sie eine Klasse, die in Ihrer gesamten Codebasis weit verbreitet ist, und eine Methode, die so aussieht, als ob sie in der Klasse "sollte", mit der Ausnahme, dass es sich um ein Projekt oder eine Bibliothek eines Drittanbieters handelt, die Sie kapseln möchten. Erweiterungsmethoden für Ihre eigenen Klassen sind eine logische Methode, um damit umzugehen.
Astrid
@Astrid: Ich denke, das ist der technischste Grund dafür. Ja.
Flater
Sehr schöne Antwort. Ein besonderer Anwendungsfall von C # ist, dass Schnittstellen kein Verhalten aufweisen. Erweiterungsmethoden ermöglichen es uns, ihnen Methoden zu geben.
RubberDuck
Was meinen Sie mit "technischen Vorteilen"? Dinge wie Leistung? Die Lesbarkeit des Codes ist ebenso wichtig, wenn nicht noch wichtiger.
Robert Harvey
@ RobertHarvey Mit technisch meine ich alles, was nicht nur syntaktischer Zucker ist. Das Verteilen einer Teilklasse auf mehrere Dateien in derselben Assembly hat beispielsweise keinen technischen Vorteil. Das Verteilen einer Klasse und ihrer Erweiterungsmethoden auf verschiedene Assemblys ist möglich. Die Lesbarkeit des Codes ist natürlich immer noch wichtig, aber nicht aus technischer Sicht.
Flater
5

Ich stimme Flater zu, dass nicht genügend Zeit für die Kristallisation von Konventionen vorhanden war, aber wir haben das Beispiel der .NET Framework-Klassenbibliotheken selbst. Beachten Sie, dass LINQ-Methoden beim Hinzufügen zu .NET nicht direkt hinzugefügt wurden IEnumerable, sondern als Erweiterungsmethoden.

Was können wir daraus ziehen? LINQ ist

  1. eine zusammenhängende Reihe von Funktionen, die nicht immer benötigt werden,
  2. soll im Methodenverkettungsstil verwendet werden (z. B. A.B().C()anstelle von C(B(A))), und
  3. erfordert keinen Zugriff auf den privaten Status des Objekts

Wenn Sie alle drei Funktionen haben, sind Erweiterungsmethoden sehr sinnvoll. In der Tat würde ich argumentieren, dass wenn eine Reihe von Methoden Feature # 3 und eines der ersten beiden Features hat, Sie erwägen sollten, sie Erweiterungsmethoden zu machen.

Erweiterungsmethoden:

  1. So können Sie vermeiden, die Kern-API einer Klasse zu verschmutzen.
  2. Ermöglichen Sie nicht nur den Methodenverkettungsstil, sondern auch die flexiblere Verarbeitung von nullWerten, da eine von einem nullWert aufgerufene Erweiterungsmethode einfach das nullerste Argument erhält.
  3. Verhindern Sie, dass Sie versehentlich auf den privaten / geschützten Status in einer Methode zugreifen / diesen ändern, die keinen Zugriff darauf haben sollte

Erweiterungsmethoden haben einen weiteren Vorteil: Sie können weiterhin als statische Methode verwendet und direkt als Wert an eine Funktion höherer Ordnung übergeben werden. Wenn MyMethodes sich also um eine Erweiterungsmethode handelt, myList.Select(x => x.MyMethod())können Sie sie einfach verwenden myList.Select(MyMethod). Ich finde das schöner.

James Jensen
quelle
4
Der Grund, warum LINQ als eine Reihe von Erweiterungsmethoden implementiert wurde, besteht darin, dass es sich um eine allgemeine Reihe von Operationen über eine Schnittstelle handelt und Sie keine Implementierungen auf Schnittstellen vornehmen können. Es hat eigentlich nichts mit Ihren Punkten (1) und (2) zu tun. Dies steht im Gegensatz zu Javas Lösung für dasselbe Problem, das darin besteht, Implementierungen in Schnittstellendefinitionen zu ermöglichen (was die Tür zu einer Welt des Schnittstellenmissbrauchs öffnet).
Ameise P
"und Sie können keine Implementierungen auf Schnittstellen setzen". Noch nicht, aber diese Aussage wird nicht mehr wahr sein, wenn C # 8 unsere Maschinen erreicht :)
David Arno
@ DavidArno wirklich? Ich bin seit einer Weile nicht mehr in der C # -Welt. Das ist eine Schande. Scheint mir ein Schritt zurück zu sein ...
Ant P
4

Die treibende Kraft hinter Erweiterungsmethoden ist die Fähigkeit, allen Klassen oder Schnittstellen eines bestimmten Typs Funktionen hinzuzufügen, unabhängig davon, ob sie versiegelt sind oder sich in einer anderen Baugruppe befinden. Sie können dies anhand von LINQ-Code nachweisen, indem Sie allen IEnumerable<T>Objekten Funktionen hinzufügen , unabhängig davon, ob es sich um Listen oder Stapel usw. handelt. Das Konzept bestand darin, die Funktionen zukunftssicher zu machen, sodass Sie IEnumerable<T>LINQ automatisch verwenden können , wenn Sie eigene Funktionen entwickeln .

Die Sprachfunktion ist jedoch so neu, dass Sie keine Richtlinien haben, wann Sie sie verwenden oder nicht verwenden sollen. Sie ermöglichen jedoch eine Reihe von Dingen, die bisher nicht möglich waren:

  • Fließende APIs ( ein Beispiel für das Hinzufügen fließender APIs zu nahezu jedem Objekt finden Sie unter Fließende Zusicherungen. )
  • Integrieren von Funktionen (NetCore Asp.NET MVC verwendet Erweiterungsmethoden, um Abhängigkeiten und Funktionen im Startcode zu verkabeln.)
  • Lokalisieren der Auswirkungen eines Features (Ihr Beispiel in der Eröffnungserklärung)

Um fair zu sein, wird nur die Zeit zeigen, wie weit zu weit ist oder an welchem ​​Punkt Erweiterungsmethoden eher ein Hindernis als ein Segen werden. Wie in einer anderen Antwort erwähnt, enthält eine Erweiterungsmethode nichts, was mit einer statischen Methode nicht möglich wäre. Bei vernünftiger Verwendung können sie den Code jedoch leichter lesbar machen.

Was ist mit der absichtlichen Verwendung von Erweiterungsmethoden in derselben Assembly?

Ich denke, es ist wertvoll, die Kernfunktionalität gut getestet und vertrauenswürdig zu haben und dann erst dann damit herumzuspielen, wenn Sie es unbedingt müssen. Neuer Code, der davon abhängt, aber nur zum Nutzen des neuen Codes Funktionen bereitstellen muss, kann mithilfe von Erweiterungsmethoden Funktionen hinzufügen, die den neuen Code unterstützen. Diese Funktionen sind nur für den neuen Code von Interesse, und der Umfang der Erweiterungsmethoden ist auf den Namespace beschränkt, in dem sie deklariert sind.

Ich würde die Verwendung von Erweiterungsmethoden nicht blind ausschließen, aber ich würde überlegen, ob die neue Funktionalität wirklich zu dem vorhandenen Kerncode hinzugefügt werden sollte, der gut getestet ist.

Berin Loritsch
quelle
1
+1 für fließende Apis.
Robert Harvey
@ RobertHarvey, Mein letzter Absatz unter der Überschrift "innerhalb derselben Versammlung"? Wenn Sie den Quellcode für diese Assembly haben, können Sie damit machen, was Sie wollen, aber sobald sie kompiliert ist, ist sie versiegelt.
Berin Loritsch
Mein Kommentar wurde entfernt, da ich meinen Standpunkt anscheinend nicht effektiv kommunizierte.
Robert Harvey
1

Gibt es etablierte Muster oder Best Practices für die Verwendung von Erweiterungsmethoden?

Einer der häufigsten Gründe für die Verwendung von Erweiterungsmethoden ist das "Erweitern, nicht Ändern" von Schnittstellen. Anstatt zu haben IFoound IFoo2, wo letztere eine neue Methode einführt Bar, Barwird IFooüber eine Erweiterungsmethode hinzugefügt .

Einer der Gründe dafür verwendet Linq - Erweiterungsmethoden für Where, Selectetc ist , um Benutzern zu ermöglichen , ihre eigenen Versionen dieser Methoden zu definieren, die dann auch von der Linq Abfragesyntax verwendet werden.

Darüber hinaus ist der Ansatz, den ich verwende und der zu dem zu passen scheint, was ich im .NET Framework normalerweise sehe, zumindest:

  1. Fügen Sie Methoden, die in direktem Zusammenhang mit der Klasse stehen, in die Klasse selbst ein.
  2. Fügen Sie alle Methoden, die nicht mit der Kernfunktionalität der Klasse zusammenhängen, in Erweiterungsmethoden ein.

Also , wenn ich eine habe Option<T>, würde ich die Dinge wie HasValue, Value, ==usw. innerhalb des Typs selbst. Aber so etwas wie eine MapFunktion, die die Monade von T1in konvertiert T2, würde ich eine Erweiterungsmethode einfügen:

public static Option<TOutput> Map<TInput, TOutput>(this Option<TInput> input, 
                                                   Func<TInput, TOutput> f) => ...
David Arno
quelle
1
One of the reasons Linq uses extension methods for Where, Select etc is in order to allow users to define their own versions of these methods, which are then used by the Linq query syntax too. -- Bist du dir da sicher? Die Linq-Syntax verwendet in großem Umfang Lambda-Ausdrücke (dh erstklassige Funktionen), sodass Sie keine eigenen Versionen von Selectund schreiben müssen Where. Ich kenne keine Fälle, in denen Leute ihre eigenen Versionen dieser Funktionen rollen.
Robert Harvey
Trotzdem finde ich Ihre Antwort die überzeugendste aller bisherigen Antworten. Erweiterungsmethoden sind eine großartige Möglichkeit, Schnittstellen weiterzuentwickeln. Genau deshalb wurden sie für Linq erstellt.
Robert Harvey
@ RobertHarvey, du musst nicht, aber du kannst .
David Arno
@RobertHarvey Wenn Sie den if (!predicate(item))Code in diesem Code auf if (predicate(item))ändern, ändern sich die Ergebnisse, da meine Version von verwendet wird Where.
David Arno
Ja ich verstehe. Aber niemand würde das jemals tun; Sie würden einfach das Prädikat ändern. Das ist der springende Punkt bei Prädikaten: Ändern Sie den Zustand, ohne die Funktion zu ändern.
Robert Harvey