是否可以避免沮丧?

发布于 2024-07-13 19:17:05 字数 3368 浏览 9 评论 0原文

我有一些逻辑,它定义并使用一些用户定义的类型,如下所示:

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

:我这样做了,然后不同的程序集可能有不同的 IMyFontIMyGraphics 接口实现,例如……

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

但是该实现需要向下转型,如上所示。

我的问题是,有没有办法在实现过程中不需要向下转型来做到这一点?“这个”,我的意思是“定义像 WordCanvass 这样的 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;
  }
}

: Dictionary实例,将 FontHandle 值映射到 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

原来分手还会想你 2024-07-20 19:17:05

首先,我想知道整个场景是不是有点做作; 您真的需要这种抽象级别吗? 也许订阅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

First, 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 a MyFont? Can it work with an IFont? 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 concrete MyGraphics like:

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

and it becomes the job of the graphics to do the translation - so then used something like:

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
}

Of course, Point might need translation too ;-p

淑女气质 2024-07-20 19:17:05

您实际上是在说“我比编译器更了解 - 我知道它必然是 MyFont 的实例。” 此时,MyFontMyGraphics 再次紧密耦合,这稍微减少了界面的要点。

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 got MyFont and MyGraphics being tightly coupled again, which reduces the point of the interface a bit.

Should MyGraphics work with any IFont, or only a MyFont? If you can make it work with any IFont 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.)

巴黎夜雨 2024-07-20 19:17:05

我目前不太了解 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 take T font as the second parameter instead of IMyFont. This should enable you to write

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

directly into your MyGraphics class.


In C# that IMyGraphics<T extends IMyFont> should be public interface IMyGraphics<T> where T:IMyFont, but I am not 100% sure about that.

绳情 2024-07-20 19:17:05

您不喜欢从 IFontMyFont 的转换? 您可以这样做:

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

You don't like the cast from IFont to MyFont? You can do this:

interface IFont {
    object Font { get; }
}

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

Sure you still need to cast from System.Object to System.Drawing.Font in the drawing method, but you've just eliminated the dependency on particular class implementation (MyFont).

public void DrawString(string text, IFont font, Point point)
{
    System.Drawing.Font f = (Font)font.Font;
    graphics.DrawString(text, f, Brushes.Block, point);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文