Nehmen wir an, ich habe zwei Klassen, die so aussehen (der erste Codeblock und das allgemeine Problem beziehen sich auf C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Diese Klassen können in keiner Weise geändert werden (sie sind Teil einer Assembly eines Drittanbieters). Daher kann ich sie nicht dazu bringen, dieselbe Schnittstelle zu implementieren oder dieselbe Klasse zu erben, die dann IntProperty enthalten würde.
Ich möchte einige Logik auf die IntProperty
Eigenschaft beider Klassen anwenden , und in C ++ könnte ich eine Template-Klasse verwenden, um dies ganz einfach zu tun:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
Und dann könnte ich so etwas machen:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
Auf diese Weise konnte ich eine einzelne generische Factory-Klasse erstellen, die sowohl für ClassA als auch für ClassB funktionieren würde.
In C # muss ich jedoch zwei Klassen mit zwei verschiedenen where
Klauseln schreiben , obwohl der Code für die Logik genau gleich ist:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
Ich weiß, dass, wenn ich verschiedene Klassen in der where
Klausel haben möchte , sie verwandt sein müssen, dh dieselbe Klasse erben, wenn ich denselben Code in dem oben beschriebenen Sinne auf sie anwenden möchte. Es ist nur sehr ärgerlich, zwei völlig identische Methoden zu haben. Wegen der Leistungsprobleme möchte ich auch keine Reflexion verwenden.
Kann jemand einen Ansatz vorschlagen, der eleganter formuliert werden kann?
Antworten:
Fügen Sie eine Proxy-Schnittstelle hinzu (manchmal Adapter genannt , gelegentlich mit geringfügigen Unterschieden), implementieren Sie sie
LogicToBeApplied
im Hinblick auf den Proxy und fügen Sie dann eine Möglichkeit hinzu, eine Instanz dieses Proxys aus zwei Lambdas zu erstellen: eine für die Eigenschaft get und eine für die Menge.Wenn Sie nun ein IProxy übergeben müssen, aber eine Instanz der Klassen von Drittanbietern haben, können Sie einfach einige Lambdas übergeben:
Darüber hinaus können Sie einfache Hilfsprogramme schreiben, um LamdaProxy-Instanzen aus Instanzen von A oder B zu erstellen. Dies können sogar Erweiterungsmethoden sein, um Ihnen einen "fließenden" Stil zu verleihen:
Und jetzt sieht die Konstruktion von Proxies so aus:
Was Ihre Fabrik betrifft, würde ich sehen, ob Sie sie in eine "Haupt" -Fabrikmethode umgestalten können, die ein IProxy akzeptiert und alle Logik darauf und andere Methoden ausführt, die nur übergeben werden,
new A().Proxied()
odernew B().Proxied()
:Es gibt keine Möglichkeit, das Äquivalent Ihres C ++ - Codes in C # zu erstellen, da C ++ - Vorlagen auf struktureller Typisierung beruhen . Solange zwei Klassen denselben Methodennamen und dieselbe Signatur haben, können Sie diese Methode in C ++ für beide generisch aufrufen. C # hat eine nominelle Typisierung - der Name einer Klasse oder Schnittstelle ist Teil ihres Typs. Daher können die Klassen
A
undB
nicht in irgendeiner Form gleich behandelt werden, es sei denn, eine explizite "is a" -Beziehung wird durch Vererbung oder Schnittstellenimplementierung definiert.Wenn das Boilerplate für die Implementierung dieser Methoden pro Klasse zu groß ist, können Sie eine Funktion schreiben, die ein Objekt übernimmt und reflektierend eine erstellt,
LambdaProxy
indem Sie nach einem bestimmten Eigenschaftsnamen suchen:Dies scheitert abgrundtief, wenn Objekte eines falschen Typs gegeben werden. Reflexion führt inhärent zu der Möglichkeit von Fehlern, die das C # -System nicht verhindern kann. Glücklicherweise können Sie Reflexionen vermeiden, bis der Wartungsaufwand der Helfer zu groß wird, da Sie die IProxy-Schnittstelle oder die LambdaProxy-Implementierung nicht ändern müssen, um den reflektierenden Zucker hinzuzufügen.
Ein Grund dafür ist, dass
LambdaProxy
es "maximal generisch" ist. Es kann jeden Wert anpassen, der den "Geist" des IProxy-Vertrags implementiert, da die Implementierung von LambdaProxy vollständig durch die angegebenen Get- und Setter-Funktionen definiert ist. Es funktioniert sogar, wenn die Klassen unterschiedliche Namen für die Eigenschaft haben oder unterschiedliche Typen, die sinnvoll und sicher alsint
s darstellbar sind, oder wenn es eine Möglichkeit gibt, das Konzept,Property
das dargestellt werden soll, auf andere Merkmale der Klasse abzubilden . Die Indirektion durch die Funktionen bietet Ihnen maximale Flexibilität.quelle
ReflectiveProxier
einen einen Proxy mit demdynamic
Schlüsselwort erstellen ? Mir scheint, Sie hätten dieselben grundlegenden Probleme (dh Fehler, die nur zur Laufzeit auftreten), aber die Syntax und Wartbarkeit wären viel einfacher.Im Folgenden finden Sie einen Überblick über die Verwendung von Adaptern ohne Vererbung von A und / oder B mit der Möglichkeit, sie für vorhandene A- und B-Objekte zu verwenden:
Normalerweise würde ich diese Art von Objektadapter den Klassenproxys vorziehen. Sie vermeiden hässliche Probleme, die bei der Vererbung auftreten können. Diese Lösung funktioniert beispielsweise auch, wenn A und B versiegelte Klassen sind.
quelle
new int Property
? Sie beschatten nichts.Sie könnten
ClassA
undClassB
über eine gemeinsame Schnittstelle anpassen . Auf diese WeiseLogicAToBeApplied
bleibt Ihr Code in unverändert. Nicht viel anders als das, was du hast.quelle
A
,B
-Typen an eine gemeinsame Schnittstelle anpassen . Der große Vorteil ist, dass wir die gemeinsame Logik nicht duplizieren müssen. Der Nachteil ist, dass die Logik jetzt den Wrapper / Proxy anstelle des tatsächlichen Typs instanziiert.LogicToBeApplied
sie eine bestimmte Komplexität aufweist und sollte unter keinen Umständen an zwei Stellen in der Codebasis wiederholt werden. Dann ist der zusätzliche Kesselschildcode oft vernachlässigbar.Die C ++ - Version funktioniert nur, weil die Vorlagen "Static Duck Typing" verwenden - alles wird kompiliert, solange der Typ die richtigen Namen enthält. Es ist eher ein Makrosystem. Das generische System von C # und anderen Sprachen funktioniert sehr unterschiedlich.
Die Antworten von devnull und Doc Brown zeigen, wie das Adaptermuster verwendet werden kann, um Ihren Algorithmus allgemein zu halten und trotzdem mit beliebigen Typen zu arbeiten ... mit ein paar Einschränkungen. Insbesondere erstellen Sie jetzt einen anderen Typ, als Sie tatsächlich möchten.
Mit ein wenig Trick ist es möglich, genau den beabsichtigten Typ ohne Änderungen zu verwenden. Jetzt müssen wir jedoch alle Interaktionen mit dem Zieltyp in eine separate Schnittstelle extrahieren. Hier sind diese Wechselwirkungen Konstruktion und Eigentumszuweisung:
In einer OOP-Interpretation wäre dies ein Beispiel für das Strategiemuster , obwohl es mit Generika gemischt ist.
Wir können dann Ihre Logik umschreiben, um diese Interaktionen zu verwenden:
Die Interaktionsdefinitionen sehen folgendermaßen aus:
Der große Nachteil dieses Ansatzes besteht darin, dass der Programmierer beim Aufrufen der Logik eine Interaktionsinstanz schreiben und übergeben muss. Dies ähnelt weitgehend Lösungen auf der Basis von Adaptermustern, ist jedoch etwas allgemeiner.
Meiner Erfahrung nach können Sie dies am ehesten mit Vorlagenfunktionen in anderen Sprachen erreichen. Ähnliche Techniken werden in Haskell, Scala, Go und Rust verwendet, um Schnittstellen außerhalb einer Typdefinition zu implementieren. In diesen Sprachen greift der Compiler jedoch implizit ein und wählt die richtige Interaktionsinstanz aus, sodass das zusätzliche Argument nicht angezeigt wird. Dies ähnelt auch den Erweiterungsmethoden von C #, ist jedoch nicht auf statische Methoden beschränkt.
quelle
Wenn Sie wirklich Vorsicht walten lassen möchten, können Sie den Compiler mit "dynamic" veranlassen, sich um die gesamte Überlegungsfiesheit zu kümmern. Dies führt zu einem Laufzeitfehler, wenn Sie ein Objekt an SetSomeProperty übergeben, das keine Eigenschaft namens SomeProperty hat.
quelle
Die anderen Antworten identifizieren das Problem korrekt und bieten praktikable Lösungen. C # unterstützt (im Allgemeinen) nicht "Enten-Typisierung" ("Wenn es wie eine Ente läuft ..."), daher gibt es keine Möglichkeit, Ihre zu erzwingen
ClassA
undClassB
austauschbar zu sein, wenn sie nicht auf diese Weise entworfen wurden.Wenn Sie jedoch bereits bereit sind, das Risiko eines Laufzeitfehlers in Kauf zu nehmen, gibt es eine einfachere Antwort als die Verwendung von Reflection.
C # hat das
dynamic
Schlüsselwort, das für Situationen wie diese perfekt ist. Es sagt dem Compiler "Ich werde erst zur Laufzeit (und vielleicht auch dann nicht) wissen, welcher Typ dies ist, also erlaube mir, überhaupt etwas dagegen zu tun ".Auf diese Weise können Sie genau die gewünschte Funktion erstellen:
Beachten Sie auch die Verwendung des
static
Schlüsselworts. Damit können Sie dies verwenden als:Die
dynamic
Verwendung von Reflection hat keine Auswirkungen auf die Gesamtleistung , ebenso wenig wie die einmalige Auswirkung (und die zusätzliche Komplexität) der Verwendung von Reflection. Wenn Ihr Code zum ersten Mal auf einen dynamischen Anruf mit einem bestimmten Typ trifft, entsteht ein geringer Overhead . Wiederholte Anrufe sind jedoch genauso schnell wie der Standardcode. Aber Sie werden erhalten ein ,RuntimeBinderException
wenn Sie versuchen , in etwas zu übergeben, die diese Eigenschaft nicht hat, und es gibt keine gute Möglichkeit , dass vor der Zeit zu überprüfen. Möglicherweise möchten Sie diesen Fehler auf nützliche Weise behandeln.quelle
Sie können Reflection verwenden, um die Eigenschaft nach Namen abzurufen.
Offensichtlich riskieren Sie mit dieser Methode einen Laufzeitfehler. Welches ist, was C # versucht, Sie zu stoppen.
Ich habe irgendwo gelesen, dass Sie mit einer zukünftigen Version von C # Objekte als Schnittstelle übergeben können, die sie nicht erben, aber zusammenpassen. Welches würde auch Ihr Problem lösen.
(Ich werde versuchen, den Artikel auszugraben)
Eine andere Methode, obwohl ich nicht sicher bin, ob sie Ihnen Code erspart, besteht darin, sowohl A als auch B zu unterklassifizieren und auch eine Schnittstelle mit IntProperty zu erben.
quelle
Ich wollte nur
implicit operator
Conversions zusammen mit dem Delegate / Lambda-Ansatz von Jacks Antwort verwenden.A
undB
sind wie angenommen:Dann ist es einfach, eine nette Syntax mit impliziten benutzerdefinierten Konvertierungen zu erhalten (keine Erweiterungsmethoden oder ähnliches erforderlich):
Anwendungsbeispiel:
Die
Initialize
Methode zeigt, wie Sie damit arbeiten können,Adapter
ohne sich darum zu kümmern, ob es sich um einA
oder einB
anderes Objekt handelt. Die invokations derInitialize
Methode zeigt , dass wir brauchen keine (sichtbaren) gegossen oder.AsProxy()
oder ähnlich zu behandeln BetonA
oderB
als einAdapter
.Überlegen Sie, ob Sie eine
ArgumentNullException
benutzerdefinierte Konvertierung durchführen möchten, wenn das übergebene Argument eine Nullreferenz ist oder nicht.quelle