Frage

Die neuen Erweiterungen in .Net 3.5 ermöglichen die Aufteilung der Funktionalität von Schnittstellen.

Zum Beispiel in .Net 2.0

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }

    List<IChild> GetChildren()
}

Kann (in 3.5) werden:

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }
}

public static class HaveChildrenExtension {
    public static List<IChild> GetChildren( this IHaveChildren ) {
        //logic to get children by parent type and id
        //shared for all classes implementing IHaveChildren 
    }
}

Dies scheint mir für viele Schnittstellen ein besserer Mechanismus zu sein.Sie benötigen keine abstrakte Basis mehr, um diesen Code zu teilen, und funktional funktioniert der Code genauso.Dadurch könnte der Code wartbarer und einfacher zu testen sein.

Der einzige Nachteil besteht darin, dass eine abstrakte Basenimplementierung virtuell sein kann, aber kann das umgangen werden (würde eine Instanzmethode eine Erweiterungsmethode mit demselben Namen verbergen?Wäre es verwirrender Code, dies zu tun?)

Gibt es weitere Gründe, dieses Muster nicht regelmäßig zu verwenden?


Klärung:

Ja, ich sehe die Tendenz bei Erweiterungsmethoden, sie überall zu haben.Ich würde besonders vorsichtig sein, wenn ich Wertetypen auf .Net ohne viel Peer-Review hätte (ich glaube, der einzige, den wir auf einer Zeichenfolge haben, ist a .SplitToDictionary() - ähnlich zu .Split() aber es wird auch ein Schlüsselwert-Trennzeichen verwendet)

Ich denke, da gibt es eine ganze Best-Practice-Debatte ;-)

(Übrigens:DannySmurf, deine PM klingt gruselig.)

Ich frage hier speziell nach der Verwendung von Erweiterungsmethoden, wo wir zuvor Schnittstellenmethoden hatten.


Ich versuche, viele Ebenen abstrakter Basisklassen zu vermeiden – die Klassen, die diese Modelle implementieren, verfügen meist bereits über Basisklassen.Ich denke, dieses Modell könnte wartbarer und weniger übermäßig gekoppelt sein als das Hinzufügen weiterer Objekthierarchien.

Ist es das, was MS mit IEnumerable und IQueryable für Linq gemacht hat?

War es hilfreich?

Lösung

Ich denke, dass der umsichtige Einsatz von Erweiterungsmethoden Schnittstellen in eine gleichwertigere Position mit (abstrakten) Basisklassen bringt.


Versionierung. Ein Vorteil von Basisklassen gegenüber Schnittstellen besteht darin, dass Sie in einer späteren Version problemlos neue virtuelle Mitglieder hinzufügen können, während das Hinzufügen von Mitgliedern zu einer Schnittstelle Implementierer beschädigt, die mit der alten Version der Bibliothek erstellt wurden.Stattdessen muss eine neue Version der Schnittstelle mit den neuen Mitgliedern erstellt werden, und die Bibliothek muss einen Workaround oder den Zugriff auf ältere Objekte beschränken, die nur die ursprüngliche Schnittstelle implementieren.

Als konkretes Beispiel könnte die erste Version einer Bibliothek eine Schnittstelle wie folgt definieren:

public interface INode {
  INode Root { get; }
  List<INode> GetChildren( );
}

Sobald die Bibliothek veröffentlicht wurde, können wir die Schnittstelle nicht mehr ändern, ohne die aktuellen Benutzer zu unterbrechen.Stattdessen müssten wir in der nächsten Version eine neue Schnittstelle definieren, um zusätzliche Funktionen hinzuzufügen:

public interface IChildNode : INode {
  INode Parent { get; }
}

Allerdings können nur Benutzer der neuen Bibliothek die neue Schnittstelle implementieren.Um mit Legacy-Code arbeiten zu können, müssen wir die alte Implementierung anpassen, was eine Erweiterungsmethode gut bewältigen kann:

public static class NodeExtensions {
  public INode GetParent( this INode node ) {
    // If the node implements the new interface, call it directly.
    var childNode = node as IChildNode;
    if( !object.ReferenceEquals( childNode, null ) )
      return childNode.Parent;

    // Otherwise, fall back on a default implementation.
    return FindParent( node, node.Root );
  }
}

Jetzt können alle Benutzer der neuen Bibliothek sowohl ältere als auch moderne Implementierungen gleich behandeln.


Überlastungen. Ein weiterer Bereich, in dem Erweiterungsmethoden nützlich sein können, ist die Bereitstellung von Überladungen für Schnittstellenmethoden.Möglicherweise verfügen Sie über eine Methode mit mehreren Parametern zur Steuerung ihrer Aktion, von denen im 90-Prozent-Fall nur der erste oder die ersten beiden wichtig sind.Da C# das Festlegen von Standardwerten für Parameter nicht zulässt, müssen Benutzer entweder jedes Mal die vollständig parametrisierte Methode aufrufen oder jede Implementierung muss die trivialen Überladungen für die Kernmethode implementieren.

Stattdessen können Erweiterungsmethoden verwendet werden, um die trivialen Überladungsimplementierungen bereitzustellen:

public interface ILongMethod {
  public bool LongMethod( string s, double d, int i, object o, ... );
}

...
public static LongMethodExtensions {
  public bool LongMethod( this ILongMethod lm, string s, double d ) {
    lm.LongMethod( s, d, 0, null );
  }
  ...
}


Bitte beachten Sie, dass beide Fälle im Hinblick auf die von den Schnittstellen bereitgestellten Operationen geschrieben sind und triviale oder bekannte Standardimplementierungen beinhalten.Das heißt, Sie können von einer Klasse nur einmal erben, und der gezielte Einsatz von Erweiterungsmethoden kann eine wertvolle Möglichkeit sein, mit einigen der Feinheiten umzugehen, die Basisklassen bieten, die Schnittstellen fehlen :)


Bearbeiten: Ein verwandter Beitrag von Joe Duffy: Erweiterungsmethoden als Standardimplementierungen von Schnittstellenmethoden

Andere Tipps

Erweiterungsmethoden sollten genau so verwendet werden:Erweiterungen.Jeder wichtige struktur-/designbezogene Code oder jede nicht-triviale Operation sollte in ein Objekt eingefügt werden, das in eine Klasse oder Schnittstelle eingefügt/von dieser geerbt wird.

Sobald ein anderes Objekt versucht, die erweiterte Version zu verwenden, werden die Erweiterungen nicht angezeigt und müssen sie möglicherweise erneut implementieren/erneut referenzieren.

Die traditionelle Weisheit besagt, dass Erweiterungsmethoden nur für Folgendes verwendet werden sollten:

  • Utility-Klassen, wie Vaibhav erwähnte
  • Erweiterung versiegelter APIs von Drittanbietern

Ich denke, das Beste, was Erweiterungsmethoden ersetzen können, sind all die Utility-Klassen, die man in jedem Projekt findet.

Zumindest im Moment habe ich das Gefühl, dass jede andere Verwendung von Erweiterungsmethoden zu Verwirrung am Arbeitsplatz führen würde.

Meine beiden Teile.

Ich halte die Trennung der Domänen-/Modell- und UI-/Ansichtsfunktionen mithilfe von Erweiterungsmethoden für eine gute Sache, insbesondere da sie sich in separaten Namespaces befinden können.

Zum Beispiel:

namespace Model
{
    class Person
    {
        public string Title { get; set; }
        public string FirstName { get; set; }
        public string Surname { get; set; }
    }
}

namespace View
{
    static class PersonExtensions
    {
        public static string FullName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName + " " + p.Surname;
        }

        public static string FormalName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName[0] + ". " + p.Surname;
        }
    }
}

Auf diese Weise können Erweiterungsmethoden ähnlich wie XAML-Datenvorlagen verwendet werden.Sie können nicht auf private/geschützte Mitglieder der Klasse zugreifen, aber es ermöglicht die Aufrechterhaltung der Datenabstraktion ohne übermäßige Codeduplizierung in der gesamten Anwendung.

An der Erweiterung von Schnittstellen ist nichts auszusetzen. Tatsächlich funktioniert LINQ auf diese Weise, um die Erweiterungsmethoden zu den Auflistungsklassen hinzuzufügen.

Allerdings sollten Sie dies nur dann tun, wenn Sie in allen Klassen, die diese Schnittstelle implementieren, die gleiche Funktionalität bereitstellen müssen und diese Funktionalität nicht Teil der „offiziellen“ Implementierung einer abgeleiteten Schnittstelle ist (und wahrscheinlich auch nicht sein sollte). Klassen.Das Erweitern einer Schnittstelle ist auch dann sinnvoll, wenn es einfach unpraktisch ist, für jeden möglichen abgeleiteten Typ, der die neue Funktionalität erfordert, eine Erweiterungsmethode zu schreiben.

Ich sehe viele Leute, die die Verwendung einer Basisklasse zur gemeinsamen Nutzung gemeinsamer Funktionen befürworten.Seien Sie dabei vorsichtig – Sie sollten die Zusammensetzung der Vererbung vorziehen.Vererbung sollte nur dann für Polymorphismus verwendet werden, wenn dies aus Modellierungssicht sinnvoll ist.Es ist kein gutes Tool für die Wiederverwendung von Code.

Zur Frage:Beachten Sie dabei die Einschränkungen – zum Beispiel wird im gezeigten Code durch die Verwendung einer Erweiterungsmethode zur Implementierung von GetChildren diese Implementierung effektiv „versiegelt“ und es keinem IHaveChildren-Impl gestattet, bei Bedarf eine eigene bereitzustellen.Wenn das in Ordnung ist, dann stört mich der Ansatz der Erweiterungsmethode nicht so sehr.Es ist nicht in Stein gemeißelt und kann in der Regel leicht umgestaltet werden, wenn später mehr Flexibilität benötigt wird.

Für mehr Flexibilität kann die Verwendung des Strategiemusters vorzuziehen sein.Etwas wie:

public interface IHaveChildren 
{
    string ParentType { get; }
    int ParentId { get; }
}

public interface IChildIterator
{
    IEnumerable<IChild> GetChildren();
}

public void DefaultChildIterator : IChildIterator
{
    private readonly IHaveChildren _parent;

    public DefaultChildIterator(IHaveChildren parent)
    {
        _parent = parent; 
    }

    public IEnumerable<IChild> GetChildren() 
    { 
        // default child iterator impl
    }
}

public class Node : IHaveChildren, IChildIterator
{ 
    // *snip*

    public IEnumerable<IChild> GetChildren()
    {
        return new DefaultChildIterator(this).GetChildren();
    }
}

Ein kleines bisschen mehr.

Wenn mehrere Schnittstellen dieselbe Signatur der Erweiterungsmethode haben, müssen Sie den Aufrufer explizit in einen Schnittstellentyp konvertieren und dann die Methode aufrufen.Z.B.

((IFirst)this).AmbigousMethod()

Autsch.Bitte erweitern Sie die Schnittstellen nicht.
Eine Schnittstelle ist ein sauberer Vertrag, den eine Klasse implementieren soll, und Ihre Verwendung dieser Klassen muss Damit dies ordnungsgemäß funktioniert, muss es auf die Inhalte der Kernschnittstelle beschränkt werden.

Deshalb deklarieren Sie immer die Schnittstelle als Typ und nicht als eigentliche Klasse.

IInterface variable = new ImplementingClass();

Rechts?

Wenn Sie wirklich einen Vertrag mit zusätzlichen Funktionen benötigen, sind abstrakte Klassen Ihre Freunde.

Rob Connery (Subsonic und MVC Storefront) hat in seiner Storefront-Anwendung ein IRepository-ähnliches Muster implementiert.Es handelt sich zwar nicht ganz um das obige Muster, aber es weist einige Gemeinsamkeiten auf.

Die Datenschicht gibt IQueryable zurück, wodurch die konsumierende Schicht darüber hinaus Filter- und Sortierausdrücke anwenden kann.Der Vorteil besteht darin, dass Sie beispielsweise eine einzelne GetProducts-Methode angeben und dann in der konsumierenden Ebene entsprechend entscheiden können, wie Sie diese Sortierung, Filterung oder auch nur einen bestimmten Bereich von Ergebnissen wünschen.

Kein traditioneller Ansatz, aber sehr cool und definitiv ein Fall von DRY.

Ein Problem, das ich sehe, besteht darin, dass dieses Muster in einem großen Unternehmen dazu führen kann, dass der Code für jedermann schwer (wenn nicht sogar unmöglich) zu verstehen und zu verwenden ist.Wenn mehrere Entwickler ständig ihre eigenen Methoden zu vorhandenen Klassen hinzufügen, getrennt von diesen Klassen (und, Gott helfe uns allen, sogar zu BCL-Klassen), könnte ich mir vorstellen, dass eine Codebasis ziemlich schnell außer Kontrolle gerät.

Sogar bei meiner eigenen Arbeit konnte ich mir vorstellen, dass das passierte. Da mein PM den Wunsch hegte, jeden Code, an dem wir arbeiten, entweder zur Benutzeroberfläche oder zur Datenzugriffsschicht hinzuzufügen, konnte ich mir gut vorstellen, dass er darauf bestand, dass 20 oder 30 Methoden hinzugefügt werden System.String, die nur tangential mit der String-Verarbeitung zusammenhängen.

Ich musste etwas Ähnliches lösen:Ich wollte, dass eine List<IIDable> an die Erweiterungsfunktion übergeben wird, wobei IIDable eine Schnittstelle ist, die über eine lange getId()-Funktion verfügt.Ich habe versucht, GetIds(this List<IIDable> bla) zu verwenden, aber der Compiler hat mir das nicht erlaubt.Ich habe stattdessen Vorlagen verwendet und dann den Typ innerhalb der Funktion in den Schnittstellentyp umgewandelt.Ich brauchte diese Funktion für einige von Linq to SQL generierte Klassen.

Ich hoffe das hilft :)

    public static List<long> GetIds<T>(this List<T> original){
        List<long> ret = new List<long>();
        if (original == null)
            return ret;

        try
        {
            foreach (T t in original)
            {
                IIDable idable = (IIDable)t;
                ret.Add(idable.getId());
            }
            return ret;
        }
        catch (Exception)
        {
            throw new Exception("Class calling this extension must implement IIDable interface");
        }
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top