Ist es möglich, einen gesenkten zu vermeiden?
-
20-08-2019 - |
Frage
Ich habe eine gewisse Logik, die definiert und verwendet einige benutzerdefinierte Typen, 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, nach Berechnungen mit den Typen zu tun (z.B. jede Word
Instanz zu suchen), indirekt einige System
APIs verwendet, beispielsweise durch die Canvass.draw
Verfahren aufgerufen wird.
Ich mag diese Logik machen, unabhängig von der System.Drawing
Namespace: meist, um mit Unit-Tests zu helfen (ich glaube, Ausgabe Unit-Tests einfacher wäre, wenn die draw
Methode zeichnet andere etwas zu überprüfen als ein echten System.Drawing.Graphics
Beispiel).
Um die Logik der Abhängigkeit von dem System.Drawing
Namensraum zu beseitigen, ich dachte, dass ich ein paar neue Schnittstellen erklären würde als Platzhalter für die System.Drawing
Typen zu handeln, 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 das tue, dann verschiedene Baugruppen unterschiedliche Implementierungen des IMyFont
und IMyGraphics
Schnittstelle haben könnten, 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);
}
}
... aber die Umsetzung würde ein niedergeschlagene müssen wie oben dargestellt.
Meine Frage ist, gibt es eine Möglichkeit, dies zu tun, ohne einen niedergeschlagenen bei der Umsetzung zu benötigen? Mit „this“, meine ich „definieren UDTs wie Word
und Canvass
, die auf spezifische hängen nicht Beton System
Typen "?
Eine Alternative wäre, abstrakt 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 das würde auch braucht einen niedergeschlagene in der Umsetzung der Unterklasse.
Ich dachte auch an die Doppel-Ausgabe Idiom verwenden, aber es hängt von den verschiedenen Unterklassen in den APIs zu benennen.
Oder, wenn nicht mit Schnittstellen oder Unterklassen, ist es eine Möglichkeit, mit Delegierten?
- Edit: -
Es gab zwei mögliche Antworten.
Eine Antwort ist, Generika zu verwenden, genau wie unten durch ‚Sir Lantis‘ Antwort vorgeschlagen, und wie durch die Blog-Post, an den John Skeet verknüpft vorgeschlagen. Ich vermute, dass dies in den meisten Szenarien in Ordnung funktionieren würde. Die Abwärtsseite aus meiner Sicht ist, dass es bedeutet, TFont
als Template-Parameter Einführung: Es ist nicht nur eine Klasse wie Word
ist (die eine Font
Instanz enthält), die eine generische Klasse zu werden braucht (wie WordT<TFont>
) ... es ist auch, dass jede Klasse, die eine WordT<TFont>
enthält (zB Paragraph
) jetzt muss auch mit einem TFont
Parameter zur Gattungsbezeichnung geworden (zB ParagraphT<TFont>
). Schließlich fast jede Klasse in der Versammlung hat eine generische Klasse worden. Das funktioniert erhalten Typsicherheit und vermeiden die Notwendigkeit, niedergeschlagenen ... und es ist irgendwie hässlich, und stört die Illusion der Kapselung (die Illusion, dass ‚Font‘ ein undurchsichtige Implementierung Detail).
Eine andere Antwort ist, eine Karte oder ein Wörterbuch in der Benutzerklasse zu verwenden. Statt Font
in der Mehrweg-Bibliothek, und statt einer abstrakten Schnittstelle, definiert eine ‚Griff‘ Klasse wie:
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
Dann wird anstelle von einziehe von FontHandle
, halten eine Dictionary<int, Font>
Instanz, die FontHandle
Werte abbildet Instanzen Font
.
Lösung
Erstens, ich frage mich, ob das gesamte Szenario nicht etwas künstlich ist; sind Sie wirklich auf Notwendigkeit dieser Ebene der Abstraktion? Vielleicht abonnieren Sie YAGNI ?
Warum hat Ihr MyGraphics
nur mit einem MyFont
arbeiten? Kann es mit einem IFont
arbeiten? Das wäre eine bessere Nutzung der Schnittstellen sein, und würde dieses ganze Problem vermeiden ...
Eine Option könnte ein bisschen ein Re-Design, so dass die IFont
nur die Metadaten für die Schrift beschreibt (Größe, font-face, usw.), und Sie haben die Dinge auf dem Beton MyGraphics
wie:
[public|internal] MyFont GetFont(IFont font) {...} // or just Font
, und es wird die Aufgabe der Grafiken, die Übersetzung zu tun - so verwendet dann so etwas wie:
public void drawString(string text, IMyFont font, Point point)
{
using(System.Drawing.Font theFont = GetFont(font))
{
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
// etc
}
Natürlich Point
müssen möglicherweise Übersetzung zu ;-p
Andere Tipps
Sie sagen, effektiv „Ich weiß besser als die Compiler -. Ich weiß, dass es gebunden ist eine Instanz von MyFont
zu sein“ An diesem Punkt haben Sie wieder MyFont
und MyGraphics
ist eng gekoppelt bekommen, das den Punkt der Schnittstelle ein wenig reduziert.
Sollte die Arbeit mit jedem MyGraphics
IFont
oder nur ein MyFont
? Wenn Sie es mit jedem IFont
funktioniert alles wird gut. Andernfalls müssen Sie bei komplizierten Generika suchen es alle Kompilierung-Typ sicher zu machen. Sie können feststellen, mein Beitrag auf Generika in Protocol Buffers nützlich als einer ähnlichen Situation.
(Side-Vorschlag -. Ihr Code wird mehr idiomatisch .NET-like, wenn Sie die Namenskonventionen, die für Methoden Pascal Fall enthält)
Ich bin zur Zeit nicht ganz bewusst, C # mehr - hat einige Zeit jetzt gewesen. Aber wenn Sie nicht wollen, da alle deine Sachen werfen, werden Sie möglicherweise gezwungen Generika zu verwenden.
Ich kann nur bieten Java-Code, aber C # sollte das gleiche über das where
Stichwort tun können.
Machen Sie Ihre Schnittstelle eine generische Schnittstelle. In Java, das wäre
IMyGraphics<T extends IMyFont>
und dann MyGraphics : IMyGraphics<MyFont>
neu definieren dann die drawString
Signature T font
als zweiten Parameter statt IMyFont
zu nehmen. Dies sollte Ihnen ermöglichen, schreiben
public void drawString(string text, MyFont font, Point point)
direkt in Ihre MyGraphics
Klasse.
In C #, dass IMyGraphics<T extends IMyFont>
public interface IMyGraphics<T> where T:IMyFont
werden soll, aber ich bin nicht 100% sicher.
Sie weiß nicht, wie die Besetzung von IFont
MyFont
? Sie können dies tun:
interface IFont {
object Font { get; }
}
class MyFont : IFont {
object Font { get { return ...; } }
}
Sicher müssen Sie noch von System.Object
werfen in der Zeichnung Methode System.Drawing.Font
, aber Sie haben nur beseitigt die Abhängigkeit von bestimmten Klassenimplementierung (MyFont
).
public void DrawString(string text, IFont font, Point point)
{
System.Drawing.Font f = (Font)font.Font;
graphics.DrawString(text, f, Brushes.Block, point);
}