与相关类紧密耦合?
我有定制的向导系统,到目前为止还非常合适。对于大多数向导来说,页面可以以相当通用的方式构建,因此只有一个类实现这些类型的页面。然而,有些页面需要定制设计,因此这些类型的页面有一个抽象基类。由于VS设计器的一些缺陷,页面本身不能是一个UI控件,也不能是一个具有通用参数的抽象类(这些参数是为了流畅的编程而存在的)。因此,我遵循的一种选择是跨两个类实现页面,一个用于 UI(派生自 UserControl 并且可以设计),另一个用于页面。该页面包含 UI 控件的一个实例,并将其嵌入到自身中以供显示。不是最佳的,但它有效。
现在,这种设置产生了一个问题:UI 控件类和页面类之间存在紧密耦合。这通常不会成为一个大问题,除非可以从中派生页面来创建页面的专门版本。因此,派生的控件和页面类也与它们自身紧密耦合。因此,当我在控件和页面类上有分别为控件或页面类键入的成员变量、属性和方法时(即,控件类将有一个属性 Page
,它指向页面)控件嵌入),我们遇到了派生类的大问题。每个派生类必须以某种方式更改这些成员的类型。我想做的是包含一个泛型类型参数,该参数允许这些成员进行泛型类型化:
public class BaseControl<TControl, TPage>
where TPage : BasePage<TPage, TControl>
where TControl : BaseControl<TControl, TPage> {
public TPage Page { get { ... } set { ... } }
...
}
public class BasePage<TPage, TControl>
where TPage : BasePage<TPage, TControl>
where TControl : BaseControl<TControl, TPage> {
public TControl Control { get { ... } set { ... }
...
}
public class DerivedControl<TControl, TPage> : BaseControl<TControl, TPage>
where TControl : DerivedControl<TControl, TPage>
where TPage : DerivedPage<TPage, TControl> { }
public class DerivedPage<TPage, TControl> : BasePage<TPage, TControl>
where TControl : DerivedControl<TControl, TPage>
where TPage : DerivedPage<TPage, TControl> { }
显然,这是我想避免的那种 C++ 风格的垃圾。除了丑陋之外,它还产生了一个实际问题,即必须创建密封的“叶”类来解决 CRTP 给我们带来的无限递归问题。
然而,其他选择也没有吸引力。我可以让这些成员具有固定的基本类型,并在任何地方进行转换。这不会强制类型安全,并且需要毫无意义的强制转换(我已经知道类型将是这样那样,但编译器不知道)。我可以吸收它并将页面和控件类合并为一个,而不需要抽象或泛型类型参数。这会破坏流畅的编程系统,或者使其更难以实现,而且会重复(如果需要,我会解释,但我们假设我的设计的那部分是合法的)。
因此,我有点不知道如何以理智的方式做到这一点。到目前为止我考虑过的一切都可以工作,但是代码味道很糟糕。是否有什么我遗漏的东西,或者到目前为止我还没有找到的某种方法?
I have custom-made wizard system which so far has been quite suitable. For most wizards, the pages can be constructed in a fairly generic fashion, so only one class implements those types of pages. However, some need to be custom-designed, so there is an abstract base class for those kinds of pages. Due to some deficiencies in the VS designer, the page can't itself be a UI control and also be an abstract class with generic parameters (those exist for fluent programming). So one option I've followed is to implement the page across two classes, one for the UI (which derives from UserControl and can be designed) and one for the page. The page contains an instance of the UI control and embeds that in itself for display. Not optimal, but it works.
Now, a problem arises from this set up: there is a tight coupling between the UI control class and the page class. This wouldn't normally be that much of an issue except that pages can be derived from to create specialized versions of the pages. So the derived control and page classes are also tightly coupled with themselves. So when I have member variables, properties and methods on the control and page classes that are typed for the control or page class, respectively (i.e., the control class will have a property Page
which points to the page the control is embedded in), we run into a big problem with derived classes. Each derived class must somehow change the type of these members. What I had thought of doing was including a generic type parameter that would allow those members to be generically typed:
public class BaseControl<TControl, TPage>
where TPage : BasePage<TPage, TControl>
where TControl : BaseControl<TControl, TPage> {
public TPage Page { get { ... } set { ... } }
...
}
public class BasePage<TPage, TControl>
where TPage : BasePage<TPage, TControl>
where TControl : BaseControl<TControl, TPage> {
public TControl Control { get { ... } set { ... }
...
}
public class DerivedControl<TControl, TPage> : BaseControl<TControl, TPage>
where TControl : DerivedControl<TControl, TPage>
where TPage : DerivedPage<TPage, TControl> { }
public class DerivedPage<TPage, TControl> : BasePage<TPage, TControl>
where TControl : DerivedControl<TControl, TPage>
where TPage : DerivedPage<TPage, TControl> { }
Obviously, this is the kind of C++-style garbage that I'd like to avoid. Besides the ugliness, it creates an actual problem wherein one must create sealed "leaf" classes to work around the infinite recursion problem that CRTP brings us.
And yet the alternatives are also unappealing. I could have those members have a fixed type of the base type and do casting everywhere. This doesn't enforce type safety and it requires pointless casts (I already know that the type will be such-and-such, but the compiler doesn't). I could suck it up and combine the page and the control class into one, with no abstract
or generic type parameters. That ruins the fluent programming system, or makes it much harder to implement, with much repetition (I'll explain if needed, but let's just assume that that part of my design is legitimate).
As such, I'm a bit at a loss for how to do this in a sane manner. Everything I've considered so far can be made to work, but the code smell is horrible. Is there something I'm missing, some way of doing this that has so far eluded me?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我在我的 Protocol Buffers 端口中有非常类似的代码,其中有一条消息类型和该消息类型的构建器是耦合的。
基本上,您试图表达一种 C# 泛型使之尴尬的关系。
就我而言,大部分内容都隐藏在生成的代码中,因此开发人员实际上不需要做太多事情......但它仍然很丑陋。我确实寻找过替代方案,但没有找到。我认为你所拥有的很可能是你能得到的最好的,恐怕......假设你写的关于 VS 约束的内容和你的其他要求是正确的。如果您发现自己能够使用非常不同的设计,那就太好了 - 但如果您需要两种相关的类型,我认为所有这些“垃圾”都是必需的:(
I have very similar code in my Protocol Buffers port, where a message type and a builder for that message type are coupled.
Basically, you're trying to express a relationship which C# generics makes awkward.
In my case most of this is hidden in generated code, so the dev doesn't need to actually do very much... but it's still ugly. I did look for alternatives, but I didn't find any. I think what you've got may well be as good as you can get, I'm afraid... assuming what you wrote about the VS constraints and your other requirements is correct. If you find yourself able to work in a very different design, that's great - but if you need the two related types, I think all of this "junk" is required :(