是否可以避免沮丧?
我有一些逻辑,它定义并使用一些用户定义的类型,如下所示:
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 命名空间:主要是为了帮助单元测试(我认为单元测试的输出会更容易验证是否绘制 方法正在绘制到除真实的 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);
}
}
但是该实现需要向下转型,如上所示。
我的问题是,有没有办法在实现过程中不需要向下转型来做到这一点?“这个”,我的意思是“定义像 Word
和 Canvass 这样的 UDT”
不依赖于特定的具体系统
类型”?
另一种选择是抽象 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
)...它也是包含 WordT
的任何类(例如 Paragraph
)现在还需要通过 TFont
参数(例如 ParagraphT
)变得通用。 最终,程序集中的几乎每个类都变成了泛型类。 这确实保留了类型安全并避免了向下转换的需要...但是它有点丑陋,并且扰乱了封装的幻觉(“Font”是一个幻觉)不透明的实施细节)。
另一个答案是在用户类中使用映射或字典。 定义一个“句柄”类,而不是可重用库中的 Font
,也不是抽象接口
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
: DictionaryFontHandle
值映射到 Font
实例。
I have some logic, which defines and uses some user-defined types, like these:
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);
}
}
The logic, after doing calculations with the types (e.g. to locate each Word
instance), indirectly uses some System
APIs, for example by invoking the Canvass.draw
method.
I'd like to make this logic independent of the System.Drawing
namespace: mostly, in order to help with unit testing (I think unit tests' output would be easier to verify if the draw
method were drawing to something other than a real System.Drawing.Graphics
instance).
To eliminate the logic's dependency on the System.Drawing
namespace, I thought I'd declare some new interfaces to act as placeholders for the System.Drawing
types, for example:
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);
}
}
If I did this, then different assemblies could have different implementations of the IMyFont
and IMyGraphics
interface, for example ...
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);
}
}
... however the implementation would need an downcast as illustrated above.
My question is, is there a way to do this without needing a downcast in the implementation? By "this", I mean "defining UDTs like Word
and Canvass
which don't depend on specific concrete System
types"?
An alternative would be abstract 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);
}
... but this too would need a downcast in the implementation of the subclass.
I also thought of using the double dispatch idiom, but it depends on naming the various subclasses in the APIs.
Or, if not with interfaces or subclasses, is there some way using delegates?
--Edit:--
There have been two possible answers.
One answer is to use generics, precisely as suggested by 'Sir Lantis' answer below, and as suggested by the blog post to which John Skeet linked. I suspect this would work fine in most scenarios. The down-side from my point of view is that it means introducing TFont
as a template parameter: it isn't only a class like Word
(which contains a Font
instance) which needs to become a generic class (like WordT<TFont>
) ... it's also that any class which contains a WordT<TFont>
(e.g. Paragraph
) now also needs to become generic with a TFont
parameter (e.g. ParagraphT<TFont>
). Eventually, almost every class in the assembly has become a generic class. This does preserve type-safety and avoid the need to downcast ... but it's kind of ugly, and disturbs the illusion of encapsulation (the illusion that 'Font' is an opaque implementation detail).
Another answer is to use a map or dictionary in the user class. Instead of Font
in the reusable library, and instead of an abstract interface, define a 'handle' class like:
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
Then, instead of downcasting from FontHandle
, keep a Dictionary<int, Font>
instance which maps FontHandle
values to Font
instances.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
首先,我想知道整个场景是不是有点做作; 您真的需要这种抽象级别吗? 也许订阅YAGNI?
为什么您的
MyGraphics
只能与MyFont
配合使用? 它可以与IFont
一起使用吗? 这将是更好地使用界面,并且可以避免整个问题...一个选项可能是重新设计一点,以便 IFont 仅描述字体的元数据(大小) 、字体等),并且您在具体的
MyGraphics
上有一些东西,例如:并且它成为图形的工作来进行翻译 - 所以然后使用类似的东西:
当然,
Point
可能也需要翻译;-pFirst, I wonder if the entire scenario isn't a little artificial; are you really going to need this level of abstraction? Perhaps subscribe to YAGNI?
Why does your
MyGraphics
only work with aMyFont
? Can it work with anIFont
? That would be a better use of interfaces, and would avoid this entire issue...One option might be a bit of a re-design, so that the
IFont
just describes the metadata for the font (size, font-face, etc), and you have things on the concreteMyGraphics
like:and it becomes the job of the graphics to do the translation - so then used something like:
Of course,
Point
might need translation too ;-p您实际上是在说“我比编译器更了解 - 我知道它必然是
MyFont
的实例。” 此时,MyFont
和MyGraphics
再次紧密耦合,这稍微减少了界面的要点。MyGraphics
应该与任何IFont
一起使用,还是仅与MyFont
一起使用? 如果您可以使其与任何IFont
兼容,那就没问题了。 否则,您可能需要查看复杂的泛型以使其在编译时类型安全。 您可能会发现 我关于 Protocol Buffers 中泛型的帖子对于类似的情况很有用。(侧面建议 - 如果您遵循命名约定(包括方法的 Pascal 大小写),您的代码将更符合 .NET 习惯。)
You're effectively saying "I know better than the compiler - I know that it's bound to be an instance of
MyFont
." At that point you've gotMyFont
andMyGraphics
being tightly coupled again, which reduces the point of the interface a bit.Should
MyGraphics
work with anyIFont
, or only aMyFont
? If you can make it work with anyIFont
you'll be fine. Otherwise, you may need to look at complicated generics to make it all compile-time type safe. You may find my post on generics in Protocol Buffers useful as a similar situation.(Side suggestion - your code will be more idiomatically .NET-like if you follow the naming conventions, which includes Pascal case for methods.)
我目前不太了解 C# - 已经有一段时间了。 但是,如果您不想将所有内容都放在那里,则可能被迫使用泛型。
我可以只提供 Java 代码,但 C# 应该能够通过
where
关键字执行相同的操作。使您的界面成为通用界面。 在 Java 中,这将是
IMyGraphics
然后
MyGraphics : IMyGraphics
然后重新定义
drawString
签名,将T font
作为第二个参数,而不是IMyFont. 这应该使您能够将
public void drawString(string text, MyFont font, Point point)
直接写入
MyGraphics
类中。在 C# 中,
IMyGraphics
应该是公共接口 IMyGraphics其中 T:IMyFont
,但我对此不是 100% 确定。I am currently not quite aware of C# anymore - has been some time now. But if you do not want to cast all your stuff there, you might be forced to use generics.
I can just provide Java code, but C# should be able to do the same via the
where
keyword.Make your Interface a generic interface. In Java that would be
IMyGraphics<T extends IMyFont>
and then
MyGraphics : IMyGraphics<MyFont>
Then redefine the
drawString
Signature to takeT font
as the second parameter instead ofIMyFont
. This should enable you to writepublic void drawString(string text, MyFont font, Point point)
directly into your
MyGraphics
class.In C# that
IMyGraphics<T extends IMyFont>
should bepublic interface IMyGraphics<T> where T:IMyFont
, but I am not 100% sure about that.您不喜欢从
IFont
到MyFont
的转换? 您可以这样做:当然,您仍然需要在绘图方法中从
System.Object
转换为System.Drawing.Font
,但您刚刚消除了对特定的依赖类实现 (MyFont
)。You don't like the cast from
IFont
toMyFont
? You can do this:Sure you still need to cast from
System.Object
toSystem.Drawing.Font
in the drawing method, but you've just eliminated the dependency on particular class implementation (MyFont
).