Вопрос

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

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.

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


--Редактировать:--

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

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

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

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

Тогда вместо понижения FontHandle, держать Dictionary<int, Font> экземпляр, который отображает FontHandle ценности для Font экземпляры.

Это было полезно?

Решение

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

Почему ваш 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

Другие советы

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

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

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

В настоящее время я больше не совсем разбираюсь в 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%.

Вам не нравится актерский состав 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);
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top