2 Stimmen

Ist es möglich, einen Niedergang zu vermeiden?

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.

2voto

Marc Gravell Punkte 970173

Erstens frage ich mich, ob das ganze Szenario nicht ein wenig künstlich ist; werden Sie wirklich brauchen diese Abstraktionsebene? Vielleicht abonnieren Sie YAGNI ?

Warum hat Ihr MyGraphics funktionieren nur mit einer MyFont ? Kann es mit einer IFont ? Das wäre eine bessere Nutzung der Schnittstellen und würde dieses ganze Problem vermeiden...

Eine Option könnte eine kleine Umgestaltung sein, so dass die IFont beschreibt nur die Metadaten für die Schriftart (Größe, Schriftart usw.), und Sie haben Dinge auf der konkreten MyGraphics mögen:

[public|internal] MyFont GetFont(IFont font) {...} // or just Font

und es wird die Aufgabe der Grafik, die Übersetzung zu machen - so dann etwas wie verwendet:

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
}

Ja, natürlich, Point muss vielleicht auch übersetzt werden ;-p

0 Stimmen

Letztendlich möchte ich einen System-API-Aufruf machen: die DrawString-Methode der Klasse System.Drawing.Graphics nimmt eine System.Drawing.Font-Instanz als Parameter.

0 Stimmen

"Brauchen Sie wirklich diese Abstraktionsebene" ... Ich versuche, das Idiom des Unit-Tests für "bescheidene Dialogfelder" auf den Test eines benutzerdefinierten Steuerelements zu übertragen: Ich möchte die Logik unit-testen, aber es ist einfacher für den Test, die Ausgabe zu bestätigen, wenn die Ausgabe auf etwas anderes als ...

0 Stimmen

... eine System.Drawing.Graphics-Instanz (z. B. wenn die Ausgabe stattdessen in ein zweidimensionales speicherinternes Array von Zeichen erfolgt).

2voto

Jon Skeet Punkte 1325502

Sie sagen damit: "Ich weiß es besser als der Compiler - ich weiß, dass es sich um eine Instanz von MyFont ." An diesem Punkt haben Sie MyFont y MyGraphics wieder eng gekoppelt werden, was den Sinn der Schnittstelle ein wenig schmälert.

Sollte MyGraphics arbeiten mit jeder IFont , oder nur eine MyFont ? Wenn Sie es mit einem beliebigen IFont wird es dir gut gehen. Andernfalls müssen Sie sich möglicherweise mit komplizierten Generika befassen, um das Ganze zur Kompilierzeit typsicher zu machen. Sie finden vielleicht mein Beitrag über Generika in Protokollpuffern nützlich als eine ähnliche Situation.

(Kleiner Tipp am Rande: Ihr Code wird idiomatischer, wenn Sie sich an die Namenskonventionen halten, zu denen auch die Pascal-Großschreibung für Methoden gehört).

0 Stimmen

Richtig. Ich werde die Antwort entsprechend bearbeiten, wenn Sie das getan haben :)

1voto

Marcel Jackwerth Punkte 51964

Ich bin mir derzeit nicht mehr ganz sicher, was C# ist - das ist jetzt schon eine Weile her. Aber wenn Sie nicht wollen, um alle Ihre Sachen dort zu casten, könnten Sie gezwungen sein, Generika zu verwenden.

Ich kann nur Java-Code zur Verfügung stellen, aber C# sollte in der Lage sein, dasselbe über die where Stichwort.

Machen Sie Ihre Schnittstelle zu einer generischen Schnittstelle. In Java wäre das

IMyGraphics<T extends IMyFont> und dann MyGraphics : IMyGraphics<MyFont>

Definieren Sie dann die drawString Unterschrift zum Mitnehmen T font als zweiten Parameter anstelle von IMyFont . Damit sollten Sie in der Lage sein, Folgendes zu schreiben

public void drawString(string text, MyFont font, Point point)

direkt in Ihr MyGraphics Klasse.


In C# ist das IMyGraphics<T extends IMyFont> sollte sein public interface IMyGraphics<T> where T:IMyFont aber ich bin mir da nicht 100%ig sicher.

0voto

arul Punkte 13880

Du magst die Besetzung nicht von IFont a MyFont ? Sie können dies tun:

interface IFont {
    object Font { get; }
}

class MyFont : IFont {
    object Font { get { return ...; } }
}

Natürlich müssen Sie immer noch von System.Object a System.Drawing.Font in der Zeichenmethode, aber Sie haben gerade die Abhängigkeit von einer bestimmten Klassenimplementierung beseitigt ( MyFont ).

public void DrawString(string text, IFont font, Point point)
{
    System.Drawing.Font f = (Font)font.Font;
    graphics.DrawString(text, f, Brushes.Block, point);
}

0 Stimmen

Wenn ich das täte, würde IFont von Font abhängen und meine Logik, die von IFont abhängt, würde somit implizit auch von Font abhängen (und könnte daher nicht mit ICanvass und IFont verwendet werden, die in ihrer Implementierung nicht die System.Drawing-Typen verwenden).

0 Stimmen

Nein, IFont hängt von System.Object ab, es weiß nicht und kümmert sich nicht darum, was in der Variablen "Font" gespeichert ist.

0 Stimmen

Jetzt sehe ich, dass du Recht hast. Allerdings gibt es, wie Sie sagten, immer noch einen Abstrich (vom Objekt zur Schrift).

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X