Ich habe einige Logik, die einige benutzerdefinierte Typen definiert und verwendet, wie diese:
class Word
{
System.Drawing.Font font; //a System type
string text;
}
class Canvass
{
System.Drawing.Graphics graphics; //another, related System type
... and other data members ...
//a method whose implementation combines the two System types
internal void draw(Word word, Point point)
{
//make the System API call
graphics.DrawString(word.text, word.font, Brushes.Block, point);
}
}
Die Logik, nachdem sie Berechnungen mit den Typen durchgeführt hat (z.B. um jede Word
Instanz), verwendet indirekt einige System
APIs, zum Beispiel durch Aufrufen der Canvass.draw
Methode.
Ich würde diese Logik gerne unabhängig von der System.Drawing
Namespace: hauptsächlich, um bei Unit-Tests zu helfen (ich denke, die Ausgabe von Unit-Tests wäre leichter zu überprüfen, wenn die draw
Methode auf etwas anderes als eine reale System.Drawing.Graphics
Instanz).
Um die Abhängigkeit der Logik von der System.Drawing
Namespace, dachte ich mir, ich deklariere einige neue Schnittstellen, die als Platzhalter für die System.Drawing
Typen, zum Beispiel:
interface IMyFont
{
}
interface IMyGraphics
{
void drawString(string text, IMyFont font, Point point);
}
class Word
{
IMyFont font; //no longer depends on System.Drawing.Font
string text;
}
class Canvass
{
IMyGraphics graphics; //no longer depends on System.Drawing.Graphics
... and other data ...
internal void draw(Word word, Point point)
{
//use interface method instead of making a direct System API call
graphics.drawText(word.text, word.font, point);
}
}
Wenn ich dies täte, dann könnten verschiedene Baugruppen unterschiedliche Implementierungen der IMyFont
y IMyGraphics
Schnittstelle, zum Beispiel ...
class MyFont : IMyFont
{
System.Drawing.Font theFont;
}
class MyGraphics : IMyGraphics
{
System.Drawing.Graphics theGraphics;
public void drawString(string text, IMyFont font, Point point)
{
//!!! downcast !!!
System.Drawing.Font theFont = ((MyFont)font).theFont;
//make the System API call
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
}
... allerdings wäre für die Umsetzung ein Downcast wie oben dargestellt erforderlich.
Meine Frage ist, Gibt es eine Möglichkeit, dies zu tun, ohne einen Downcast in der Implementierung zu benötigen? Mit "das" meine ich "UDTs definieren wie Word
y Canvass
die nicht von bestimmten konkreten Maßnahmen abhängen System
Typen"?
Eine Alternative wären abstrakte UDTs ...
class Word
{
//System.Drawing.Font font; //declared in a subclass of Word
string text;
}
class Canvass
{
//System.Drawing.Graphics graphics; //declared in a subclass of Canvass
//concrete draw method is defined in a subclass of Canvass
internal abstract void draw(Word word, Point point);
}
... aber auch dies würde einen Downcast in der Implementierung der Unterklasse erfordern.
Ich dachte auch an die Verwendung des Double Dispatch Idioms, aber das hängt von der Benennung der verschiedenen Unterklassen in den APIs ab.
Oder, wenn nicht mit Schnittstellen oder Unterklassen, gibt es eine Möglichkeit mit Delegaten?
--Bearbeitung:--
Es gibt zwei mögliche Antworten.
Eine Antwort ist die Verwendung von Generika, genau wie in der Antwort von Sir Lantis weiter unten und in dem von John Skeet verlinkten Blog-Beitrag vorgeschlagen. Ich vermute, dass dies in den meisten Szenarien gut funktionieren würde. Der Nachteil aus meiner Sicht ist, dass es bedeutet die Einführung TFont
als Template-Parameter: Es ist nicht nur eine Klasse wie Word
(die eine Font
Instanz), die eine generische Klasse werden muss (wie WordT<TFont>
) ... es ist auch so, dass jede Klasse, die eine WordT<TFont>
(z.B.. Paragraph
) muss nun auch generisch werden mit einer TFont
Parameter (z.B. ParagraphT<TFont>
). Schließlich ist fast jede Klasse in der Baugruppe zu einer generischen Klasse geworden. Diese hace die Typensicherheit zu wahren und die Notwendigkeit zu vermeiden, ... sondern es ist irgendwie hässlich und stört die Illusion der Kapselung (die Illusion, dass "Font" ein undurchsichtiges Implementierungsdetail ist).
Eine andere Lösung ist die Verwendung einer Karte oder eines Wörterbuchs in der Benutzerklasse. Anstelle von Font
in der wiederverwendbaren Bibliothek, und definieren Sie statt einer abstrakten Schnittstelle eine "Handle"-Klasse wie:
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
Dann, anstatt sich von FontHandle
einhalten. Dictionary<int, Font>
Instanz, die die FontHandle
Werte zu Font
Instanzen.