У меня есть некоторая логика, которая определяет и использует некоторые пользовательские типы, такие как:
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
экземплярами.