Можно ли избежать удручения? - PullRequest
2 голосов
/ 02 февраля 2009

У меня есть некоторая логика, которая определяет и использует некоторые пользовательские типы, такие как:

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);
  }
}

Логика после выполнения вычислений с типами (например, для определения местоположения каждого Word экземпляра) косвенно использует некоторые System API, например, вызывая метод Canvass.draw.

Я бы хотел сделать эту логику независимой от пространства имен System.Drawing: в основном, чтобы помочь с модульным тестированием (я думаю, что результаты модульных тестов было бы легче проверить, если бы метод draw рисовал что-то кроме реального System.Drawing.Graphics экземпляра).

Чтобы устранить зависимость логики от пространства имен System.Drawing, я подумал, что я бы объявил несколько новых интерфейсов, которые будут действовать как заполнители для типов System.Drawing, например:

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);
  }
}

Если бы я сделал это, то разные сборки могли бы иметь разные реализации интерфейса IMyFont и IMyGraphics, например ...

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);
  }
}

... однако реализация будет нуждаться в понижении, как показано выше.

У меня такой вопрос, есть ли способ сделать это без необходимости снижения производительности в реализации? Под "этим" я подразумеваю "определение UDT, таких как Word и Canvass, которые не зависит от конкретных типов бетона System?

Альтернативой могут быть абстрактные UDT ...

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); 
}

... но для реализации подкласса это тоже потребует снижения.

Я также думал об использовании идиомы двойной диспетчеризации, но это зависит от именования различных подклассов в API.

Или, если не с интерфейсами или подклассами, есть какой-то способ использования делегатов?


- Edit: -

Было два возможных ответа.

Один из ответов - использовать дженерики, именно так, как предложено в ответе «сэра Лантиса» ниже, и в соответствии с предложением в блоге, на которое ссылается Джон Скит. Я подозреваю, что это будет работать нормально в большинстве сценариев. Недостатком с моей точки зрения является то, что это означает введение TFont в качестве параметра шаблона: это не только класс, подобный Word (который содержит экземпляр Font), который должен стать универсальным классом ( как WordT<TFont>) ... также любой класс, который содержит WordT<TFont> (например, Paragraph), теперь также должен стать универсальным с параметром TFont (например, ParagraphT<TFont>). В конце концов, почти каждый класс в сборке стал общим классом. Это сохраняет безопасность типов и позволяет избежать необходимости понижать ... , но это немного уродливо и нарушает иллюзию инкапсуляции (иллюзия, что 'Font' является непрозрачной реализацией подробно).

Другой ответ - использовать карту или словарь в классе пользователя. Вместо Font в повторно используемой библиотеке и вместо абстрактного интерфейса определите класс «handle», например:

public struct FontHandle
{
  public readonly int handleValue;
  FontHandle(int handleValue)
  {
    this.handleValue = handleValue;
  }
}

Затем, вместо понижения с FontHandle, сохраните экземпляр Dictionary<int, Font>, который сопоставляет значения FontHandle с Font экземплярами.

Ответы [ 4 ]

2 голосов
/ 02 февраля 2009

Вы фактически говорите: «Я знаю лучше, чем компилятор - я знаю, что это обязательно будет экземпляр MyFont». В этот момент у вас снова возникает сильная связь MyFont и MyGraphics, что немного уменьшает точку интерфейса.

Должен ли MyGraphics работать с любым IFont или только с MyFont? Если вы можете заставить его работать с любым IFont, у вас все будет хорошо. В противном случае вам, возможно, придется взглянуть на сложные обобщенные элементы, чтобы все это было безопасно во время компиляции. Вы можете найти мой пост об обобщениях в Protocol Buffers полезным в качестве аналогичной ситуации.

(Дополнительное предложение - ваш код будет более идиоматически похожим на .NET, если вы будете следовать соглашениям об именах, которые включают регистр Pascal для методов.)

2 голосов
/ 02 февраля 2009

Во-первых, мне интересно, не является ли весь сценарий немного искусственным; Вы действительно собираетесь нуждаться в этом уровне абстракции? Возможно подписаться на YAGNI ?

Почему ваш MyGraphics работает только с MyFont? Может ли он работать с IFont? Это было бы лучшим использованием интерфейсов и позволило бы избежать всей этой проблемы ...

Один вариант может быть немного изменен, так что IFont просто описывает метаданные для шрифта (размер, шрифт и т. Д.), И у вас есть вещи на конкретном MyGraphics, такие как:

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

и переводом становится работа графики - так что тогда использовали что-то вроде:

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
}

Конечно, Point может потребоваться и перевод; -p

1 голос
/ 02 февраля 2009

В настоящее время я не совсем осведомлен о C # - уже давно. Но если вы не хотите разыгрывать там все свои вещи, вас могут заставить использовать дженерики.

Я могу просто предоставить код Java, но C # должен делать то же самое с помощью ключевого слова where.

Сделайте ваш интерфейс универсальным интерфейсом. На Java это будет

IMyGraphics<T extends IMyFont> а затем MyGraphics : IMyGraphics<MyFont>

Затем переопределите подпись drawString, чтобы принять T font в качестве второго параметра вместо IMyFont. Это должно позволить вам написать

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

прямо в ваш MyGraphics класс.


В C # IMyGraphics<T extends IMyFont> должно быть public interface IMyGraphics<T> where T:IMyFont, но я не уверен на 100% в этом.

0 голосов
/ 02 февраля 2009

Вам не нравится актёрский состав от IFont до MyFont? Вы можете сделать это:

interface IFont {
    object Font { get; }
}

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

Конечно, вам все еще нужно привести от System.Object к System.Drawing.Font в методе рисования, но вы только что устранили зависимость от конкретной реализации класса (MyFont).

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