Можно ли избежать уныния?
-
20-08-2019 - |
Вопрос
У меня есть некоторая логика, которая определяет и использует некоторые определяемые пользователем типы, например:
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);
}